愿许秋风知我意,解我心中意难平。
项目介绍
项目准备
推荐使用,
每个程序员都有自己的管理方式。
验证码登录
HTML结构:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.3/css/bootstrap.min.css"> <link rel="stylesheet" href="./index.css"> <title>黑马头条-数据管理平台</title></head><body> <!-- 警告框 --> <div class="alert info-box"> 操作结果 </div> <!-- 登录页面 --> <div class="login-wrap"> <div class="title">黑马头条</div> <div> <form class="login-form"> <div class="item"> <input type="text" class="form-control" name="mobile" placeholder="请输入手机号" value="13888888888"> </div> <div class="item"> <input type="text" class="form-control" name="code" placeholder="默认验证码246810" value="246810"> </div> <div class="item"> <button type="button" class="btn btn-primary btn">登 录</button> </div> </form> </div> </div> <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.4/axios.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.3/js/bootstrap.min.js"></script> <script src="../../lib/form-serialize.js"></script> <script src="../../utils/request.js"></script> <script src="../../utils/alert.js"></script> <script src="./index.js"></script></body></html>
1.为什么要提取公共前缀地址(基地址),因为公司业务可能会更换服务器,如果你不想一条一条地修改请求地址的话。
后续使用axios时,url不需要再写前缀。
2.请求成功与失败
成功返回message“OK”还有token等(作用后续讲)
输入错误验证码,请求失败的时候,返回message“验证码不正确”
message可用于提示框文字显示
3.提示框控制
之前的案例已经做过了。把之前封装的函数放到utils下,
alert.js
// 弹窗插件// 需要先准备 alert 样式相关的 DOM/** * BS 的 Alert 警告框函数,2秒后自动消失 * @param {*} isSuccess 成功 true,失败 false * @param {*} msg 提示消息 */function myAlert(isSuccess, msg) { const myAlert = document.querySelector('.alert') myAlert.classList.add(isSuccess ? 'alert-success' : 'alert-danger') myAlert.innerHTML = msg myAlert.classList.add('show') setTimeout(() => { myAlert.classList.remove(isSuccess ? 'alert-success' : 'alert-danger') myAlert.innerHTML = '' myAlert.classList.remove('show') }, 2000)}
成功的提示
错误的提示
验证码登录-流程
token 的介绍
token的正确打开方式:
判断无token令牌字符串,则强制跳转到登录页
强制访问内容页面
被踢回登录页面
登录成功后保存token到本地
登录成功后跳转到内容页面
延时跳转,展示alert警告框
个人信息设置和 axios 请求拦截器
问题:很多接口,都需要携带 token 令牌字符串
解决:在请求拦截器统一设置公共 headers 选项
第二个函数的应用场景非常少,一般也可以不写第二个函数体
请求拦截器统一携带token令牌
如此使用
内容管理页面和发布文章页面的html文件都引入了auth.js
个人信息显示成功
axios 响应拦截器和身份验证失败
axios 响应拦截器:响应回到 then/catch 之前,触发的拦截函数,对响应结果统一处理
Axios中文文档 | Axios中文网
请求拦截器,响应拦截器,拦截器:在做出对应动作前(在执行发送请求或 响应回到then/catch之前)进行拦截,补充额外的统一的操作。
制作错误:修改token值(不可以删除,前端只判断有无token,无token将跳转登录界面;后端判断有效性)
console.dir打印错误信息
在response中找到响应状态码
对响应状态码进行判断 === 401 (注意使用可选链操作符):
对401身份验证失败情况做出处理:
①错误提示②清理过期token③跳转登录界面
TEST
先登录进去
修改token
点击确定后跳转,缓存被清空
实操代码(request.js):
// axios 公共配置// 基地址axios.defaults.baseURL = 'http://geek.itheima.net'// 添加请求拦截器axios.interceptors.request.use(function (config) { // 在发送请求之前做些什么 // 统一携带 token 令牌字符串在请求头上 const token = localStorage.getItem('token') token && (config.headers.Authorization = `Bearer ${token}`) return config;}, function (error) { // 对请求错误做些什么 return Promise.reject(error);});// 添加响应拦截器axios.interceptors.response.use(function (response) { // 2xx 范围内的状态码都会触发该函数。 // 对响应数据做点什么}, function (error) { // 超出 2xx 范围的状态码都会触发该函数。 // 对响应错误做点什么,例如:统一对 401 身份验证失败情况做出处理 console.dir(error) if (error?.response?.status === 401) { alert('身份验证失败,请重新登录') localStorage.clear() location.href = '../login/index.html' } return Promise.reject(error);});
优化-axios 响应结果
背景:axios中返回的result(请求成功的结果对象)如右上,为axios内部封装的结果对象,它把服务器返回的数据对象挂载到了data属性下。
目标:axios 直接接收服务器返回的响应结果
注意:响应拦截器成功函数中的response参数即result结果对象
实践代码:
// axios 公共配置// 基地址axios.defaults.baseURL = 'http://geek.itheima.net'// 添加请求拦截器axios.interceptors.request.use(function (config) { // 在发送请求之前做些什么 // 统一携带 token 令牌字符串在请求头上 const token = localStorage.getItem('token') token && (config.headers.Authorization = `Bearer ${token}`) return config;}, function (error) { // 对请求错误做些什么 return Promise.reject(error);});// 添加响应拦截器axios.interceptors.response.use(function (response) { // 2xx 范围内的状态码都会触发该函数。 // 对响应数据做点什么,例如:直接返回服务器的响应结果对象 const result = response.data return result;}, function (error) { // 超出 2xx 范围的状态码都会触发该函数。 // 对响应错误做点什么,例如:统一对 401 身份验证失败情况做出处理 console.dir(error) if (error?.response?.status === 401) { alert('身份验证失败,请重新登录') localStorage.clear() location.href = '../login/index.html' } return Promise.reject(error);});
修改之前使用服务器返回数据的写法(去掉一个.data)
测试效果:
修改代码后,个人信息正常展示
登录后token值正常保存
发布文章-富文本编辑器
什么叫富文本呢?——带样式、多格式的文本
使用插件wangEditor实现富文本编辑器
123步骤在官方文档-快速开始:wangEditor
JS创建编辑器语句逐句解析:(独立存放于editor.js)
// 富文本编辑器// 创建编辑器函数,创建工具栏函数const { createEditor, createToolbar } = window.wangEditor// 编辑器配置对象const editorConfig = { // 占位提示文字 placeholder: '发布文章内容...', // 编辑器变化时回调函数 onChange(editor) { // 获取富文本内容 const html = editor.getHtml() // 也可以同步到 <textarea> // 为了后续快速收集整个表单内容做铺垫 document.querySelector('.publish-content').value = html }}// 创建编辑器const editor = createEditor({ // 创建位置 selector: '#editor-container', // 默认内容 html: '<p><br></p>', // 配置项 config: editorConfig, // 配置集成模式(default 全部)(simple 简洁) mode: 'default', // or 'simple'})// 工具栏配置对象const toolbarConfig = {}// 创建工具栏const toolbar = createToolbar({ // 为指定编辑器创建工具栏 editor, // 工具栏创建的位置 selector: '#toolbar-container', // 工具栏配置对象 config: toolbarConfig, // 配置集成模式 mode: 'default', // or 'simple'})
4.监听内容改变,保存在隐藏文本域(便于后期收集)
因为该富文本编辑器输入内容的地方实际上是p标签,而不是在textarea(隐藏)内
浏览器中检查可发现
需要准备textarea用于收集文本内容,将其隐藏
发布文章-频道列表
编写代码之前要想一想,这段代码要不要复用呢?
发布文章页面中需要展示频道列表
内容管理页面也需要展示频道列表
完整代码:
发布文章-封面设置
目标:点击加号框(或“封面”文字)上传图片作为封面
input type属性值为file 的表单元素很难改成加号框
所以,加号盒子(label,所以监听的是input元素的change事件),img封面盒子(img)和input表单元素(input)为三个标签
封面回显的时候,需要使加号盒子隐藏,img显示(rounded类选择器使其隐藏)
下面是提前写好的有关隐藏和显示的相关样式
上传图片接口要求传入表单数据FormData
上传请求的响应结果包含图片的URL地址,用于回显
优化:点击 img 可以重新切换封面
实践代码(publish/index.js):
/** * 目标1:设置频道下拉菜单 * 1.1 获取频道列表数据 * 1.2 展示到下拉菜单中 */// 1.1 获取频道列表数据async function setChannleList() { const res = await axios({ url: '/v1_0/channels' }) // 1.2 展示到下拉菜单中 const htmlStr = `<option value="" selected="">请选择文章频道</option>` + res.data.channels.map(item => `<option value="${item.id}">${item.name}</option>`).join('') document.querySelector('.form-select').innerHTML = htmlStr}// 网页运行后,默认调用一次setChannleList()/** * 目标2:文章封面设置 * 2.1 准备标签结构和样式 * 2.2 选择文件并保存在 FormData * 2.3 单独上传图片并得到图片 URL 网址 * 2.4 回显并切换 img 标签展示(隐藏 + 号上传标签) */// 2.2 选择文件并保存在 FormDatadocument.querySelector('.img-file').addEventListener('change', async e => { const file = e.target.files[0] const fd = new FormData() fd.append('image', file) // 2.3 单独上传图片并得到图片 URL 网址 const res = await axios({ url: '/v1_0/upload', method: 'POST', data: fd }) // 2.4 回显并切换 img 标签展示(隐藏 + 号上传标签) const imgUrl = res.data.url document.querySelector('.rounded').src = imgUrl document.querySelector('.rounded').classList.add('show') document.querySelector('.place').classList.add('hide')})// 优化:点击 img 可以重新切换封面// 思路:img 点击 => 用 JS 方式触发文件选择元素 click 事件方法document.querySelector('.rounded').addEventListener('click', () => { document.querySelector('.img-file').click()})
注意:此时,图片(封面)地址临时存储在 img 标签上,并未和文章关联保存
发布文章-收集并保存
实现代码:
/** * 目标3:发布文章保存 * 3.1 基于 form-serialize 插件收集表单数据对象 * 3.2 基于 axios 提交到服务器保存 * 3.3 调用 Alert 警告框反馈结果给用户 * 3.4 重置表单并跳转到列表页 */// 3.1 基于 form-serialize 插件收集表单数据对象document.querySelector('.send').addEventListener('click', async e => { if (e.target.innerHTML !== '发布') return const form = document.querySelector('.art-form') const data = serialize(form, { hash: true, empty: true }) // 发布文章的时候,不需要 id 属性,所以可以删除掉(id 为了后续做编辑使用) delete data.id console.log(data) // 自己收集封面图片地址并保存到 data 对象中 data.cover = { type: 1, // 封面类型 images: [document.querySelector('.rounded').src] // 封面图片 URL 网址 } // 3.2 基于 axios 提交到服务器保存 try { const res = await axios({ url: '/v1_0/mp/articles', method: 'POST', data: data }) // 3.3 调用 Alert 警告框反馈结果给用户 myAlert(true, '发布成功') // 3.4 重置表单并跳转到列表页 form.reset() // 封面需要手动重置 document.querySelector('.rounded').src = '' document.querySelector('.rounded').classList.remove('show') document.querySelector('.place').classList.remove('hide') // 富文本编辑器重置 editor.setHtml('') setTimeout(() => { location.href = '../content/index.html' }, 1500) } catch (error) { myAlert(false, error.response.data.message) }})
await错误捕捉,使用try...catch语句进行
错误不捕捉:
捕捉错误并打印
内容管理-文章列表展示
实践代码(content/index.js):
/** * 目标1:获取文章列表并展示 * 1.1 准备查询参数对象 * 1.2 获取文章列表数据 * 1.3 展示到指定的标签结构中 */// 1.1 准备查询参数对象const queryObj = { status: '', // 文章状态(1-待审核,2-审核通过)空字符串-全部 channel_id: '', // 文章频道 id,空字符串-全部 page: 1, // 当前页码 per_page: 2 // 当前页面条数}let totalCount = 0 // 保存文章总条数// 获取并设置文章列表async function setArtileList() { // 1.2 获取文章列表数据 const res = await axios({ url: '/v1_0/mp/articles', params: queryObj }) // 1.3 展示到指定的标签结构中 const htmlStr = res.data.results.map(item => `<tr> <td> <img src="${item.cover.type === 0 ? `https://img2.baidu.com/it/u=2640406343,1419332367&fm=253&fmt=auto&app=138&f=JPEG?w=708&h=500`: item.cover.images[0]}" alt=""> </td> <td>${item.title}</td> <td> ${item.status === 1 ? `<span class="badge text-bg-primary">待审核</span>` : `<span class="badge text-bg-success">审核通过</span>`} </td> <td> <span>${item.pubdate}</span> </td> <td> <span>${item.read_count}</span> </td> <td> <span>${item.comment_count}</span> </td> <td> <span>${item.like_count}</span> </td> <td data-id="${item.id}"> <i class="bi bi-pencil-square edit"></i> <i class="bi bi-trash3 del"></i> </td></tr>`).join('') document.querySelector('.art-list').innerHTML = htmlStr
内容管理-筛选功能
实践代码:
/** * 目标2:筛选文章列表 * 2.1 设置频道列表数据 * 2.2 监听筛选条件改变,保存查询信息到查询参数对象 * 2.3 点击筛选时,传递查询参数对象到服务器 * 2.4 获取匹配数据,覆盖到页面展示 */// 2.1 设置频道列表数据async function setChannleList() { const res = await axios({ url: '/v1_0/channels' }) const htmlStr = `<option value="" selected="">请选择文章频道</option>` + res.data.channels.map(item => `<option value="${item.id}">${item.name}</option>`).join('') document.querySelector('.form-select').innerHTML = htmlStr}setChannleList()// 2.2 监听筛选条件改变,保存查询信息到查询参数对象// 筛选状态标记数字->change事件->绑定到查询参数对象上document.querySelectorAll('.form-check-input').forEach(radio => { radio.addEventListener('change', e => { queryObj.status = e.target.value })})// 筛选频道 id -> change事件 -> 绑定到查询参数对象上document.querySelector('.form-select').addEventListener('change', e => { queryObj.channel_id = e.target.value})// 2.3 点击筛选时,传递查询参数对象到服务器document.querySelector('.sel-btn').addEventListener('click', () => { // 2.4 获取匹配数据,覆盖到页面展示 setArtileList()})
内容管理-分页功能
实践代码:
/** * 目标3:分页功能 * 3.1 保存并设置文章总条数 * 3.2 点击下一页,做临界值判断,并切换页码参数并请求最新数据 * 3.3 点击上一页,做临界值判断,并切换页码参数并请求最新数据 */// 3.2 点击下一页,做临界值判断,并切换页码参数并请求最新数据document.querySelector('.next').addEventListener('click', e => { // 当前页码小于最大页码数 if (queryObj.page < Math.ceil(totalCount / queryObj.per_page)) { queryObj.page++ document.querySelector('.page-now').innerHTML = `第 ${queryObj.page} 页` setArtileList() }})// 3.3 点击上一页,做临界值判断,并切换页码参数并请求最新数据document.querySelector('.last').addEventListener('click', e => { // 大于 1 的时候,才能翻到上一页 if (queryObj.page > 1) { queryObj.page-- document.querySelector('.page-now').innerHTML = `第 ${queryObj.page} 页` setArtileList() }})
内容管理-删除功能
实践代码:
/** * 目标4:删除功能 * 4.1 关联文章 id 到删除图标 * 4.2 点击删除时,获取文章 id * 4.3 调用删除接口,传递文章 id 到服务器 * 4.4 重新获取文章列表,并覆盖展示 * 4.5 删除最后一页的最后一条,需要自动向前翻页 */// 4.2 点击删除时,获取文章 iddocument.querySelector('.art-list').addEventListener('click', async e => { // 判断点击的是删除元素 if (e.target.classList.contains('del')) { const delId = e.target.parentNode.dataset.id // 4.3 调用删除接口,传递文章 id 到服务器 const res = await axios({ url: `/v1_0/mp/articles/${delId}`, method: 'DELETE' }) // 4.5 删除最后一页的最后一条,需要自动向前翻页 const children = document.querySelector('.art-list').children if (children.length === 1 && queryObj.page !== 1) { queryObj.page-- document.querySelector('.page-now').innerHTML = `第 ${queryObj.page} 页` } // 4.4 重新获取文章列表,并覆盖展示 setArtileList() }})
内容管理-删除最后一条
代码:
内容管理-编辑文章-回显
实践代码:
/** * 目标4:编辑-回显文章 * 4.1 页面跳转传参(URL 查询参数方式) * 4.2 发布文章页面接收参数判断(共用同一套表单) * 4.3 修改标题和按钮文字 * 4.4 获取文章详情数据并回显表单 */ ; (function () { // 4.2 发布文章页面接收参数判断(共用同一套表单) const paramsStr = location.search const params = new URLSearchParams(paramsStr) params.forEach(async (value, key) => { // 当前有要编辑的文章 id 被传入过来 if (key === 'id') { // 4.3 修改标题和按钮文字 document.querySelector('.title span').innerHTML = '修改文章' document.querySelector('.send').innerHTML = '修改' // 4.4 获取文章详情数据并回显表单 const res = await axios({ url: `/v1_0/mp/articles/${value}` }) console.log(res) // 组织我仅仅需要的数据对象,为后续遍历回显到页面上做铺垫 const dataObj = { channel_id: res.data.channel_id, title: res.data.title, rounded: res.data.cover.images[0], // 封面图片地址 content: res.data.content, id: res.data.id } // 遍历数据对象属性,映射到页面元素上,快速赋值 Object.keys(dataObj).forEach(key => { if (key === 'rounded') { // 封面设置 if (dataObj[key]) { // 有封面 document.querySelector('.rounded').src = dataObj[key] document.querySelector('.rounded').classList.add('show') document.querySelector('.place').classList.add('hide') } } else if (key === 'content') { // 富文本内容 editor.setHtml(dataObj[key]) } else { // 用数据对象属性名,作为标签 name 属性选择器值来找到匹配的标签 document.querySelector(`[name=${key}]`).value = dataObj[key] } }) } }) })();
内容管理-编辑文章-保存
给按钮绑定了两个函数,都会走一遍
但需要进行判断
/** * 目标5:编辑-保存文章 * 5.1 判断按钮文字,区分业务(因为共用一套表单) * 5.2 调用编辑文章接口,保存信息到服务器 * 5.3 基于 Alert 反馈结果消息给用户 */document.querySelector('.send').addEventListener('click', async e => { // 5.1 判断按钮文字,区分业务(因为共用一套表单) if (e.target.innerHTML !== '修改') return // 修改文章逻辑 const form = document.querySelector('.art-form') const data = serialize(form, { hash: true, empty: true }) // 5.2 调用编辑文章接口,保存信息到服务器 try { const res = await axios({ url: `/v1_0/mp/articles/${data.id}`, method: 'PUT', data: { ...data, cover: { type: document.querySelector('.rounded').src ? 1 : 0, images: [document.querySelector('.rounded').src] } } }) console.log(res) myAlert(true, '修改文章成功') } catch (error) { myAlert(false, error.response.data.message) }})
退出登录
/** * 目标3:退出登录 * 3.1 绑定点击事件 * 3.2 清空本地缓存,跳转到登录页面 */// 3.1 绑定点击事件document.querySelector('.quit').addEventListener('click', e => { // 3.2 清空本地缓存,跳转到登录页面 localStorage.clear() location.href = '../login/index.html'})