Skip to content

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

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

代码 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 之后即可,不必等提示框关闭再刷新。

常见错误 ​

  1. 接口报错了,页面照样弹"添加成功" 现象:后端返回 ok: -1 或 500,.then 里的成功提示仍然执行。 原因:拦截器的失败分支 return res; / return undefined; / 只写了 toastr.error 没写 return,执行链没有被真正中断,失败被"洗白"成成功。 修法:两个分支统一 return new Promise(() => {}),让链停在一个永远 pending 的 Promise 上;不要在失败分支返回任何有值的 Promise。

  2. 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"。

  3. 列表渲染报 data.adminList.forEach is not a function,表格空白 现象:页面表格区域一片空白,控制台报 forEach 不是函数。 原因:拦截器已经返回了 res.data(响应体),then 里又直接把 res 传给组件,组件拿到的 adminList 是 undefined。 修法:组件入参写 {adminList: res.data},先 console.log(res) 确认当前层拿到的是哪一层数据。

  4. 删除按钮第二次点击就失效,或一次点击删两条 现象:第一次删除正常,刷新列表后再点没反应;或者点一下弹出两次确认。 原因:每次 innerHTML 重渲染后又对按钮/容器重新 addEventListener,监听器叠加;或者监听绑在会被替换掉的按钮本身上。 修法:只在 export default 初始化阶段对固定的父容器 #amdinListBox 绑定一次 click,靠 event.target.classList.contains('btn-danger') 做事件委托判断。

  5. 删除请求地址变成 /admin/undefined,后端 404 现象:点删除发出请求,URL 里是 undefined,删除无效。 原因:ejs 里属性写成 data-ID / 取值写成 dataset.ID,HTML 属性大小写不敏感而 dataset 是驼峰,对不上。 修法:属性统一写 data-id="<%= adminItem._id %>",取值统一 event.target.dataset.id;点中按钮内文字时用 closest('button') 兜底。

  6. $ is not defined 或 Swal is not defined 现象:点添加按钮想关模态框时控制台报错,模态框不关。 原因:public 里 jQuery / bootstrap 的引入顺序不对,或使用前脚本尚未加载;sweetalert2 则是导入名大小写写错(swal vs Swal)。 修法:jQuery 必须在 bootstrap 之前引入,自己写的脚本放最后;第三方库的默认导出名照抄文档,不自己发明。

  7. 点"取消"也把数据删了 现象:确认框弹出来,点取消,记录照样被删。 原因:confirm(...) 的返回值没接,或者接了但删除逻辑写在了 if 外面。 修法:确认逻辑必须包住整个请求调用,Swal.fire(...).then(result => { if (result.isConfirmed) { ... } }),未确认时直接不进入分支。

  8. 删除/添加成功后列表不刷新,必须手动 F5 现象:提示"删除成功",但表格里那条还在。 原因:只写了提示,忘了在成功回调里重新拉取列表。 修法:把 getAdminExec() 抽成可复用函数,在所有写操作的成功回调末尾统一调用一次,顺序为"提示 → 关弹层/清表单 → 刷新列表"。

练习 ​

  1. 在 src/api/admin.js 中补全 changePassword(PATCH /changpwd,请求体含 id 与新旧密码),并在控制器里把表格中"修改密码"按钮接上:点击后弹表单 → 提交 → 成功后刷新列表。

  2. 把 deleteAdminExec 里的原生 confirm 换成 sweetalert2 的确认框,要求:标题"确定删除?"、确认/取消按钮均为中文、确认后发请求、成功后再弹一个 icon: 'success' 的提示框,最后刷新列表。全程不得出现 alert。

  3. 改造响应拦截器:网络错误、超时(ECONNABORTED)、4xx、5xx 分别给不同的 toastr 文案;同时保证同一次请求只弹一次提示(思考:error.response 存在与不存在的分支怎么区分)。

  4. 给 AdminTable.ejs 加空态处理:adminList 长度为 0 时渲染一行"暂无管理员数据",跨列显示;并验证添加/删除后空态与正常态能正确切换。

  5. 把 addAdminExec 里的三段表单校验抽成 validateAdminForm(data),返回 { ok: boolean, msg: string },控制器只负责"校验不通过就 toastr 并 return,通过就发请求",体会校验逻辑与请求逻辑分离后的可测试性。

原视频对照 ​

#视频时长
1axios二次封装21:41
2当前项目目录结构分析03:47
3获取管理员数据并渲染页面16:34
4完成添加管理员并更新列表09:06
5删除管理员08:56
6使用sweetalert2优化确认框11:14

小结 ​

  1. 拦截器的价值不在"少写几行 toastr",而在把"失败"统一收敛成一种终态——链在拦截器处断掉,业务层的 .then 就天然等价于"百分百成功",业务代码不需要再写 if-else 判 ok。
  2. request 只管一件事:造一个配好 baseURL、timeout、拦截器的实例;它不关心任何具体业务接口,所以它只被 api 层引用,绝不被控制器引用。
  3. api 层是"后端接口的镜像",一个函数对应一个接口(或多个接口组合成一个函数),URL 与请求方式在 api 层写完,控制器里就只剩业务语义,连路径都不用知道。
  4. 控制器始终是调度中心:导入 api 拿数据、导入组件把数据变成字符串、再把字符串塞进模板占位元素。三步顺序固定,任何一步跳过都会在页面上表现为"没反应"。
  5. 事件委托不是可选项:列表是动态 innerHTML 生成的,绑定必须落在不会被替换的父容器上,并只绑一次;判断目标用类名或 closest,不要依赖节点层级。
  6. 写操作的成功回调是一个固定套路——提示 → 收尾(关模态框、reset() 表单)→ 复用同一个 getAdminExec() 刷新列表,三件事漏一件用户就会觉得"没生效"。
  7. 第三方 UI 库(toastr、sweetalert2、bootstrap 插件)的正确用法只在官方文档里,不在记忆里;导入名、配置项、返回的 Promise 形态一律照抄文档,出错时先怀疑名字和版本。
  8. 分层带来的收益是"改一处、活一片":刷新逻辑抽成函数后,添加、删除、改密码三处都能直接调用;反过来说,如果发现某段逻辑需要复制粘贴第二遍,就是该抽函数的信号。