【AJAX项目】黑马头条——数据管理平台
目录
一、 项目准备
二、 验证码登录
真正的验证码登录原理:
三、 token的介绍
概念:访问权限的令牌,本质上是一串字符串
创建:正确登录后,由后端签发并返回
作用:判断是否有登录状态等,控制访问权限
四、个人信息设置和axios请求拦截器
五、axios响应拦截器
优化-axios响应结果
六、发布文章 - 富文本编辑器 (wangEditor插件)
七、发布文章-频道列表
八、发布文章 - 封面设置
九、发布文章 - 收集并保存
十、内容管理 - 文章列表展示
十一、内容管理 - 筛选功能
十二、内容管理 - 分页功能
十三、内容管理 - 删除功能
十四、内容管理-编辑文章-回显
十五、编辑文字-保存
总结不易~ 本章节对我有很大收获,希望对你也是!!!
本节素材已上传至Gitee:ajax_study: 这是ajax、Node.j学习的仓库 - Gitee.comhttps://gitee.com/liu-yihao-hhh/ajax_study/tree/master/%E9%A1%B9%E7%9B%AE-%E6%95%B0%E6%8D%AE%E7%AE%A1%E7%90%86
一、 项目准备
技术:
- 基于BootStrap 搭建网站标签和样式
- 集成wangEditor插件实现富文本编辑器
- 使用原生JS完成增删改查等业务
- 基于axios与黑马头条线上接口交互
- 使用axios拦截器进行权限判断
包含html, css, js, 静态图片, 第三方插件
目录管理:
- assets: 资源文件夹(图片、字体等)
- lib: 资料文件夹(第三方插件,例如:form-serialize)
- page: 页面文件夹
- utils: 实用程序文件夹(工具插件)
二、 验证码登录
- 目标:完成验证码登录,后端设置验证码默认为246810
- 原因:因为短信接口不是免费的,防止攻击者恶意盗刷
步骤:
- 再utils/request.js配置axios请求基地址
- 作用:提取公共前缀地址,配置后axios请求时都会baseURL + url 防止项目太大,基地址发生变化会导致所有页面请求数据地址全要变动
- 收集手机号和验证码数据
- 基于axios调用验证码登录接口
- 使用BootStrap的Alert警告框反馈结果给用户
收集表单数据,上传到服务器,然后通过try……catch……来判断是否请求成功,通过myAlert函数来进行登录效果弹窗
// 1.2 收集手机号和验证码数据document.querySelector(\'.btn\').addEventListener(\'click\', async () => { const form = document.querySelector(\'.login-form\') const data = serialize(form, { hash: true, empty: true }) console.log(data) try { // 1.3 基于 axios 调用验证码登录接口 const response = await axios.post(\'/v1_0/authorizations\', data) const result = response.data console.log(\'#\', result) myAlert(true, \'登录成功\') } catch (error) { myAlert(false, error.response.data.message) }})
alert检查验证码和手机号是否正确的弹窗
// 弹窗插件// 需要先准备 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)}
真正的验证码登录原理:
- 用户操作触发:用户在登录界面输入手机号码,主动点击 “发送验证码” 按钮,发起获取验证码流程,这是整个登录校验的起始交互 。
- 调用发送接口:系统携带用户输入的手机号码,调用专门用于发送短信验证码的服务器接口,传递手机号码参数,请求服务端执行发送验证码前置操作 。
- 服务端生成校验码:服务器接收到请求后,为该手机号码生成唯一的验证码,同时记录验证码生成时间,将这些信息存储在服务器,用于后续验证匹配 。
- 调用运营商通道:服务器携带手机号码,调用运营商(电信、移动、联通 )提供的短信发送接口,借助运营商网络能力传递验证码下发指令 。
- 基站下发短信:运营商通过基站,以无连接的短信方式,将包含验证码的信息发送到指定手机号码对应的终端设备 。
- 反馈发送状态:运营商将短信发送结果(成功 / 失败等 )返回给服务器,告知验证码是否已成功进入下发流程 。
- 返回前端结果:服务器把验证码发送成功的状态反馈给前端页面,让用户知晓验证码已尝试下发 。
- 用户填写验证码:用户查看手机收到的短信,获取验证码内容后,手动填写到登录页面的验证码输入框,完成信息回填 。
- 发起登录验证:用户点击 “登录”,系统携带手机号码和填写的验证码,调用服务器的验证码登录验证接口,提交验证请求 。
- 服务端校验逻辑:服务器接收请求后,拿收到的手机号码、验证码,与第 3 步生成存储的验证码记录(含号码、验证码、生成时间 )比对,校验验证码是否正确、是否在有效期(基于生成时间判断 ),然后返回登录成功或失败结果 。
三、 token的介绍
概念:访问权限的令牌,本质上是一串字符串
创建:正确登录后,由后端签发并返回
eyJzX0BAiOiJkV1QLChbc6IojT2UziIN19.eyJpc3MiOiJodHRwczovL3dL5dy5pdmNhc3RQVyJ2F1iOiJ3iCVjIof0wIY2THNnGEYJN5COUZDYLT110WOdGQYZ2ZIzUYJtxW11amR10j0wWZMMNJet2UY5DGYz3L7G4MKcAT2M2K62KtgNDcz21YtwFUj0xJ0nKwJKxLcJleHAiOjE20DE20YOT9FJkvIGauMp1Z6w.co0eqX4SCR6v8VbouuPzYVw84
作用:判断是否有登录状态等,控制访问权限
注意:前端只能判断token有无,而后端才能判断token的有效性
token的使用
目标:只有登录状态,才可以访问内容页面
步骤:
- 在 utils/auth.js 中判断无 token 令牌字符串,则强制跳转到登录页 (手动修改地址栏测试)
- 在登录成功后,保存 token 令牌字符串到本地,再跳转到首页 (手动修改地址栏测试)
观察一下没有进行登录,也就没用本地存储的token,会被踢回登录页面
// 1.1 判断无 token 令牌字符串,则强制跳转到登录页const token = localStorage.getItem(\'token\')if(!token) { localStorage.href = \'../login/index.html\'}
登录成功后,将当前服务器返回的token进行本地存储
// 1.2 收集手机号和验证码数据document.querySelector(\'.btn\').addEventListener(\'click\', async () => { const form = document.querySelector(\'.login-form\') const data = serialize(form, { hash: true, empty: true }) console.log(data) try { // 1.3 基于 axios 调用验证码登录接口 const response = await axios.post(\'/v1_0/authorizations\', data) const result = response.data console.log(\'#\', result) myAlert(true, \'登录成功\') // 登录成功后 保存token令牌到本地 并跳转到列表表表面 localStorage.setItem(\'token\', result.data.token) console.log(result.data) setTimeout(() => { // 延迟跳转 让alert警告框停留一会 location.href = \'../content/index.html\' }, 1500) } catch (error) { myAlert(false, error.response.data.message) }})
四、个人信息设置和axios请求拦截器
axios请求拦截器:发起请求之前,触发的配置函数,对请求参数进行额外配置
axios.interceptors.request.use(function (config) { const token = localStorage.getItem(\'token\'); token && (config.headers.Authorization = `Bearer ${token}`); // 在发送请求之前做些什么 return config;}, function (error) { // 对请求错误做些什么 return Promise.reject(error);});
- 身份验证:如上述例子,在每次请求发送到服务器前,检查并添加用户的身份验证 token,确保只有已登录且拥有合法权限的用户请求才能被服务器处理 。
- 请求参数统一处理:可以在拦截器中对所有请求的参数进行统一的格式化、添加公共参数(如时间戳、设备标识等) 等操作。
- 请求头统一设置:除了设置身份验证信息,还可以统一设置
Content-Type
、Accept
等请求头字段,确保所有请求的格式符合服务器要求。
在request.js文件中设置axios请求拦截器
// 目标2:设置个人信息// 2.1 在utils/request.js 设置请求拦截器 同意携带token// 2.2 请求个人信息并设置到页面// utils/request.jsaxios.interceptors.request.use(function (config) { // 统一携带 token const token = localStorage.getItem(\'token\') if (token) { config.headers.Authorization = `Bearer ${token}` } return config}, function (error) { return Promise.reject(error)})
在每次 HTTP 请求发送前自动添加身份验证令牌(Token),确保后端能够识别请求者的身份。这样,所有需要身份验证的 API 请求都会自动携带 Token,无需在每个请求中手动设置。
五、axios响应拦截器
axios响应拦截器:作用是在请求返回结果(进入 then
/catch
前)统一处理响应,让你不用在每个请求里重复写逻辑。
axios.interceptors.response.use( function (response) { // 1. 响应成功(状态码 2xx 范围)时触发 return response; }, function (error) { // 2. 响应失败(状态码非 2xx,或请求报错)时触发 return Promise.reject(error); });
Axios 响应拦截器会在所有请求完成后自动触发,无论请求成功(状态码 2xx)还是失败(状态码非 2xx 或网络错误)。它的本质是一个中间层,在响应数据到达你的业务代码(
.then()
或.catch()
)之前执行。
无论哪一个页面发生响应401请求错误,都会在这段代码里进行拦截,并且跳转到登录页面。
// 响应拦截器axios.interceptors.response.use( function (response) { // 1. 响应成功(状态码 2xx 范围)时触发 return response; }, function (error) { // 2. 响应失败(状态码非 2xx,或请求报错)时触发 console.dir(error) if (error?.response?.status === 401) { alert(\'身份验证失败,请重新登陆\') localStorage.clear() location.href = \'../login/index.html\' } return Promise.reject(error); });
当我修改了token值后,后端服务器发现token值对不上了,就被axios响应拦截了。
打个比方:
- 请求拦截器是 “快递员”,负责把包裹(请求)送到仓库(后端)。
- 后端是 “仓库安检员”,检查包裹里的 token(身份)是否合法。
- 响应拦截器是 “你”,收到仓库(后端)退回的 “身份不符” 包裹(401 响应),然后决定怎么处理(提示重新登录)。
总结:
- 响应回到then和catch之前,触发的拦截函数,对响应结果统一处理时axios响应拦截器
- axios响应拦截器状态为2xx触发成功回调,其他则触发失败回调函数
优化-axios响应结果
axios直接接收服务器返回的响应结果 就是return response.data,返回后,其他文件得到服务器返回的数据都可以少一层.data操作
// 响应拦截器axios.interceptors.response.use( function (response) { // 1. 响应成功(状态码 2xx 范围)时触发 const result = response.data return result }, function (error) { // 2. 响应失败(状态码非 2xx,或请求报错)时触发 console.dir(error) if (error?.response?.status === 401) { alert(\'身份验证失败,请重新登陆\') localStorage.clear() location.href = \'../login/index.html\' } return Promise.reject(error); });
这是因为 Axios 响应拦截器的 “全局预处理” 特性,能在响应返回业务代码前,统一 “剥掉”
response.data
这一层。
六、发布文章 - 富文本编辑器 (wangEditor插件)
富文本:带样式,多格式的文本,在前端一般使用标签配合内联样式实现
就是跟elementpuls组件库一样
editor.js文件
// 富文本编辑器// 创建编辑器函数,创建工具栏函数const { createEditor, createToolbar } = window.wangEditor// 编辑器配置对象const editorConfig = { // 占位提示文字 placeholder: \'发布文章内容...\', // 编辑器变化时回调函数 onChange(editor) { // 获取富文本内容 const html = editor.getHtml() // 也可以同步到
七、发布文章-频道列表
很简单,就是通过axios来获取服务器的数据,然后进行数据渲染
// 1.1 获取频道列表数据const setChannleList = async () => { const res = await axios(\'/v1_0/channels\') console.log(res.data) const htmlStr = \'请选择文章频道\' + res.data.channels.map(item => `${item.name}`).join(\'\') document.querySelector(\'.form-select\').innerHTML = htmlStr}setChannleList()
八、发布文章 - 封面设置
axios获取图片后进行切换,对 + 进行显示和隐藏,然后为了再次能够点击进行触发,那么就要进行代码的优化,对图片进行绑定点击事件 ,当进行点击的时候,图片文件再次自动触发click()事件
// 目标2:封面设置// 选择文件并保存在FormDatadocument.querySelector(\'.img-file\').addEventListener(\'change\', async e => { const file = e.target.files[0] const fd = new FormData() fd.append(\'image\', file) // 单独上传图片并得到图片 URL 网址 const response = await axios.post(\'/v1_0/upload\', fd) console.log(response) // 回显并切回 img 标签展示(隐藏 + 号上传标签) const imgUrl = response.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()})
九、发布文章 - 收集并保存
- 基于form-serialize插件收集表单数据对象
- 基于axios提交到服务器保存
- 调用Alert警告框反馈结果给用户
- 重置表单并跳转到列表页
利用serialize插件进行表单数据获取,然后通过axios.post进行数据提交 提交成功后进行表单数据清空和页面跳转
// 发布文章 - 收集并保存document.querySelector(\'.send\').addEventListener(\'click\', async () => { const form = document.querySelector(\'.art-form\') const data = serialize(form, { hash: true, empty: true }) // 发布文章的时候 不需要id属性 可以删除id 为后续做编辑使用 delete data.id // 自己收集封面图片地址并保存到data对象中 data.cover = { type: 1, // 封面类型 images: [document.querySelector(\'.rounded\').src] // 封面图片 URL } try { // axios 提交到服务器 const response = await axios.post(\'/v1_0/mp/articles\', data) // 调用提示用户 myAlert(true, \'发布成功\') // 重置表单 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, \'发布错误\') }})
十、内容管理 - 文章列表展示
这里第一步就是用到axios查询语法
axios.get(\'/接口地址\', { params: { 键1: 值1, 键2: 值2 }})
axios 会自动把
params
中的键值对拼接到 URL 上,形成查询字符串。
然后对html结构进行map编辑生成一个join()字符串,进行渲染
// 1.1 准备查询参数对象const queryObj = { status: \'\', // 文章状态 (1- 待审核 2-审核通过) 空字符串 - 全部 channel_id: \'\', // 文章频道 id 空字符串 - 全部 page: 1, // 当前页码 per_page: 2 // 当前页面条数}const setArtileList = async () => { const response = await axios(\'/v1_0/mp/articles\', { params: { queryObj } }) console.log(response) // 展示到指定标签的结构中 const htmlStr = response.data.results.map(item => `
${item.title} ${item.status === 1 ? `审核通过` : `待审核`} ${item.pubdate} ${item.read_count} ${item.comment_count} ${item.like_count} `).join(\'\') console.log(htmlStr) document.querySelector(\'.art-list\').innerHTML = htmlStr}setArtileList()
十一、内容管理 - 筛选功能
- 设置频道列表数据
- 监听筛选条件改变,保存查询信息到查询参数对象
- 点击筛选时, 传递查询参数对象到服务器
- 获取匹配数据,覆盖到页面展示
通过将筛选内容进行赋值,传入服务器得到筛选后的结果进行返回,再次进行渲染
// 1.1 准备查询参数对象const queryObj = { status: \'\', // 文章状态 (1- 待审核 2-审核通过) 空字符串 - 全部 channel_id: \'\', // 文章频道 id 空字符串 - 全部 page: 1, // 当前页码 per_page: 2 // 当前页面条数}
// 监听筛选条件改变, 保存查询信息到查询参数对象// 筛选状态标记数字 -> change事件 -> 绑定到查询参数对象上document.querySelectorAll(\'.form-check-input\').forEach(radio => { radio.addEventListener(\'change\', e => { queryObj.status = e.target.value })})// 筛选频道 id -> chenge事件 -> 绑定到查询参数对象上document.querySelector(\'.form-select\').addEventListener(\'change\', e => { queryObj.channel_id = e.target.value})// 点击筛选时 传递查询参数对象到服务器document.querySelector(\'.sel-btn\').addEventListener(\'click\', () => { setArtileList()})
十二、内容管理 - 分页功能
- 保存并设置文章总条数
- 点击下一页,做临界值判断,并切换页码参数请求最新数据
首先就是要设置分页总共有多少条,才能判断上一页和下一页的临界条件
// 目标3 分页功能// 3.2 点击下一页 做临界判断document.querySelector(\'.next\').addEventListener(\'click\', e => { // 当前页码小于最大页码数 if (queryObj.page { if (queryObj.page > 1) { queryObj.page-- document.querySelector(\'.page-now\').innerHTML = `第${queryObj.page}页` setArtileList() }})
十三、内容管理 - 删除功能
- 关联文章id到删除图标
- 点击删除获取文章id
- 调用删除接口,传递文章id到服务器
- 重写获取文章列表,再次渲染
// 目标四:删除文章document.querySelector(\'.art-list\').addEventListener(\'click\', async e => { // 判断删除的元素 if (e.target.classList.contains(\'del\')) { const delId = e.target.parentNode.dataset.id // 4.3 调用删除接口 传递文章 id 到服务器 console.log(delId) const response = await axios.delete(`/v1_0/mp/articles/${delId}`) // 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}页` } setArtileList() }})
十四、内容管理-编辑文章-回显
- 页面跳转传参(URL查询参数方式)
- 发布文章页面接收参数判断(共用同一套表单)
- 修改标题和文章按钮
- 获取文章详情数据回显表单
// 进行文章的编辑 // 自调用函数 ; (function () { const paramsStr = location.search const parmas = new URLSearchParams(paramsStr) parmas.forEach(async (value, key) => { // 当前有要编辑的文章id 被传入过来 console.log(\'#\', value, key) if (key === \'id\') { document.querySelector(\'.title span\').innerHTML = \'修改文章\' document.querySelector(\'.send\').innerHTML = \'修改\' const response = await axios(`/v1_0/mp/articles/${value}`) console.log(response) // 组织我需要的数据对象 const dataObj = { channel_id: response.data.channel_id, title: response.data.title, rounded: response.data.cover.images[0], content: response.data.content, id: response.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 document.querySelector(`[name=${key}]`).value = dataObj[key] } }) } }) })()
十五、编辑文字-保存
当按钮内容为“修改”时,收集表单中的文章数据(包括标题、内容、频道等),并使用
serialize
函数将其转为对象格式。随后,通过 DOM 获取封面图片地址,并组装成符合接口要求的cover
数据结构(包含封面类型和图片数组)。接着,调用axios.put
向后端发送修改文章的请求,请求地址中携带文章的id
,请求体中包含完整的文章数据和封面信息。整个操作被包装在try...catch
中,用于捕获可能出现的错误,并根据请求结果通过myAlert
提示用户是否修改成功。
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 }) try { const response = await axios.put(`/v1_0/mp/articles/${data.id}`, { ...data, cover: { type: document.querySelector(\'.rounded\').src ? 1 : 0, images: [document.querySelector(\'.rounded\').src] } }) console.log(response) myAlert(true, \'修改文章成功\') } catch (error) { myAlert(false, \'修改错误\') }})