目录
[一、 项目准备](#一、 项目准备)
[二、 验证码登录](#二、 验证码登录)
[三、 token的介绍](#三、 token的介绍)
[六、发布文章 - 富文本编辑器 (wangEditor插件)](#六、发布文章 - 富文本编辑器 (wangEditor插件))
[八、发布文章 - 封面设置](#八、发布文章 - 封面设置)
[九、发布文章 - 收集并保存](#九、发布文章 - 收集并保存)
[十、内容管理 - 文章列表展示](#十、内容管理 - 文章列表展示)
[十一、内容管理 - 筛选功能](#十一、内容管理 - 筛选功能)
[十二、内容管理 - 分页功能](#十二、内容管理 - 分页功能)
[十三、内容管理 - 删除功能](#十三、内容管理 - 删除功能)
[总结不易~ 本章节对我有很大收获,希望对你也是!!!](#总结不易~ 本章节对我有很大收获,希望对你也是!!!)


一、 项目准备
技术:
- 基于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函数来进行登录效果弹窗
javascript
// 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检查验证码和手机号是否正确的弹窗
javascript
// 弹窗插件
// 需要先准备 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.eyJpc3MiOiJodHRwczovL3dL5dy5pdmNhc3RQVyJ2F1iOiJ3iCVjIof0wIY2THNnGEYJN5CO
UZDYLT110WOdGQYZ2ZIzUYJtxW11amR10j0wWZMMNJet2UY5DGYz3L7G4MKcAT2M2K62KtgNDcz21YtwFUj0xJ0nK
wJKxLcJleHAiOjE20DE20YOT9FJkvIGauMp1Z6w.co0eqX4SCR6v8VbouuPzYVw84
作用:判断是否有登录状态等,控制访问权限

注意:前端只能判断token有无,而后端才能判断token的有效性
token的使用
目标:只有登录状态,才可以访问内容页面
步骤:
- 在 utils/auth.js 中判断无 token 令牌字符串,则强制跳转到登录页 (手动修改地址栏测试)
- 在登录成功后,保存 token 令牌字符串到本地,再跳转到首页 (手动修改地址栏测试)
观察一下没有进行登录,也就没用本地存储的token,会被踢回登录页面
javascript
// 1.1 判断无 token 令牌字符串,则强制跳转到登录页
const token = localStorage.getItem('token')
if(!token) {
localStorage.href = '../login/index.html'
}
登录成功后,将当前服务器返回的token进行本地存储
javascript
// 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请求拦截器:发起请求之前,触发的配置函数,对请求参数进行额外配置

javascript
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请求拦截器
javascript
// 目标2:设置个人信息
// 2.1 在utils/request.js 设置请求拦截器 同意携带token
// 2.2 请求个人信息并设置到页面
// utils/request.js
axios.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
前)统一处理响应,让你不用在每个请求里重复写逻辑。
javascript
axios.interceptors.response.use(
function (response) {
// 1. 响应成功(状态码 2xx 范围)时触发
return response;
},
function (error) {
// 2. 响应失败(状态码非 2xx,或请求报错)时触发
return Promise.reject(error);
}
);
Axios 响应拦截器会在所有请求完成后 自动触发,无论请求成功(状态码 2xx)还是失败(状态码非 2xx 或网络错误)。它的本质是一个中间层 ,在响应数据到达你的业务代码(
.then()
或.catch()
)之前执行。
无论哪一个页面发生响应401请求错误,都会在这段代码里进行拦截,并且跳转到登录页面。
javascript
// 响应拦截器
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操作
javascript
// 响应拦截器
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组件库一样
javascript
<div id="editor---wrapper">
<div id="toolbar-container"><!-- 工具栏 --></div>
<div id="editor-container"><!-- 编辑器 --></div>
</div>
editor.js文件
javascript
// 富文本编辑器
// 创建编辑器函数,创建工具栏函数
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'
})
七、发布文章-频道列表

很简单,就是通过axios来获取服务器的数据,然后进行数据渲染
javascript
// 1.1 获取频道列表数据
const setChannleList = async () => {
const res = await axios('/v1_0/channels')
console.log(res.data)
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()
八、发布文章 - 封面设置

axios获取图片后进行切换,对 + 进行显示和隐藏,然后为了再次能够点击进行触发,那么就要进行代码的优化,对图片进行绑定点击事件 ,当进行点击的时候,图片文件再次自动触发click()事件
javascript
// 目标2:封面设置
// 选择文件并保存在FormData
document.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进行数据提交 提交成功后进行表单数据清空和页面跳转
javascript
// 发布文章 - 收集并保存
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查询语法
javascript
axios.get('/接口地址', {
params: {
键1: 值1,
键2: 值2
}
})
axios 会自动把
params
中的键值对拼接到 URL 上,形成查询字符串。
然后对html结构进行map编辑生成一个join()字符串,进行渲染
javascript
// 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 => `
<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-success">审核通过</span>` : `<span class="badge text-bg-primary">待审核</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>
<i class="bi bi-pencil-square edit"></i>
<i class="bi bi-trash3 del"></i>
</td>
</tr>
`).join('')
console.log(htmlStr)
document.querySelector('.art-list').innerHTML = htmlStr
}
setArtileList()
十一、内容管理 - 筛选功能
- 设置频道列表数据
- 监听筛选条件改变,保存查询信息到查询参数对象
- 点击筛选时, 传递查询参数对象到服务器
- 获取匹配数据,覆盖到页面展示

通过将筛选内容进行赋值,传入服务器得到筛选后的结果进行返回,再次进行渲染
javascript
// 1.1 准备查询参数对象
const queryObj = {
status: '', // 文章状态 (1- 待审核 2-审核通过) 空字符串 - 全部
channel_id: '', // 文章频道 id 空字符串 - 全部
page: 1, // 当前页码
per_page: 2 // 当前页面条数
}
javascript
// 监听筛选条件改变, 保存查询信息到查询参数对象
// 筛选状态标记数字 -> 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()
})
十二、内容管理 - 分页功能
- 保存并设置文章总条数
- 点击下一页,做临界值判断,并切换页码参数请求最新数据
首先就是要设置分页总共有多少条,才能判断上一页和下一页的临界条件

javascript
// 目标3 分页功能
// 3.2 点击下一页 做临界判断
document.querySelector('.next').addEventListener('click', e => {
// 当前页码小于最大页码数
if (queryObj.page < Math.ceil(totalCount / queryObj.per_page)) {
queryObj.page++
setArtileList()
}
})
// 3.3 点击上一页
document.querySelector('.last').addEventListener('click', e => {
if (queryObj.page > 1) {
queryObj.page--
document.querySelector('.page-now').innerHTML = `第${queryObj.page}页`
setArtileList()
}
})
十三、内容管理 - 删除功能
- 关联文章id到删除图标
- 点击删除获取文章id
- 调用删除接口,传递文章id到服务器
- 重写获取文章列表,再次渲染

javascript
// 目标四:删除文章
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查询参数方式)
- 发布文章页面接收参数判断(共用同一套表单)
- 修改标题和文章按钮
- 获取文章详情数据回显表单
javascript
// 进行文章的编辑
// 自调用函数
; (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
提示用户是否修改成功。
javascript
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, '修改错误')
}
})