主题
axios 响应拦截器统一处理可能出现的错误与axios 二(本地存储 1/3)
本页对应 课堂视频,共 6 段视频,约 1 小时 11 分。
学习目标
- 能够说明
request实例、API 模块、控制器、模板/组件之间的调用关系,并准确描述当前项目目录结构中的数据流。 - 能够基于 axios 实例按后端接口封装管理员相关 API 函数,使控制器调用添加、获取、删除等操作时无需重复编写 URL。
- 能够获取管理员列表数据并传入管理员表格组件,正确渲染用户名、注册时间、最后登录时间等字段。
- 能够实现添加管理员后关闭模态框、重置表单并刷新列表,并验证新增数据即时显示。
- 能够使用事件委托绑定删除按钮,读取
data-id调用删除接口,并在删除成功后刷新管理员列表。 - 能够安装并导入 SweetAlert2,将原生
confirm替换为异步确认框,并在删除成功后弹出成功提示。
知识点
响应拦截器要解决什么问题
为什么需要:请求后端时,错误不只一种。转写稿里明确提到:一类是网络错误、404、5xx 等,axios 会进入失败回调;另一类是 HTTP 状态码 200,但响应体里的业务字段 ok 不是 1,这同样不能当成成功。如果每个控制器都自己判断一遍,代码会重复,提示也不统一。
怎么想:把“请求失败”和“业务失败”都收口到 axios 实例的响应拦截器里。控制器只关心成功后的业务处理,失败提示由拦截器统一完成。
怎么做:在 src/request/advserver.js 创建实例并设置响应拦截器。注意转写稿里的“excels”应纠正为 axios,“permiss”应纠正为 Promise,“显示体”应纠正为响应体。
易错点:业务失败判断要用 res.data.ok !== 1;失败分支和 error 分支都要 return,不能漏。
js
// 导入 axios
import axios from 'axios';
import toastr from 'toastr';
// 创建 axios 实例
const advServer = axios.create({
baseURL: '/api',
timeout: 5000
});
// 给 实例设置响应拦截器
advServer.interceptors.response.use(res => {
if (res.data.ok !== 1) {
toastr.error(res.data.msg);
// 返回不改变状态的 Promise,执行链结束了
return new Promise(()=>{});
}
return res.data;
}, error => {
toastr.error('请求错误!');
// 返回不改变状态的 Promise,执行链结束了
return new Promise(()=>{});
});
// 暴露实例
export default advServer;成功时为什么要 return res.data
为什么需要:axios 默认把后端返回的数据包在 res.data 里。控制器真正需要的是响应体,而不是整个 axios 响应对象。
怎么想:拦截器成功时直接解包,把 res.data 返回出去,这样控制器里的 then(res => {}) 拿到的就是业务响应体。
怎么做:在成功分支写 return res.data;。转写稿说“我只要显体的”,这里应理解为“我只要响应体”。
易错点:如果忘记 return res.data,控制器拿到的仍是 axios 包装对象,后面取 res.ok、res.msg 就会错位。
new Promise(()=>{}) 的作用
为什么需要:业务失败或请求失败后,不希望控制器里的 .then() 继续执行。转写稿说“返回一个不改变状态的 Promise,执行链结束了、链中断”,就是让后续成功逻辑不再走。
怎么想:这个 Promise 既不 resolve 也不 reject,所以不会进入后续 then,也不会产生未捕获的 reject。
怎么做:失败分支统一 return new Promise(()=>{});。
易错点:不要随手改成 return false,否则后续 then 仍可能收到 false 继续执行;也不要随意 Promise.reject() 而不处理,容易在控制台产生未捕获错误。
控制器里如何用封装好的实例
为什么需要:控制器负责调度业务。添加管理员时要做表单校验、发请求、成功提示,但不应该重复写 axios 基础配置和错误提示。
怎么想:控制器导入 advServer,只写业务校验和成功后的动作;失败由拦截器统一 toastr 提示。
怎么做:保留笔记中的 src/controllers/admin.js 代码。注意 toastr 的 CSS 也要导入。
易错点:document.adminForm 依赖表单的 name;取值后要 trim();账号、密码、确认密码校验顺序要清楚;advServer.post('/admin', {...}) 的地址要与后端接口对应。
js
// 导入 toastr
import toastr from 'toastr';
import '../../node_modules/toastr/build/toastr.css';
// 导入请求实例
import advServer from '../request/advserver';
// 导入模板页
import adminV from '@/views/admin';
// 执行添加管理员账号
const addAdminExec = () => {
// 获取表单数据
const adminName = document.adminForm.adminName.value.trim();
const passWord = document.adminForm.passWord.value.trim();
const rePassWord = document.adminForm.rePassWord.value.trim();
// 管理员账号只能由英文字母组成 ^[a-zA-Z]+$
if (adminName.search(/^[a-zA-Z]+$/) === -1) {
toastr.error('管理员账号只能由英文字母组成!');
return;
}
// 密码是6-18位的数字、字母、下划线 ^\w{6,18}$
if (passWord.search(/^\w{6,18}$/) === -1) {
toastr.error('密码必须由6到18位数字、字母、下划线组成');
return;
}
// 确认密码
if (passWord !== rePassWord) {
toastr.error('两次密码不一致');
return;
}
// 使用 axios 发送请求
advServer
.post('/admin', {
adminName,
passWord
})
.then( res => {
toastr.success('管理员添加成功');
console.log('成功:',res);
})
}
export default (req, res) => {
// 加载模板内容
res.render(adminV());
// 给添加按钮监听click事件
document.querySelector('#addAdminBtn').addEventListener('click', addAdminExec)
}调试过程与验证
为什么需要:只看代码不容易确认拦截器是否真的统一处理了错误。
怎么想:分别制造“前端校验失败”“后端业务失败”“请求成功”三种情况。
怎么做:转写稿中先故意输错密码,页面用 toastr 提示密码规则;再用已存在账号提交,后端返回 ok 不为 1,拦截器用 res.data.msg 提示;最后换新账号提交,进入成功分支提示“管理员添加成功”。这说明错误提示和成功逻辑已经分层。
易错点:业务失败时不要以为会进 error 回调;HTTP 200 但 ok !== 1 仍走成功回调,所以必须在成功回调里判断业务码。
为什么要做 axios 二次封装
笔记里先有了 request 目录中的 axios 实例,它已经配置好基础地址、响应拦截器等。但控制器里如果直接写 advServer.post('/admin', body),仍然要反复写 URL 和请求方法。转写稿里说得很清楚:二次封装的目标是让控制器“连 URL 都不用写”,只调用语义化函数,例如 postAdmin、getAdmin、deleteAdmin。这样控制器负责调度,API 文件负责对接后端接口。
怎么想:一个后端接口对应一个函数;也可以把多个后端接口组合成一个函数。怎么做:新建 src/api/admin.js,导入实例并导出函数。易错点:API 文件里的函数名、请求方法、URL 必须和后端接口一致;不要忘记 return,否则拿不到 Promise。
编写 src/api/admin.js
下面保留本节笔记代码:
js
// 导入实例
import advServer from '../request/advserver';
// 添加管理员
export const postAdmin = body => {
return advServer.post('/admin', body);
}
// 获取管理员
export const getAdmin = () => {
return advServer.get('/admin');
}
// 删除指定id的管理员
export const deleteAdmin = id => {
return advServer.delete('/admin/'+id);
}
// 修改密码
export const changePassword = body => {
return advServer.patch('/changpwd', body)
}为什么需要:控制器不再直接依赖 axios 实例。怎么想:postAdmin 需要请求体,getAdmin 不需要参数,deleteAdmin 需要 id 拼到 URL 后。怎么做:按上表导出。易错点:deleteAdmin 的 URL 是 /admin/ 加 id,不要漏斜杠;changePassword 用 patch,因为只局部更新密码。
控制器中导入 api 函数并完成添加管理员
控制器是业务调度中心。转写稿强调:先从控制器入手,需要什么就去 API 文件取什么。添加管理员时,控制器导入 postAdmin,不再导入 axios 实例。
js
// 导入 toastr
import toastr from 'toastr';
import '../../node_modules/toastr/build/toastr.css';
// 导入发送请求的函数
import {postAdmin} from '../api/admin';
// 导入模板页
import adminV from '@/views/admin';
// 执行添加管理员账号
const addAdminExec = () => {
// 获取表单数据
const adminName = document.adminForm.adminName.value.trim();
const passWord = document.adminForm.passWord.value.trim();
const rePassWord = document.adminForm.rePassWord.value.trim();
// 管理员账号只能由英文字母组成 ^[a-zA-Z]+$
if (adminName.search(/^[a-zA-Z]+$/) === -1) {
toastr.error('管理员账号只能由英文字母组成!');
return;
}
// 密码是6-18位的数字、字母、下划线 ^\w{6,18}$
if (passWord.search(/^\w{6,18}$/) === -1) {
toastr.error('密码必须由6到18位数字、字母、下划线组成');
return;
}
// 确认密码
if (passWord !== rePassWord) {
toastr.error('两次密码不一致');
return;
}
// 调用后端API 添加管理员
postAdmin({
adminName,
passWord
})
.then( res => {
toastr.success('管理员添加成功');
console.log('成功:',res);
})
}
export default (req, res) => {
// 加载模板内容
res.render(adminV());
// 给添加按钮监听click事件
document.querySelector('#addAdminBtn').addEventListener('click', addAdminExec)
}为什么需要:表单数据不能直接发,先做前端校验。怎么想:账号、密码、确认密码分别校验,错误用 toastr.error 提示。怎么做:校验通过后调用 postAdmin。易错点:trim() 后校验;return 要及时;成功回调里还应关闭模态框、清空表单、更新列表,否则页面看起来“没反应”。
获取管理员并渲染列表
转写稿中,获取列表仍从控制器开始:导入 getAdmin,封装 getAdminExec,调用后拿到 res.data。然后把表格抽成组件 adminTable,控制器导入组件,将 res.data 传给组件,组件返回替换数据后的字符串,最后设置到列表容器。
怎么做:调用 getAdmin().then(res => { const html = adminTable({ adminList: res.data }); document.querySelector('#adminList').innerHTML = html; })。易错点:组件接收的数据名要和模板里遍历名一致;动态列表更新后,删除按钮也是新的,所以删除事件要用事件委托。
删除管理员与事件委托
删除时先获取按钮上的 data-id,调用 deleteAdmin(id)。因为列表会不停更新,不能只给初始按钮绑定事件,而要把 click 事件委托到列表容器上,再判断 e.target.classList.contains(...) 是否点中删除按钮。删除前用确认框,用户点取消就不删;点确定才发请求。成功后调用 getAdminExec() 更新列表。易错点:data-id 要用 dataset.id 读取;确认框是异步的,不能在判断前就发请求;删除成功提示可来自前端,也可用后端 res.msg。
用 SweetAlert2 优化确认框
转写稿中把原生 confirm 换成 sweetalert2。安装为生产依赖,控制器中 import Swal from 'sweetalert2',不需要再手动导入 CSS。调用 Swal.fire({...}) 返回 Promise,then 中判断用户是否确认。可配置标题、确认按钮文字、取消按钮文字 cancelButtonText: '取消',成功后再弹提示。易错点:变量名大小写要一致,转写里曾把 Swal 写成小写导致 Swal is not defined;确认框是异步的,删除逻辑要放在确认结果之后;删除成功后再更新列表。
本节目标:从控制器出发取管理员数据
为什么需要:管理员列表不能继续写死在页面里,必须从后端拿到真实数据再渲染。
怎么想:所有业务从控制器入手,控制器负责调度:调用 API 取数,再把数据交给组件和模板。
怎么做:在 src/controllers/admin.js 中导入 getAdmin、管理员表格组件和模板,封装 getAdminExec,在默认导出函数里先渲染模板,再执行取数。
易错点:不要一上来改模板;先确认 API 层已封装好 getAdmin,否则控制器无法“连 URL 都不用写”。
控制器先导入 API 与组件
为什么需要:控制器如果直接导入 axios 实例,会把请求细节散落在业务代码里;API 层应统一封装后端接口。
怎么想:一个后端接口对应一个 API 函数,控制器只导入函数。组件则负责生成表格 HTML 字符串。
怎么做:保留本节代码中的导入:postAdmin, getAdmin、AdminTabelComponent、adminV。
易错点:导入路径和名字要一致;笔记中 AdminTabelComponent、#amdinListBox 的拼写要按项目现状保留,改错会导致取不到组件或元素。
js
// 导入 toastr
import toastr from 'toastr';
import '../../node_modules/toastr/build/toastr.css';
// 导入发送请求的函数
import {postAdmin, getAdmin} from '../api/admin';
// 导入管理员列表表格的组件
import AdminTabelComponent from '../components/AdminTable';
// 导入模板页
import adminV from '@/views/admin';
// 执行添加管理员账号
const addAdminExec = () => {
// 获取表单数据
const adminName = document.adminForm.adminName.value.trim();
const passWord = document.adminForm.passWord.value.trim();
const rePassWord = document.adminForm.rePassWord.value.trim();
// 管理员账号只能由英文字母组成 ^[a-zA-Z]+$
if (adminName.search(/^[a-zA-Z]+$/) === -1) {
toastr.error('管理员账号只能由英文字母组成!');
return;
}
// 密码是6-18位的数字、字母、下划线 ^\w{6,18}$
if (passWord.search(/^\w{6,18}$/) === -1) {
toastr.error('密码必须由6到18位数字、字母、下划线组成');
return;
}
// 确认密码
if (passWord !== rePassWord) {
toastr.error('两次密码不一致');
return;
}
// 调用后端API 添加管理员
postAdmin({
adminName,
passWord
})
.then( res => {
toastr.success('管理员添加成功');
console.log('成功:',res);
})
}
// 获取管理员信息 并渲染页面
const getAdminExec = () => {
// 向后端API请求
getAdmin()
.then(res => {
// 将数据发送到管理员表格组件中 将组件设置到模板的指定位置
document.querySelector('#amdinListBox').innerHTML = AdminTabelComponent({adminList: res.data})
});
}
export default (req, res) => {
// 加载模板内容
res.render(adminV());
// 获取管理员信息
getAdminExec();
// 给添加按钮监听click事件
document.querySelector('#addAdminBtn').addEventListener('click', addAdminExec)
}封装 getAdminExec:请求并交给组件
为什么需要:取数和渲染是一组固定流程,封装后添加、删除成功后都能复用。
怎么想:getAdmin() 返回 Promise,成功回调里拿到响应结果;按本节代码,管理员数组在 res.data。
怎么做:调用组件函数 AdminTabelComponent({adminList: res.data}),它返回替换数据后的 HTML 字符串,再写入 #amdinListBox.innerHTML。
易错点:调试时先在 then 里 console.log(res),确认 res.data 是数组;若响应拦截器返回值不同,要按实际结构调整,不能盲目套 res.data。
组件 AdminTable.ejs 负责循环渲染
为什么需要:表格结构较长,抽成组件后模板更干净,也方便控制器把数据“喂”进去。
怎么想:组件接收 data.adminList,用 forEach 生成多行;字段名以接口返回为准。
怎么做:保留下面组件代码,用户名、最后登录时间、注册时间分别取 adminName、lastLoginTime、regTime,删除按钮用 data-id 保存 _id,为后续删除做准备。
易错点:EJS 中 data.adminList 不能漏;日期字段名不要猜,转写稿中明确要对照 API 文档;data-id 是自定义属性,页面上不可见但 JS 可读。
ejs
<table class="table table-bordered">
<thead>
<tr>
<!-- <th style="width: 10px">ID</th> -->
<th>用户名</th>
<th>最后登录时间</th>
<th>注册时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<% data.adminList.forEach(adminItem => { %>
<tr>
<!-- <td>648d3b13dbeadd55199ab0a7</td> -->
<td><%= adminItem.adminName %></td>
<td><%= adminItem.lastLoginTime %></td>
<td><%= adminItem.regTime %></td>
<td>
<button type="button" class="btn btn-danger" data-id="<%= adminItem._id %>">删除</button>
<button type="button" class="btn btn-success">修改密码</button>
</td>
</tr>
<% }) %>
</tbody>
</table>挂载与调试:先模板,再取数,再绑事件
为什么需要:模板加载后才有 #amdinListBox 和 #addAdminBtn,顺序错了会取不到元素。
怎么想:默认导出中先 res.render(adminV()),再 getAdminExec(),最后给添加按钮绑定 addAdminExec。
怎么做:启动 MongoDB 和后端,打开页面看控制台输出;确认 res.data 有数据后,检查 #amdinListBox 是否被写入组件字符串。转写稿中的调试过程就是先打印响应、再确认表格出现。
易错点:querySelector 选择器拼写要与模板一致;组件导入路径错会报函数不存在;如果页面空白,先看控制台和网络请求,不要直接怀疑 EJS 循环。
本节目标很明确:在管理员列表页完成“添加管理员”闭环——校验表单、调用后端、提示结果,并在成功后关闭模态框、清空表单、刷新列表。控制器负责调度,API 模块负责发请求,表格组件负责渲染。
控制器职责:导入与调度
为什么需要:控制器是业务入口,不该在控制器里直接创建 axios 实例、拼 URL、处理响应拦截器。转写稿里已经把请求封装到 ../api/admin,一个后端接口对应一个函数,控制器只导入函数。
怎么想:控制器需要四类东西:提示库 toastr、请求函数 postAdmin/getAdmin、表格组件 AdminTabelComponent、模板 adminV。
怎么做:按笔记保留导入语句,尤其是 toastr 的 CSS 也要导入;请求函数用解构导入。
易错点:不要又写回 import axios from ...;toastr CSS 路径要正确;组件名、模板路径要和项目一致。笔记中 AdminTabelComponent、#amdinListBox 的拼写要按项目原样统一,不能一半改一半不改。
添加管理员:先校验再提交
为什么需要:账号、密码、确认密码不合规时,发请求只会增加无效交互;前端先拦一道,用户能立即看到原因。
怎么想:先从 document.adminForm 取三个值并 trim();账号用 /^[a-zA-Z]+$/,密码用 /^\w{6,18}$/,再比较两次密码。失败就 toastr.error(...) 并 return,成功才调用 postAdmin({adminName, passWord})。
怎么做:见完整代码中的 addAdminExec。调试时转写稿先故意输入错误账号、错误密码,页面会弹出对应提示;换成新账号后请求成功。
易错点:search() 不匹配返回 -1,不要写成 0;trim() 后再比较;document.adminForm 依赖表单 name 和字段 name;密码正则不要漏掉下划线。
添加成功后的界面收尾:关闭、清空、刷新
为什么需要:只弹“添加成功”不够。转写稿调试时发现:添加成功后模态框没关、列表没更新、表单里还留着旧值,再点开还能看到上次输入。
怎么想:成功回调里做三件事:关闭模态框、重置表单、重新拉列表。
怎么做:$('#addAdminModal').modal('hide');、document.adminForm.reset();、getAdminExec();。其中模态框依赖全局 jQuery 和 Bootstrap 插件,reset() 依赖表单元素。
易错点:模态框 id 要和模板一致;reset() 不是清空某个 input,而是重置整个表单;刷新列表要调用已经封装好的 getAdminExec,不要重复写请求逻辑。顺序上先提示、再关闭/清空/刷新即可。
获取管理员并渲染表格组件
为什么需要:列表数据来自后端,表格结构抽成组件后,控制器只负责“取数据并放到指定容器”。
怎么想:getAdmin() 返回的 res.data 是管理员数组;把 {adminList: res.data} 传给组件函数,得到替换好数据的 HTML 字符串,再写入 #amdinListBox。
怎么做:见 getAdminExec。调试时可先 console.log(res) 确认 res.data 是数组,再检查组件内部是否按 data.adminList 遍历。
易错点:不要把整个 res 传给组件;容器 id 要和模板一致;innerHTML 会整体替换容器内容。
初始化入口与事件绑定
为什么需要:进入页面要先渲染模板壳,再异步取列表,最后给“添加”按钮绑定事件。
怎么想:默认导出函数中先 res.render(adminV()),再 getAdminExec(),最后 querySelector('#addAdminBtn').addEventListener('click', addAdminExec)。
怎么做:见完整代码。事件绑定必须在模板渲染后执行,否则可能找不到按钮。
易错点:按钮 id 要和模板一致;不要重复绑定;getAdminExec 要在模板加载后调用。
完整控制器代码
js
// 导入 toastr
import toastr from 'toastr';
import '../../node_modules/toastr/build/toastr.css';
// 导入发送请求的函数
import {postAdmin, getAdmin} from '../api/admin';
// 导入管理员列表表格的组件
import AdminTabelComponent from '../components/AdminTable';
// 导入模板页
import adminV from '@/views/admin';
// 执行添加管理员账号
const addAdminExec = () => {
// 获取表单数据
const adminName = document.adminForm.adminName.value.trim();
const passWord = document.adminForm.passWord.value.trim();
const rePassWord = document.adminForm.rePassWord.value.trim();
// 管理员账号只能由英文字母组成 ^[a-zA-Z]+$
if (adminName.search(/^[a-zA-Z]+$/) === -1) {
toastr.error('管理员账号只能由英文字母组成!');
return;
}
// 密码是6-18位的数字、字母、下划线 ^\w{6,18}$
if (passWord.search(/^\w{6,18}$/) === -1) {
toastr.error('密码必须由6到18位数字、字母、下划线组成');
return;
}
// 确认密码
if (passWord !== rePassWord) {
toastr.error('两次密码不一致');
return;
}
// 调用后端API 添加管理员
postAdmin({
adminName,
passWord
})
.then( res => {
toastr.success('管理员添加成功');
// 关闭模态框
$('#addAdminModal').modal('hide');
// 清空表单
document.adminForm.reset();
// 更新列表
getAdminExec();
});
}
// 获取管理员信息 并渲染页面
const getAdminExec = () => {
// 向后端API请求
getAdmin()
.then(res => {
// 将数据发送到管理员表格组件中 将组件设置到模板的指定位置
document.querySelector('#amdinListBox').innerHTML = AdminTabelComponent({adminList: res.data})
});
}
export default (req, res) => {
// 加载模板内容
res.render(adminV());
// 获取管理员信息
getAdminExec();
// 给添加按钮监听click事件
document.querySelector('#addAdminBtn').addEventListener('click', addAdminExec)
}调试与易错点清单
转写稿的调试顺序很值得照做:先输错账号看 toastr 提示;再输错密码看密码提示;再让两次密码不一致;最后用新账号添加。成功后如果发现弹框没关、列表没变、表单还有旧值,就回到 then 里检查三件事是否都写了。还要注意:toastr 和 CSS 是否导入、postAdmin/getAdmin 是否从 API 模块导入、#addAdminModal 与 #addAdminBtn 是否和模板一致、document.adminForm 的字段名是否一致、res.data 是否确实是数组。按这个闭环走,添加管理员和列表刷新就能稳定工作。
删除管理员这一节要解决什么
为什么需要:管理员列表是动态渲染的,删除按钮会随列表反复更新;如果逐个绑定事件,按钮一刷新就失效。删除还必须先确认,删除成功后列表要立刻更新。
怎么想:把点击事件委托到稳定的列表容器 #amdinListBox 上,在事件里判断目标是不是删除按钮,再取按钮上的 data-id 调删除接口。
怎么做:在控制器里定义 deleteAdminExec,用 event.target.classList.contains('btn-danger') 判断按钮,用 event.target.dataset.id 取 ID,confirm 通过后调用 deleteAdmin,成功后再执行 getAdminExec() 刷新列表。
易错点:容器 ID 要和模板一致,笔记里是 #amdinListBox(注意拼写);删除按钮必须有 data-id;dataset.id 对应 HTML 的 data-id;删除成功后不要只弹提示而忘记刷新列表。
删除请求要先封装到 API 层
为什么需要:控制器只负责调度,不应到处写 URL。转写稿里把 axios 二次封装成 API 文件,一个后端接口对应一个函数,删除管理员就对应 deleteAdmin。
怎么想:删除接口需要 ID,地址通常是 /admin/${id},用请求实例发 delete,返回的仍是 Promise。
怎么做:在 src/api/admin.js 中导入请求实例,暴露 deleteAdmin(id);控制器只导入函数使用,不直接导入 axios 实例。
易错点:地址前要有斜杠;ID 要用模板字符串拼进去;函数要 return 请求结果,否则控制器拿不到 Promise;失败会走响应拦截器,不会进入成功 then。
在控制器里执行删除并刷新列表
为什么需要:删除是业务动作,控制器要处理“确认—请求—提示—刷新”的完整流程。
怎么想:删除函数只处理删除按钮的点击;绑定处用事件委托,避免列表更新后事件丢失。
怎么做:本节笔记代码如下,保留原代码:
js
// 执行管理员的删除
const deleteAdminExec = event => {
// 判断目标元素是否是删除按钮
if (event.target.classList.contains('btn-danger')) {
// 弹出确认框
if (confirm('确定删除?')) {
// 调用后端API执行删除
deleteAdmin(event.target.dataset.id)
.then(res => {
alert('删除成功!');
// 更新列表
getAdminExec();
})
}
}
};
export default (req, res) => {
// 加载模板内容
res.render(adminV());
// 获取管理员信息
getAdminExec();
// 给添加按钮监听click事件
document.querySelector('#addAdminBtn').addEventListener('click', addAdminExec)
// 给管理员列表中的删除按钮监听单击事件 事件位置
document.querySelector('#amdinListBox').addEventListener('click', deleteAdminExec)
}易错点:confirm 返回布尔值,点取消不应发请求;alert 只是本节笔记写法,后续可优化;deleteAdminExec 里不要重新获取容器再绑事件,否则会重复绑定。
调试与确认框优化
转写稿里先用 confirm 和 alert 跑通流程,再安装 sweetalert2 优化。导入时使用 SWAL,它不用像 toastr 那样额外导入 CSS;Swal.fire({...}).then(...) 是异步的,点确定才继续删除。调试时出现 SWAL is not defined,原因是代码里写成了小写 swal,改成大写即可。取消按钮中文可配置 cancelButtonText: '取消',成功提示可用 icon: 'success'。易错点:优化确认框后仍要保证删除成功后调用 getAdminExec() 更新列表。
本节控制器完整代码(保留笔记代码)
js
// 导入 toastr
import toastr from 'toastr';
import '../../node_modules/toastr/build/toastr.css';
// 导入发送请求的函数
import {postAdmin, getAdmin, deleteAdmin} from '../api/admin';
// 导入管理员列表表格的组件
import AdminTabelComponent from '../components/AdminTable';
// 导入模板页
import adminV from '@/views/admin';
// 执行添加管理员账号
const addAdminExec = () => {
// 获取表单数据
const adminName = document.adminForm.adminName.value.trim();
const passWord = document.adminForm.passWord.value.trim();
const rePassWord = document.adminForm.rePassWord.value.trim();
// 管理员账号只能由英文字母组成 ^[a-zA-Z]+$
if (adminName.search(/^[a-zA-Z]+$/) === -1) {
toastr.error('管理员账号只能由英文字母组成!');
return;
}
// 密码是6-18位的数字、字母、下划线 ^\w{6,18}$
if (passWord.search(/^\w{6,18}$/) === -1) {
toastr.error('密码必须由6到18位数字、字母、下划线组成');
return;
}
// 确认密码
if (passWord !== rePassWord) {
toastr.error('两次密码不一致');
return;
}
// 调用后端API 添加管理员
postAdmin({
adminName,
passWord
})
.then( res => {
toastr.success('管理员添加成功');
// 关闭模态框
$('#addAdminModal').modal('hide');
// 清空表单
document.adminForm.reset();
// 更新列表
getAdminExec();
});
}
// 获取管理员信息 并渲染页面
const getAdminExec = () => {
// 向后端API请求
getAdmin()
.then(res => {
// 将数据发送到管理员表格组件中 将组件设置到模板的指定位置
document.querySelector('#amdinListBox').innerHTML = AdminTabelComponent({adminList: res.data})
});
}
// 执行管理员的删除
const deleteAdminExec = event => {
// 判断目标元素是否是删除按钮
if (event.target.classList.contains('btn-danger')) {
// 弹出确认框
if (confirm('确定删除?')) {
// 调用后端API执行删除
deleteAdmin(event.target.dataset.id)
.then(res => {
alert('删除成功!');
// 更新列表
getAdminExec();
})
}
}
};
export default (req, res) => {
// 加载模板内容
res.render(adminV());
// 获取管理员信息
getAdminExec();
// 给添加按钮监听click事件
document.querySelector('#addAdminBtn').addEventListener('click', addAdminExec)
// 给管理员列表中的删除按钮监听单击事件 事件位置
document.querySelector('#amdinListBox').addEventListener('click', deleteAdminExec)
}课堂画面






代码示例
代码 1:axios 实例 + 响应拦截器 + API 二次封装(src/request/advserver.js、src/api/admin.js)
js
// ============ src/request/advserver.js ============
// 导入 axios
import axios from 'axios';
import toastr from 'toastr';
// 创建 axios 实例
const advServer = axios.create({
baseURL: '/api',
timeout: 5000
});
// 给实例设置响应拦截器
advServer.interceptors.response.use(res => {
if (res.data.ok !== 1) {
toastr.error(res.data.msg);
// 返回不改变状态的 Promise,执行链结束了
return new Promise(()=>{});
}
return res.data;
}, error => {
toastr.error('请求错误!');
// 返回不改变状态的 Promise,执行链结束了
return new Promise(()=>{});
});
// 暴露实例
export default advServer;
// ============ src/api/admin.js ============
// 导入实例
import advServer from '../request/advserver';
// 添加管理员
export const postAdmin = body => {
return advServer.post('/admin', body);
}
// 获取管理员
export const getAdmin = () => {
return advServer.get('/admin');
}
// 删除指定id的管理员
export const deleteAdmin = id => {
return advServer.delete('/admin/'+id);
}
// 修改密码
export const changePassword = body => {
return advServer.patch('/changpwd', body)
}要点解释
res.data.ok !== 1判断的是「HTTP 200 但业务失败」的情况;而 4xx/5xx、网络错误走第二个回调,两类错误都在拦截器里统一toastr.error,控制器里就不用重复写错误提示。return new Promise(()=>{})是永远不 resolve 的 Promise,作用是让后面的.then()不再执行、执行链中断——这是「统一处理后不再往下走」的关键行,也是易错点:写成return或return res都会让业务代码继续执行。- 成功时
return res.data,所以控制器里.then(res => …)拿到的res已经是响应体本身,不再是 axios 包装对象,取数据时要写res.data(后端返回的 data 字段),不要写res.data.data。 src/api/admin.js里一个后端接口对应一个函数,URL 全部写死在这里;advServer.delete('/admin/'+id)注意'/'前要拼上 id,少写斜杠会变成/admin123。- 这些函数都
return了 axios 的 Promise,所以调用处依然能.then(),并且一定会经过响应拦截器。
代码 2:控制器调度 + 表格组件(src/controllers/admin.js、src/components/AdminTable.ejs)
js
// ============ src/controllers/admin.js ============
// 导入 toastr
import toastr from 'toastr';
import '../../node_modules/toastr/build/toastr.css';
// 导入发送请求的函数
import { postAdmin, getAdmin, deleteAdmin } from '../api/admin';
// 导入管理员列表表格的组件
import AdminTabelComponent from '../components/AdminTable';
// 导入模板页
import adminV from '@/views/admin';
// 执行添加管理员账号
const addAdminExec = () => {
// 获取表单数据
const adminName = document.adminForm.adminName.value.trim();
const passWord = document.adminForm.passWord.value.trim();
const rePassWord = document.adminForm.rePassWord.value.trim();
// 管理员账号只能由英文字母组成 ^[a-zA-Z]+$
if (adminName.search(/^[a-zA-Z]+$/) === -1) {
toastr.error('管理员账号只能由英文字母组成!');
return;
}
// 密码是6-18位的数字、字母、下划线 ^\w{6,18}$
if (passWord.search(/^\w{6,18}$/) === -1) {
toastr.error('密码必须由6到18位数字、字母、下划线组成');
return;
}
// 确认密码
if (passWord !== rePassWord) {
toastr.error('两次密码不一致');
return;
}
// 调用后端API 添加管理员
postAdmin({
adminName,
passWord
})
.then( res => {
toastr.success('管理员添加成功');
// 关闭模态框
$('#addAdminModal').modal('hide');
// 清空表单
document.adminForm.reset();
// 更新列表
getAdminExec();
});
}
// 获取管理员信息 并渲染页面
const getAdminExec = () => {
// 向后端API请求
getAdmin()
.then(res => {
// 将数据发送到管理员表格组件中 将组件设置到模板的指定位置
document.querySelector('#amdinListBox').innerHTML = AdminTabelComponent({adminList: res.data})
});
}
// 执行管理员的删除
const deleteAdminExec = event => {
// 判断目标元素是否是删除按钮
if (event.target.classList.contains('btn-danger')) {
// 弹出确认框
if (confirm('确定删除?')) {
// 调用后端API执行删除
deleteAdmin(event.target.dataset.id)
.then(res => {
alert('删除成功!');
// 更新列表
getAdminExec();
})
}
}
};
export default (req, res) => {
// 加载模板内容
res.render(adminV());
// 获取管理员信息
getAdminExec();
// 给添加按钮监听click事件
document.querySelector('#addAdminBtn').addEventListener('click', addAdminExec)
// 给管理员列表中的删除按钮监听单击事件 事件位置
document.querySelector('#amdinListBox').addEventListener('click', deleteAdminExec)
}ejs
<!-- ============ src/components/AdminTable.ejs ============ -->
<table class="table table-bordered">
<thead>
<tr>
<!-- <th style="width: 10px">ID</th> -->
<th>用户名</th>
<th>最后登录时间</th>
<th>注册时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<% data.adminList.forEach(adminItem => { %>
<tr>
<!-- <td>648d3b13dbeadd55199ab0a7</td> -->
<td><%= adminItem.adminName %></td>
<td><%= adminItem.lastLoginTime %></td>
<td><%= adminItem.regTime %></td>
<td>
<button type="button" class="btn btn-danger" data-id="<%= adminItem._id %>">删除</button>
<button type="button" class="btn btn-success">修改密码</button>
</td>
</tr>
<% }) %>
</tbody>
</table>要点解释
- 控制器只做「调度」:调用 API 拿数据 → 交给组件渲染成字符串 → 塞进模板指定位置(
#amdinListBox的innerHTML)。注意#amdinListBox与AdminTabelComponent这两个拼写是笔记原文,改动时模板与控制器的 id 必须一致,否则querySelector返回null报错。 - 组件是函数,调用
AdminTabelComponent({adminList: res.data})时传的对象在 EJS 里用data.adminList取;res.data已经是数组(后端返回体里的 data 字段)。 - 删除用事件委托绑在
#amdinListBox上:因为列表是异步渲染、每次删除后还会重新渲染,直接给按钮绑事件会失效。 event.target.classList.contains('btn-danger')先判断点的是不是删除按钮,再用event.target.dataset.id取自定义属性data-id(data-后面的连字符写法对应驼峰id)。- 添加成功后三件事缺一不可:
$('#addAdminModal').modal('hide')关闭模态框、document.adminForm.reset()清空表单、getAdminExec()刷新列表。$是 jQuery 的全局变量,来自 public 中引入的 jQuery,且必须在自己的脚本之前引入。 document.adminForm是表单元素的特殊取法,等价于document.forms.adminForm,前提是表单有name="adminForm"。- 校验用
search(正则),返回-1表示不匹配;.trim()先去掉首尾空格,否则只输空格也能过\w{6,18}之外的判断。
代码 3:用 sweetalert2 替换原生 confirm(依据本节转写稿视频 6 整理)
js
// 安装:npm i sweetalert2
// 导入(无需再单独导入 css)
import Swal from 'sweetalert2';
// 执行管理员的删除
const deleteAdminExec = event => {
// 判断目标元素是否是删除按钮
if (!event.target.classList.contains('btn-danger')) return;
// 漂亮的确认框,返回 Promise
Swal.fire({
title: '确定删除?',
icon: 'warning',
showCancelButton: true,
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(result => {
// 只有点了「确定」才执行删除
if (result.isConfirmed) {
deleteAdmin(event.target.dataset.id)
.then(res => {
// 删除成功后的提示框
Swal.fire({
title: '删除成功',
icon: 'success'
});
// 更新列表
getAdminExec();
})
}
});
};要点解释
Swal.fire({...})返回 Promise,用.then(result => …)接结果;result.isConfirmed为true才是点了确认按钮,否则是取消——这一步替代了原来的if (confirm(...))。- 变量名必须是
Swal(大写 S),转写稿里写成小写SWAL直接报SWAL is not defined。 showCancelButton: true才会出现取消按钮;confirmButtonText/cancelButtonText用来把按钮文案改成中文。- sweetalert2 只需
import Swal from 'sweetalert2',不用像 toastr 那样再引toastr.css。 - 删除成功后更新列表
getAdminExec()放在Swal.fire之后即可,不必等提示框关闭再刷新。
常见错误
接口报错了,页面照样弹"添加成功" 现象:后端返回
ok: -1或 500,.then里的成功提示仍然执行。 原因:拦截器的失败分支return res;/return undefined;/ 只写了toastr.error没写 return,执行链没有被真正中断,失败被"洗白"成成功。 修法:两个分支统一return new Promise(() => {}),让链停在一个永远 pending 的 Promise 上;不要在失败分支返回任何有值的 Promise。postAdmin(...).then is not a function或.then拿到undefined现象:控制器里调 api 函数,控制台报Cannot read properties of undefined (reading 'then'),或res是 undefined。 原因:api 文件里写了export const postAdmin = body => { advServer.post('/admin', body) }——加了花括号却没有return。 修法:要么去掉花括号用表达式体,要么在花括号里显式return advServer.post(...)。封装 api 时"有花括号必写 return"。列表渲染报
data.adminList.forEach is not a function,表格空白 现象:页面表格区域一片空白,控制台报 forEach 不是函数。 原因:拦截器已经返回了res.data(响应体),then里又直接把res传给组件,组件拿到的adminList是undefined。 修法:组件入参写{adminList: res.data},先console.log(res)确认当前层拿到的是哪一层数据。删除按钮第二次点击就失效,或一次点击删两条 现象:第一次删除正常,刷新列表后再点没反应;或者点一下弹出两次确认。 原因:每次
innerHTML重渲染后又对按钮/容器重新addEventListener,监听器叠加;或者监听绑在会被替换掉的按钮本身上。 修法:只在export default初始化阶段对固定的父容器#amdinListBox绑定一次 click,靠event.target.classList.contains('btn-danger')做事件委托判断。删除请求地址变成
/admin/undefined,后端 404 现象:点删除发出请求,URL 里是 undefined,删除无效。 原因:ejs 里属性写成data-ID/ 取值写成dataset.ID,HTML 属性大小写不敏感而dataset是驼峰,对不上。 修法:属性统一写data-id="<%= adminItem._id %>",取值统一event.target.dataset.id;点中按钮内文字时用closest('button')兜底。$ is not defined或Swal is not defined现象:点添加按钮想关模态框时控制台报错,模态框不关。 原因:public里 jQuery / bootstrap 的引入顺序不对,或使用前脚本尚未加载;sweetalert2 则是导入名大小写写错(swalvsSwal)。 修法:jQuery 必须在 bootstrap 之前引入,自己写的脚本放最后;第三方库的默认导出名照抄文档,不自己发明。点"取消"也把数据删了 现象:确认框弹出来,点取消,记录照样被删。 原因:
confirm(...)的返回值没接,或者接了但删除逻辑写在了if外面。 修法:确认逻辑必须包住整个请求调用,Swal.fire(...).then(result => { if (result.isConfirmed) { ... } }),未确认时直接不进入分支。删除/添加成功后列表不刷新,必须手动 F5 现象:提示"删除成功",但表格里那条还在。 原因:只写了提示,忘了在成功回调里重新拉取列表。 修法:把
getAdminExec()抽成可复用函数,在所有写操作的成功回调末尾统一调用一次,顺序为"提示 → 关弹层/清表单 → 刷新列表"。
练习
在
src/api/admin.js中补全changePassword(PATCH /changpwd,请求体含 id 与新旧密码),并在控制器里把表格中"修改密码"按钮接上:点击后弹表单 → 提交 → 成功后刷新列表。把
deleteAdminExec里的原生confirm换成 sweetalert2 的确认框,要求:标题"确定删除?"、确认/取消按钮均为中文、确认后发请求、成功后再弹一个icon: 'success'的提示框,最后刷新列表。全程不得出现alert。改造响应拦截器:网络错误、超时(
ECONNABORTED)、4xx、5xx 分别给不同的 toastr 文案;同时保证同一次请求只弹一次提示(思考:error.response存在与不存在的分支怎么区分)。给
AdminTable.ejs加空态处理:adminList长度为 0 时渲染一行"暂无管理员数据",跨列显示;并验证添加/删除后空态与正常态能正确切换。把
addAdminExec里的三段表单校验抽成validateAdminForm(data),返回{ ok: boolean, msg: string },控制器只负责"校验不通过就 toastr 并 return,通过就发请求",体会校验逻辑与请求逻辑分离后的可测试性。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | axios二次封装 | 21:41 |
| 2 | 当前项目目录结构分析 | 03:47 |
| 3 | 获取管理员数据并渲染页面 | 16:34 |
| 4 | 完成添加管理员并更新列表 | 09:06 |
| 5 | 删除管理员 | 08:56 |
| 6 | 使用sweetalert2优化确认框 | 11:14 |
小结
- 拦截器的价值不在"少写几行 toastr",而在把"失败"统一收敛成一种终态——链在拦截器处断掉,业务层的
.then就天然等价于"百分百成功",业务代码不需要再写 if-else 判 ok。 request只管一件事:造一个配好 baseURL、timeout、拦截器的实例;它不关心任何具体业务接口,所以它只被 api 层引用,绝不被控制器引用。- api 层是"后端接口的镜像",一个函数对应一个接口(或多个接口组合成一个函数),URL 与请求方式在 api 层写完,控制器里就只剩业务语义,连路径都不用知道。
- 控制器始终是调度中心:导入 api 拿数据、导入组件把数据变成字符串、再把字符串塞进模板占位元素。三步顺序固定,任何一步跳过都会在页面上表现为"没反应"。
- 事件委托不是可选项:列表是动态
innerHTML生成的,绑定必须落在不会被替换的父容器上,并只绑一次;判断目标用类名或closest,不要依赖节点层级。 - 写操作的成功回调是一个固定套路——提示 → 收尾(关模态框、
reset()表单)→ 复用同一个getAdminExec()刷新列表,三件事漏一件用户就会觉得"没生效"。 - 第三方 UI 库(toastr、sweetalert2、bootstrap 插件)的正确用法只在官方文档里,不在记忆里;导入名、配置项、返回的 Promise 形态一律照抄文档,出错时先怀疑名字和版本。
- 分层带来的收益是"改一处、活一片":刷新逻辑抽成函数后,添加、删除、改密码三处都能直接调用;反过来说,如果发现某段逻辑需要复制粘贴第二遍,就是该抽函数的信号。