Skip to content

添加管理员验证数据与使用 toastr 提示信息(项目功能实现 3/3) ​

本页对应 课堂视频,共 6 段视频,约 1 小时 34 分。

学习目标 ​

  1. 能够分析管理员列表的增删改查需求,准确定位模板、控制器与路由职责,并在控制器中完成添加管理员的数据获取与提交逻辑。
  2. 能够使用表单 name 和 DOM 方式获取管理员账号、密码、确认密码,运用正则表达式完成账号与密码格式校验,并通过 toastr 给出对应提示。
  3. 能够安装并配置 toastr,正确处理其 JS 与 CSS 的引入,修改配置后重启项目并验证提示效果。
  4. 能够配置 webpack devServer 代理,使用 pathRewrite 与 changeOrigin 解决开发环境跨域问题,并能解释代理服务器转发请求与响应的原理。
  5. 能够使用 axios.create 创建请求实例并设置 baseURL、超时时间和响应拦截器,统一判断后端 code、处理网络错误与业务错误,并通过返回不改变状态的 Promise 中断后续执行链。
  6. 能够通过封装后的请求实例调用添加管理员接口,在成功回调中更新管理员列表,并使用 sweetalert2 优化删除确认框。

知识点 ​

本节要解决的问题 ​

为什么需要:添加管理员不能点一下按钮就直接把数据发给后端。账号格式、密码长度、两次密码是否一致,都应该在提交前先检查,否则会给后端制造无效请求,也不利于用户及时改正。
怎么想:模板只负责结构和样式,验证逻辑放到控制器里;点击“添加”按钮时读取表单值,按规则逐条判断,不通过就提示并 return。
怎么做:先确认模态框里的表单控件有 name,按钮有 id,再在控制器中导入模板、渲染页面并绑定点击事件。
易错点:按钮在笔记里是 type="button",不会自动提交表单,所以要手动监听 click;监听代码要确保运行在浏览器端,若在 Node 控制器里直接执行,document 会未定义。

先给表单控件补 name 和按钮 id ​

为什么需要:验证时要通过表单快速拿到三个输入框的值,name 就是读取入口;按钮也需要 id 才能绑定事件。
怎么想:转写稿里强调,这里不是直接把表单提交给后端,而是先用 JS 取数据、做验证,所以 name 可以先按语义取,后面发请求时再和后端接口字段对齐。
怎么做:账号输入框用 name="adminName",密码用 name="passWord",重复密码用 name="rePassWord";添加按钮设置 id="addAdminBtn"。
易错点:name 拼写必须和读取时完全一致;不要用容易重复的类名取元素,优先用 id。

在控制器中绑定添加按钮 ​

为什么需要:点击“添加”后要执行验证函数,必须有事件入口。
怎么想:转写稿的调试顺序是:先 console.log 看按钮能否取到,再绑定 click 弹出 OK,确认事件通了自己再写业务。
怎么做:在控制器里 document.querySelector('#addAdminBtn').addEventListener('click', addAdminExec),把具体逻辑封装到 addAdminExec。
易错点:事件绑定前要确保 DOM 已渲染;函数名要和定义一致;querySelector 的选择器要带 #。

获取三个输入值并去空格 ​

为什么需要:用户可能误输入前后空格,直接比较会影响验证结果。
怎么想:用表单 name 快速定位:document.adminForm.adminName.value,再统一 trim()。
怎么做:分别取 adminName、passWord、rePassWord 三个变量。
易错点:表单 name 写成 adminForm 后,读取时不要写成 document.adminform;密码一般不建议 trim 改变原值,但笔记中按示例处理,实际项目要团队约定。

账号验证:只允许英文字母 ​

为什么需要:管理员账号有格式约束,避免中文、数字、符号混入。
怎么想:用正则 /^[a-zA-Z]+$/,^ 和 $ 表示整串匹配,+ 表示至少一个字母。
怎么做:adminName.search(/^[a-zA-Z]+$/) === -1 时提示“管理员账号只能由英文字母组成!”并 return。
易错点:search 不匹配返回 -1;不要漏掉首尾锚点,否则 abc123 也可能被部分匹配。

密码验证:6 到 18 位数字字母下划线 ​

为什么需要:限制密码长度和字符范围,符合后端要求。
怎么想:用 /^\w{6,18}$/,JS 中 \w 等价于字母、数字、下划线。
怎么做:passWord.search(/^\w{6,18}$/) === -1 时提示“密码必须由6到18位数字、字母、下划线组成”。
易错点:{6,18} 不要写成 {6-18};要区分大小写时注意 \w 的范围。

确认密码:两次必须一致 ​

为什么需要:防止用户输错密码,造成后续登录失败。
怎么想:前两条通过后,再比较 passWord !== rePassWord。
怎么做:不一致就 alert('两次密码不一致') 并 return。
易错点:比较的是两个变量,不是输入框元素;return 不能漏,否则会继续执行后面的添加逻辑。

调试顺序与常见错误 ​

为什么需要:验证规则多,直接写完容易不知道哪一步没生效。
怎么想:转写稿的做法是先测按钮事件,再逐条测空账号、中文账号、短密码、不一致密码。
怎么做:每加一条规则就点一次“添加”,看提示是否符合预期;控制台可用 console.log 看取到的值。
易错点:同音字要纠正为“模态框、正则、控制台、name、id、search、test”;alert 只是本节临时提示,后续可换成更友好的提示组件。

完整代码 ​

src/controllers/admin.js:

js
// 导入模板页
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) {
        alert('管理员账号只能由英文字母组成!');
        return;
    }

    // 密码是6-18位的数字、字母、下划线 ^\w{6,18}$
    if (passWord.search(/^\w{6,18}$/) === -1) {
        alert('密码必须由6到18位数字、字母、下划线组成');
        return;
    }

    // 确认密码
    if (passWord !== rePassWord) {
        alert('两次密码不一致');
        return;
    }

}

export default (req, res) => {
    // 加载模板内容
    res.render(adminV());

    // 给添加按钮监听click事件
    document.querySelector('#addAdminBtn').addEventListener('click', addAdminExec)
  
}

src/views/admin.ejs:

ejs
<div id="__sub-route-view">
    <div class="card adminlist">
        <div class="card-header">
            <button type="button" class="btn btn-primary btn-sm" data-toggle="modal"
                data-target="#addAdminModal">添加管理员</button>
        </div>
        <!-- /.card-header -->
        <div class="card-body" id="amdinListBox">
            <table class="table table-bordered">
                <thead>
                    <tr>
                        <th style="width: 10px">ID</th>
                        <th>用户名</th>
                        <th>最后登录时间</th>
                        <th>注册时间</th>
                        <th>操作</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>648d3b13dbeadd55199ab0a7</td>
                        <td>admin</td>
                        <td>2023-06-17 12:50:22</td>
                        <td>2023-06-17 12:48:19</td>
                        <td>
                            <button type="button" class="btn btn-danger" data-id="648d3b13dbeadd55199ab0a7">删除</button>
                            <button type="button" class="btn btn-success">修改密码</button>
                        </td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>

    <!-- 模态框 -->
    <div class="modal fade" id="addAdminModal" style="display: none;" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h4 class="modal-title">添加管理员</h4>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">×</span>
                    </button>
                </div>
                <div class="modal-body">
                    <form name="adminForm" id="addAdminForm">
                        <div class="form-group">
                            <label for="exampleInputEmail1">管理员账号</label>
                            <input type="text" name="adminName" class="form-control" id="exampleInputEmail1" placeholder="请输入管理员账号">
                        </div>
                        <div class="form-group">
                            <label for="exampleInputPassword1">管理员密码</label>
                            <input type="password" name="passWord" class="form-control" id="exampleInputPassword1"
                                placeholder="请输入管理员密码">
                        </div>
                        <div class="form-group">
                            <label for="exampleInputPassword2">重复密码</label>
                            <input name="rePassWord" type="password" class="form-control" id="exampleInputPassword2"
                                placeholder="请输入管理员密码">
                        </div>
                    </form>
                </div>
                <div class="modal-footer justify-content-between">
                    <button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
                    <button type="button" id="addAdminBtn" class="btn btn-primary">添加</button>
                </div>
            </div>
            <!-- /.modal-content -->
        </div>
        <!-- /.modal-dialog -->
    </div>
</div>

为什么需要 toastr 提示信息 ​

为什么需要:添加管理员时,如果账号格式不对、密码不合规、两次密码不一致,必须给用户明确反馈。最早用原生 alert 也能提示,但转写稿里演示时也吐槽“最 low 的弹框”,它打断操作、样式也差。
怎么想:提示信息应该由执行添加逻辑的控制器函数统一触发,模板页只负责结构,不写业务判断。
怎么做:引入 toastr,在验证失败时调用 toastr.error('提示内容'),并 return 终止后续提交。
易错点:不要只在模板里写提示;验证失败后忘记 return,代码会继续往下执行。

安装 toastr 产品依赖 ​

为什么需要:toastr 是运行时提示插件,页面发布后仍然需要它,所以属于产品依赖,不是开发依赖。
怎么想:转写稿中明确说“必须是产品依赖”,因此安装时不要加 --save-dev。
怎么做:

shell
npm install toastr

易错点:把它装成开发依赖,后续打包部署时可能缺失;安装后要确认 package.json 中出现在 dependencies 里。

配置 webpack 处理 CSS ​

为什么需要:toastr 不只有 JS,还有自己的 CSS。转写稿里特别强调这是个“老包”,需要单独导入 CSS;而项目要能处理 .css,webpack 必须配置对应 loader。
怎么想:先让 webpack 认识 CSS,再在控制器里导入 toastr.css,否则提示能弹出来但没有样式。
怎么做:修改 config/webpack.base.config.js,在 module.rules 中加入:

js
 module: {
        rules: [
            
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            }
        ]
    },

易错点:改完 webpack 配置必须重启开发服务;test 写错、loader 顺序写反、漏装 style-loader/css-loader 都会导致 CSS 不生效。

在控制器中导入 toastr ​

为什么需要:只有导入后,控制器里的 toastr.error 才有定义。
怎么想:转写稿中先导入 JS,再单独导入 node_modules/toastr/build/toastr.css,因为老包不会自动加载样式。
怎么做:在 src/controllers/admin.js 顶部写:

js
// 导入 toastr
import toastr from 'toastr';
import '../../node_modules/toastr/build/toastr.css';

// 导入模板页
import adminV from  '@/views/admin';

易错点:CSS 路径容易写错;不要误写成 toastr.css 的包名路径;如果样式没出现,先检查 CSS 是否真的被导入、webpack 是否重启。

在表单验证中调用 toastr.error ​

为什么需要:添加管理员前要先验证数据,符合要求才允许继续提交。
怎么想:把添加逻辑封装成 addAdminExec,先取表单值并 trim() 去空格,再按规则逐项验证;不通过就用 toastr.error 提示并 return。
怎么做:笔记中的核心代码如下:

js
// 执行添加管理员账号
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;
    }

}

export default (req, res) => {
    // 加载模板内容
    res.render(adminV());

    // 给添加按钮监听click事件
    document.querySelector('#addAdminBtn').addEventListener('click', addAdminExec)
  
}

易错点:search() 不匹配返回 -1,判断条件不要写反;三个输入框的 name 要与代码一致;按钮要绑定 click,且验证失败后必须 return。

调试过程与易错点 ​

转写稿中演示了完整调试:先点“添加管理员”,不填账号会提示“管理员账号只能由英文字母组成”;输入中文或数字仍不通过;密码位数不足会提示密码规则;两次密码不一致会提示“两次密码不一致”;全部通过后暂时不再弹错误。这个过程说明验证顺序和 return 很关键。
易错点:toastr 的 CSS 没导入时提示框样式会缺失;修改 webpack 配置后忘记重启;表单按钮若 type="button",不会自动提交表单,需要手动监听点击;后续真正发请求时还要处理接口返回的 ok 值,不能只看请求是否成功。

安装 axios 产品依赖 ​

为什么需要:添加管理员不能只在前端校验,最终要把数据提交给后端;浏览器里发请求最常用的是 axios。转写稿里明确说它是“产品依赖”,因为上线部署后仍然要发请求,不是只在开发时用。

怎么想:先装依赖,再写请求。安装时不要加 -D,否则会被当成开发依赖。

怎么做:

shell
npm install axios

易错点:把 axios 装成开发依赖;装完不重启开发服务;在模板文件里直接写请求逻辑,导致模板不纯。

配置开发代理解决跨域 ​

为什么需要:转写稿调试时直接请求后端出现跨域:前端页面是 shirly.com:80,后端是 127.0.0.1:8088,协议、域名、端口不一致,浏览器会拦截。开发阶段用 webpack devServer 代理,让浏览器先请求同源的 /api/...,再由代理转发给后端。

怎么想:接口文档写的是 /admin,但前端希望用 /api/admin 作为识别前缀。代理收到 /api/admin 后,要转发到 http://127.0.0.1:8088/admin,所以需要 pathRewrite 把开头的 /api 去掉。转写稿还提到 changeOrigin:默认会保留原 host,设为 true 后可把 host 改成目标服务器,避免后端识别出代理来源。

怎么做:修改 config/webpack.dev.config.js:

js
const {merge} = require('webpack-merge');
const baseConfig = require('./webpack.base.config');

module.exports = merge(baseConfig, {

    // 模式
    mode: 'development',

    // 自动开启服务
    devServer: {
        host: 'shirly.com',
        port: '80',
        open: true,
        // 开发中,允许前端路由使用 history 模式
        historyApiFallback: true,

        proxy: {
            // /api/admin  -> http://127.0.0.1:8088/api/admin
            // '/api': 'http://127.0.0.1:8088'

            // /api/admin  -> http://127.0.0.1:8088/admin
            '/api': {
                target: 'http://127.0.0.1:8088',
                pathRewrite: { '^/api': ''},
            }

        }
    }
})

易错点:只写 '/api': 'http://127.0.0.1:8088' 会把 /api/admin 原样转发成 /api/admin,而后端接口是 /admin,会 404;改 webpack 配置后必须重启 devServer;前端请求地址要写 /api/admin,不能写后端完整地址。

使用 axios 调用添加管理员接口 ​

为什么需要:表单校验通过后,要把 adminName、passWord 提交给后端。转写稿强调接口是 POST,请求体是 JSON,字段名要和接口文档对上。

怎么想:先在控制器里监听添加按钮,再在 addAdminExec 中取表单值、校验、发请求。校验用 search:返回 -1 表示不匹配。请求成功后不能只看“请求发出去了”,还要看后端返回的 ok 是否为 1;转写稿中后端统一返回对象,ok: 1 才是成功,ok: -1 是业务失败。

怎么做:src/controller/admin.js 保留如下代码:

js
// 导入 toastr
import toastr from 'toastr';
import '../../node_modules/toastr/build/toastr.css';

// 导入 axios
import axios from 'axios';

// 导入模板页
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 发送请求
    axios
    .post('/api/admin', {
        adminName,
        passWord
    })
    .then( res => {
        console.log(res);
    })
    .catch (err => {
        console.log('请示失败', err);
    })

}

export default (req, res) => {
    // 加载模板内容
    res.render(adminV());

    // 给添加按钮监听click事件
    document.querySelector('#addAdminBtn').addEventListener('click', addAdminExec)
  
}

易错点:post 第二个参数是请求体对象,不是 URL 拼接;字段名写错后端收不到;then 里直接提示“成功”不严谨,要判断 res.data.ok;404 或网络错误会走 catch,但“账号已存在”这类业务失败可能仍走 then。转写稿进一步演示了用 axios 实例和响应拦截器统一处理:失败时提示 message,并返回不改变状态的 Promise 中断执行链,避免继续提示成功。

为什么要把响应处理统一起来 ​

为什么需要:添加管理员时,后端可能返回 { ok: 1, msg: ... },也可能返回 { ok: -1, msg: '管理员账号已经存在' }。后者虽然 HTTP 请求成功,then 也会执行,但它并不是业务成功。如果每个接口都自己判断 ok、自己弹错误提示,代码会重复且容易漏判。

怎么想:把“判断业务是否成功、提示错误、处理 404/500/网络错误”放到 axios 实例的响应拦截器里。控制器只关心成功后的页面提示。

怎么做:创建 src/request/advserver.js,用 axios.create 建实例,设置 baseURL: '/api'、timeout: 5000,再挂响应拦截器。

易错点:拦截器里拿到的是 axios 响应对象,业务数据在 res.data;判断的是 res.data.ok,不是 res.ok。

创建实例并设置响应拦截器 ​

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;

推导过程:转写稿中先直接发请求,发现后端返回 ok: -1 时,then 仍然执行,页面误以为成功。因此需要在响应进入 then 之前拦截:ok !== 1 就 toastr.error(res.data.msg),然后返回一个不改变状态的 Promise,让后续 .then 不再执行。失败回调 error 处理的是 404、500、网络错误、跨域等请求层错误,也统一提示“请求错误!”。

易错点:不要在这里返回 Promise.reject(...)。转写稿特别提醒:返回失败状态还可能继续走 catch,而这里的目的就是让执行链结束;new Promise(()=>{}) 不 resolve 也不 reject,后续成功回调不会执行。

控制器里只处理成功分支 ​

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)
  
}

为什么需要:控制器里已经有表单校验,失败时 return。真正发请求后,业务失败应由拦截器统一提示,控制器只写成功提示。

怎么想:advServer.post(...).then(res => ...) 中的 res 已经不是完整响应对象,而是拦截器 return res.data 后的业务数据。失败时拦截器返回 pending Promise,所以这里的 toastr.success 不会执行。

怎么做:导入 advServer 实例,不再直接导入 axios;成功回调里提示“管理员添加成功”,并打印 res 便于调试。

易错点:toastr 的 CSS 只需在项目里导入一次,不要每个文件重复导入;拦截器文件里也要导入 toastr,否则无法提示;控制器里的表单校验不能省,因为账号格式、密码长度、两次密码一致属于前端校验,后端业务失败才交给拦截器。

本节目标:把 axios 用到“可维护” ​

为什么需要:添加管理员时,要取表单值、验证账号密码、发请求、提示成功失败;如果每个功能都直接写 axios.post,业务码判断和错误提示会到处复制。
怎么想:把“请求公共逻辑”抽到 request 实例里,把“页面交互逻辑”留在控制器里,模板只负责结构。
怎么做:先完成表单验证,再安装 axios、toastr,创建实例并加响应拦截器,最后用代理解决跨域。
易错点:不要把所有逻辑塞进 admin.ejs;请求体字段必须按接口文档,例如 adminName、password。

先验证表单,再发请求 ​

为什么需要:管理员账号只能由英文字母组成,密码要求 6 到 18 位数字、字母、下划线,两次密码要一致;不先验证会浪费请求。
怎么想:点击“添加”不是直接提交表单,而是监听按钮单击,先取三个输入框的值并 trim(),逐项校验。
怎么做:转写稿里用 document.adminForm.adminName.value 这种表单快捷取值;账号正则 /^[A-Za-z]+$/,密码可用 /^\w{6,18}$/,不一致就提示并 return。
易错点:input 的 name 可以为了取值自己起,但发给后端的字段名必须和 API 文档对齐;test() 不匹配返回 false,别把 -1 和 test 混用。

js
const adminName = document.adminForm.adminName.value.trim()
const password = document.adminForm.password.value.trim()
const rePassword = document.adminForm.rePassword.value.trim()

if (!/^[A-Za-z]+$/.test(adminName)) {
  toastr.error('管理员账号只能由英文字母组成')
  return
}
if (!/^\w{6,18}$/.test(password)) {
  toastr.error('密码必须是6到18位数字、字母和下划线')
  return
}
if (password !== rePassword) {
  toastr.error('两次密码不一致')
  return
}

用 toastr 替代原生弹框 ​

为什么需要:alert 体验差,线上示例用的是右下角提示。
怎么想:toastr 是现成提示库,成功用 success,失败用 error。
怎么做:安装 toastr,导入 JS 和 CSS;老包需要单独引入 toastr/build/toastr.min.css,若构建报 CSS 解析错误,按项目现有 webpack 规则处理。
易错点:只导入 JS 可能没有样式;改 webpack 配置后要重启开发服务。

js
import toastr from 'toastr'
import 'toastr/build/toastr.min.css'

配置代理:先让请求同源 ​

为什么需要:页面在 shirley.com,接口在 127.0.0.1:8088,协议/域名/端口不一致会跨域。
怎么想:开发阶段让浏览器请求 devServer 的 /api,devServer 再转发给真实后端;服务器之间没有浏览器跨域限制。
怎么做:在 devServer 配 proxy,用 pathRewrite 去掉 /api,否则后端会收到 /api/admin。
易错点:baseURL 写 /api,请求写 /admin;改配置必须重启;changeOrigin 可按后端限制决定是否开启。

js
devServer: {
  proxy: {
    '/api': {
      target: 'http://127.0.0.1:8088',
      changeOrigin: true,
      pathRewrite: { '^/api': '' }
    }
  }
}

axios 二次封装:实例 + 响应拦截器 ​

为什么需要:后端统一返回 { ok, message },ok === 1 才是成功;但 ok: -1 时 HTTP 仍是成功响应,会进入 then,每个请求都判断很重复。
怎么想:创建 advServer 实例,统一 baseURL、超时和响应拦截;业务码不是 1 就提示并中断执行链。
怎么做:成功回调里判断 res.data.ok,失败回调提示“请求出现错误”;两者都返回 new Promise(() => {}),让后续 then 不再执行。
易错点:axios 响应对象是包了一层的,业务数据在 res.data;失败回调若返回普通值,执行链仍可能继续。

js
import axios from 'axios'
import toastr from 'toastr'

const advServer = axios.create({
  baseURL: '/api',
  timeout: 5000
})

advServer.interceptors.response.use(
  res => {
    if (res.data.ok !== 1) {
      toastr.error(res.data.message)
      return new Promise(() => {})
    }
    return res.data
  },
  error => {
    toastr.error('请求出现错误')
    return new Promise(() => {})
  }
)

export default advServer

控制器调用与调试顺序 ​

为什么需要:封装后控制器只关心“成功做什么”。
怎么想:先打印按钮确认能取到,再验证表单,再发请求看 404/跨域,最后配代理看真实返回。
怎么做:导入实例,await advServer.post('/admin', { adminName, password }),成功提示“管理员添加成功”。
易错点:接口字段以文档为准;ok: -1 时拦截器已提示,控制器不应再当成功处理;删除、列表等也应走同一实例。

本节要完成什么:先把管理员数据“取出来、验一遍、发出去” ​

转写稿里这一节虽然标题是“获取管理员数据并渲染页面”,但实际演示重点落在管理员列表页的“添加管理员”流程:从表单取数据、前端验证、用 toastr 提示、通过代理调用后端接口,并用 axios 响应拦截器统一处理业务错误。列表渲染要等后端返回数据后再做,本节先把数据链路打通。
为什么需要:如果直接在模板里写逻辑,页面会越来越乱;如果只做后端验证,用户要等请求回来才知道格式错,体验差。
怎么想:模板只负责结构,事件监听、取值、验证、发请求放在控制器对应的 JS 里;提交前先做前端校验,提交后用统一拦截器处理 { ok, message }。
怎么做:先给表单控件起 name,在按钮上监听点击,取表单值并验证,再发请求。
易错点:不要把模板写成逻辑文件;不要以为按钮在 form 外就不能监听点击;name 可以前端先定,但真正发给后端的字段名要按接口文档对齐。

表单取值:用表单元素的 name 快速拿数据 ​

为什么需要:添加管理员要拿账号、密码、确认密码三个值,逐个用 ID 取也可以,但表单元素有更直接的取法。
怎么想:转写稿中给 form 起名 adminForm,给输入框起 adminName、password、rePassword,然后通过 document.adminForm.adminName.value 取。
怎么做:

js
const adminName = document.adminForm.adminName.value.trim();
const password = document.adminForm.password.value.trim();
const rePassword = document.adminForm.rePassword.value.trim();

易错点:这种写法只适用于表单控件;要加 trim() 去掉两边空格;变量名和接口字段名最好一致,减少后面组装请求体的麻烦。

正则验证:账号、密码、确认密码分别校验 ​

为什么需要:线上示例会提示“管理员账号只能由英文字母组成”“密码必须是 6 到 18 位数字字母下划线”“两次密码不一致”,这些都应该在提交前拦住。
怎么想:账号用英文字母正则,密码用 6–18 位数字字母下划线正则,确认密码直接比较。
怎么做:

js
if (adminName.search(/^[A-Za-z]+$/) === -1) {
  toastr.error('管理员账号只能由英文字母组成');
  return;
}
if (password.search(/^[A-Za-z0-9_]{6,18}$/) === -1) {
  toastr.error('密码必须是6到18位数字字母下划线');
  return;
}
if (password !== rePassword) {
  toastr.error('两次密码不一致');
  return;
}

易错点:search 不匹配返回 -1;不要只校验密码不校验确认密码;正则要写开始 ^ 和结束 $,否则可能只匹配一部分。

用 toastr 替换 alert:提示要统一、好看 ​

为什么需要:转写稿中演示线上页面时,提示是浮层,不是原生 alert;原生弹框体验差。
怎么想:toastr 提供 success、error、info 等方法,适合替换验证失败和请求结果提示。
怎么做:安装 toastr,在控制器中导入 JS;老版本还要单独导入 CSS:

js
import toastr from 'toastr';
import 'toastr/build/toastr.css';

然后验证失败处写 toastr.error('...'),成功处写 toastr.success('管理员添加成功')。
易错点:只导 JS 不导 CSS 可能没样式;改 webpack 配置后要重启;老包可能需要深入 node_modules 导 CSS,这是转写稿中特别强调的。

配置 devServer 代理:开发阶段解决跨域 ​

为什么需要:前端页面在 localhost,后端接口在 127.0.0.1:8088,协议、域名、端口不一致会跨域。转写稿中实际请求报跨域,所以用代理。
怎么想:浏览器只限制页面直接请求;让页面先请求同源的 devServer,再由 devServer 转发给后端,就不跨域。
怎么做:在 webpack 的 devServer.proxy 中配置:

js
proxy: {
  '/api': {
    target: 'http://127.0.0.1:8088',
    changeOrigin: true,
    pathRewrite: { '^/api': '' }
  }
}

请求地址写 /api/admin,代理会去掉 /api 再拼到后端地址。
易错点:不写 pathRewrite 会把 /api 也带给后端;改配置必须重启;代理是开发手段,上线通常由后端允许静态资源服务器地址,或部署时统一处理。

axios 拦截器:统一处理“HTTP 成功但业务失败” ​

为什么需要:后端统一返回 { ok, message },ok === 1 才是成功;账号已存在时 HTTP 可能仍是成功响应,但 ok 是 -1。如果每个请求都判断,代码重复。
怎么想:创建 axios 实例,设置 baseURL: '/api',在响应拦截器里统一判断 res.data.ok。失败时提示并返回一个不改变状态的 Promise,中断后续 .then。
怎么做:

js
import axios from 'axios';
import toastr from 'toastr';

const advServer = axios.create({
  baseURL: '/api',
  timeout: 5000
});

advServer.interceptors.response.use(
  res => {
    if (res.data.ok !== 1) {
      toastr.error(res.data.message);
      return new Promise(() => {});
    }
    return res.data;
  },
  () => {
    toastr.error('请求出现错误');
    return new Promise(() => {});
  }
);

export default advServer;

易错点:axios 响应对象包了一层,业务数据在 res.data;失败时如果返回普通值,后面还可能继续执行;返回 new Promise(() => {}) 是为了中断执行链。
这样控制器里只关心成功逻辑,错误提示交给拦截器统一处理。

添加管理员:先验证,再提交 ​

为什么需要:添加管理员不能点一下就直接发请求。账号、密码、确认密码都有格式要求,前端先拦一道,能减少无效请求,也能让用户立刻知道错在哪。转写稿里先演示线上效果:账号只能英文字母,密码 6 到 18 位数字、字母、下划线,两次密码要一致;这些都要在提交前验证。

怎么想:按钮在模板里,但业务逻辑不要写在模板中。模板只负责结构和弹模态框;点击按钮后的取值、验证、发请求放在控制器里。转写稿特别提醒:模态框由 Bootstrap 的 data-toggle、data-target 控制,不需要我们手写弹出逻辑。表单控件要加 name,方便用 document.adminForm.adminName.value 这类表单专属方式取值,并统一 trim() 去空格。

怎么做:给添加按钮监听单击事件,取三个值后按顺序验证:

js
const adminName = document.adminForm.adminName.value.trim();
const password = document.adminForm.password.value.trim();
const rePassword = document.adminForm.rePassword.value.trim();

if (!/^[A-Za-z]+$/.test(adminName)) {
  toastr.error('管理员账号只能由英文字母组成');
  return;
}
if (!/^\w{6,18}$/.test(password)) {
  toastr.error('密码必须是6到18位数字、字母和下划线');
  return;
}
if (password !== rePassword) {
  toastr.error('两次密码不一致');
  return;
}

易错点:验证不通过一定要 return,否则还会继续发请求;账号正则不要漏掉首尾锚点;密码规则转写稿用 \w 表示数字、字母、下划线;取值后要 trim(),避免用户前后空格造成误判。

用 toastr 替代 alert ​

为什么需要:alert 太生硬,转写稿演示线上效果后明确要换成 toastr 这类提示插件。成功、失败、错误分别用不同颜色提示,体验更接近真实项目。

怎么想:toastr 是第三方包,先安装再引入。它属于产品依赖,因为上线后仍然要显示提示。转写稿还强调:这个包比较老,除了引入 JS,还要单独引入 CSS,否则没效果。

怎么做:

bash
npm i toastr -S
js
import toastr from 'toastr';
import 'toastr/build/toastr.min.css';

验证失败处用 toastr.error('提示信息'),添加成功处用 toastr.success('管理员添加成功')。

易错点:只引 JS 不引 CSS 是最常见问题;老包路径要进 node_modules 找;不要每个页面重复引 CSS,统一在请求模块或入口处理即可。

跨域与 devServer 代理 ​

为什么需要:转写稿调试时发现页面地址和接口地址协议、域名、端口不一致,浏览器报跨域。后端不一定愿意加响应头,尤其团队安全限制严格时,前端可以用代理解决开发期跨域。

怎么想:页面和代理服务器由同一个 devServer 提供,协议、域名、端口一致,所以浏览器请求代理不跨域;代理服务器再去请求真正的后端,服务器之间不受浏览器同源策略限制。请求地址加 /api 作为标识,让代理识别哪些请求需要转发。

怎么做:在 devServer 代理配置中写:

js
proxyTable: {
  '/api': {
    target: 'http://127.0.0.1:8088',
    changeOrigin: true,
    pathRewrite: { '^/api': '' }
  }
}

易错点:不写 pathRewrite 会把 /api 也带给后端,导致 404;changeOrigin 用于改写 host;改配置后必须重启 devServer;代理只是开发期手段,上线后应由后端允许静态资源服务器地址或走统一网关。

axios 实例与响应拦截器 ​

为什么需要:后端可能返回“账号已存在”这类业务失败,HTTP 状态仍是 200,如果只看 then 会误判成功。转写稿用拦截器统一判断成功标识,并统一提示错误,避免每个接口重复写。

怎么想:创建 axios 实例,统一 baseURL: '/api';响应拦截器里先判断业务成功标识,不是成功值就提示并中断执行链。网络错误、404 等走失败回调。中断用 return new Promise(() => {}),返回 pending 状态,后面的 then 不再执行。

怎么做:

js
import axios from 'axios';
import toastr from 'toastr';

const service = axios.create({ baseURL: '/api', timeout: 5000 });

service.interceptors.response.use(
  res => {
    // 转写稿称成功标识为 OK,实际字段按接口文档替换
    if (res.data.OK !== 1) {
      toastr.error(res.data.msg);
      return new Promise(() => {});
    }
    return res.data;
  },
  () => {
    toastr.error('请求出现错误');
    return new Promise(() => {});
  }
);

export default service;

易错点:拦截器返回的是响应对象,业务数据在 res.data;失败回调里如果 return Promise.reject() 又没捕获,会产生未处理错误;用 pending Promise 是为了让执行链静默结束。

添加成功后更新列表 ​

转写稿本节完成到“添加成功并提示”,列表更新没有展开具体代码,因此不虚构实现。正确思路是:添加接口成功后,复用已有列表查询逻辑重新拉取数据,不要手动往 DOM 里拼一条假数据。易错点是只提示成功却不刷新,或直接改页面导致与数据库不一致。

从“添加管理员”流程说起 ​

为什么需要:管理员列表里已有“添加管理员”按钮和 Bootstrap 模态框,但按钮只负责弹框,真正提交前必须验证账号、密码、确认密码。转写稿里的“模特框”应写作“模态框”,“but swift”指 Bootstrap。
怎么想:先明确职责:模板只负责结构和弹框;数据获取、验证、发请求放在控制器对应的前端 JS 中。
怎么做:给输入框加 name,给添加按钮加 id,在控制器里监听单击事件,先取表单数据,再验证,最后提交。
易错点:按钮不在 form 内、type="button" 时不会自动提交,必须手动监听;不要把所有逻辑堆在模板里。

用表单名和 name 取数据 ​

为什么需要:三个输入框的数据要逐项验证,直接操作 DOM 容易拿错元素。
怎么想:转写稿演示了表单专用取法:document.表单name.控件name.value,只适用于表单元素。
怎么做:

js
const adminName = document.adminForm.adminName.value.trim();
const password = document.adminForm.password.value.trim();
const repassword = document.adminForm.repassword.value.trim();

易错点:trim() 去掉首尾空格;普通 div、span 不适用这种写法;变量名与后端字段名可先不同,发请求时再按接口文档组装。

正则验证账号、密码和确认密码 ​

为什么需要:线上要求管理员账号只能由英文字母组成;密码必须是 6 到 18 位数字、字母、下划线;两次密码必须一致。
怎么想:用正则做格式判断,用 search() 判断是否匹配:返回 -1 表示不匹配。
怎么做:

js
const regAdminName = /^[A-Za-z]+$/;
const regPassword = /^[A-Za-z0-9_]{6,18}$/;

if (adminName.search(regAdminName) === -1) {
  Toastr.error('管理员账号只能由英文字母组成');
  return;
}
if (password.search(regPassword) === -1) {
  Toastr.error('密码必须是6到18位数字、字母和下划线');
  return;
}
if (password !== repassword) {
  Toastr.error('两次密码不一致');
  return;
}

易错点:忘记 return 会继续往下执行;正则要加首尾锚点;转写稿里的“政则”应写作“正则”。

用 toastr 替换原始弹框 ​

为什么需要:alert 太生硬,线上演示使用右上角提示。转写稿里的“TVSTR”“TASTR”指 toastr。
怎么想:它是生产依赖,安装后导入 JS;老包还需要单独处理 CSS。
怎么做:

js
import Toastr from 'toastr';

并在 webpack 配置中处理其 CSS;改配置后必须重启。
易错点:只导 JS 可能没有样式;转写稿强调这是“老包”,不要套用新包“只导 JS 就自动带样式”的经验。

axios 实例、代理与响应拦截器 ​

为什么需要:直接请求 http://127.0.0.1:8088/admin 会跨域;后端返回 { ok: 1, message },但 ok 不是 1 时 HTTP 仍可能是成功响应,必须在业务层统一判断。
怎么想:用 axios 实例统一 baseURL,用 devServer 代理解决开发跨域,用响应拦截器统一提示错误并中断后续 then。
怎么做:

js
import axios from 'axios';
import Toastr from 'toastr';

const advServer = axios.create({
  baseURL: '/api',
  timeout: 5000
});

advServer.interceptors.response.use(
  res => {
    if (res.data.ok !== 1) {
      Toastr.error(res.data.message);
      return new Promise(() => {});
    }
    return res.data;
  },
  error => {
    Toastr.error('请求出现错误');
    return new Promise(() => {});
  }
);

export default advServer;

代理配置要点:target 指向真实后端,pathRewrite 去掉 /api,changeOrigin 可覆盖 Host。
易错点:pathRewrite 不去前缀会把 /api 带到后端;改 webpack 配置要重启;拦截器返回 new Promise(() => {}) 是为了不改变状态、终止执行链,转写稿里的“permiss”应写作 Promise。

删除确认框与 sweetalert2 的落点 ​

为什么需要:删除比添加更危险,必须先确认“确定删除/取消”,取消就不发请求。
怎么想:转写稿末尾明确说删除确认框要用“新包”,它的特点是只要导包即可,不用像 toastr 那样再单独导入 CSS。
怎么做:删除按钮先弹确认,取消则 return,确认后再调用删除接口;具体调用代码本节转写稿未展开,不编造。
易错点:不要把“确认框”写成原生 confirm 后又在业务层重复提示;老包与新包的 CSS 引入方式不同,不能混用经验。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:管理员列表模板(src/views/admin.ejs) ​

ejs
<div id="__sub-route-view">
    <div class="card adminlist">
        <div class="card-header">
            <button type="button" class="btn btn-primary btn-sm" data-toggle="modal"
                data-target="#addAdminModal">添加管理员</button>
        </div>
        <!-- /.card-header -->
        <div class="card-body" id="amdinListBox">
            <table class="table table-bordered">
                <thead>
                    <tr>
                        <th style="width: 10px">ID</th>
                        <th>用户名</th>
                        <th>最后登录时间</th>
                        <th>注册时间</th>
                        <th>操作</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>648d3b13dbeadd55199ab0a7</td>
                        <td>admin</td>
                        <td>2023-06-17 12:50:22</td>
                        <td>2023-06-17 12:48:19</td>
                        <td>
                            <button type="button" class="btn btn-danger" data-id="648d3b13dbeadd55199ab0a7">删除</button>
                            <button type="button" class="btn btn-success">修改密码</button>
                        </td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>

    <!-- 模态框 -->
    <div class="modal fade" id="addAdminModal" style="display: none;" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h4 class="modal-title">添加管理员</h4>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">×</span>
                    </button>
                </div>
                <div class="modal-body">
                    <form name="adminForm" id="addAdminForm">
                        <div class="form-group">
                            <label for="exampleInputEmail1">管理员账号</label>
                            <input type="text" name="adminName" class="form-control" id="exampleInputEmail1" placeholder="请输入管理员账号">
                        </div>
                        <div class="form-group">
                            <label for="exampleInputPassword1">管理员密码</label>
                            <input type="password" name="passWord" class="form-control" id="exampleInputPassword1"
                                placeholder="请输入管理员密码">
                        </div>
                        <div class="form-group">
                            <label for="exampleInputPassword2">重复密码</label>
                            <input name="rePassWord" type="password" class="form-control" id="exampleInputPassword2"
                                placeholder="请输入管理员密码">
                        </div>
                    </form>
                </div>
                <div class="modal-footer justify-content-between">
                    <button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
                    <button type="button" id="addAdminBtn" class="btn btn-primary">添加</button>
                </div>
            </div>
            <!-- /.modal-content -->
        </div>
        <!-- /.modal-dialog -->
    </div>
</div>

关键行与易错点

  • data-toggle="modal" + data-target="#addAdminModal" 是 Bootstrap 弹出模态框的开关,data-target 的值必须和模态框的 id 完全一致,写错就弹不出来。
  • 三个 input 的 name(adminName / passWord / rePassWord)是后面 document.adminForm.adminName 这种“表单元素快速取值”的依据,名字必须和控制器里取值的属性名一一对应,改一个就得改另一个。
  • form 的 name="adminForm" 同理,是 document.adminForm 的来源,不要漏写。
  • 添加按钮 id="addAdminBtn" 必须存在,控制器里用 querySelector('#addAdminBtn') 找它;并且它故意写成 type="button",不会触发表单默认提交。
  • 模板文件只放结构,不要在这里写任何 JS 逻辑,逻辑统一放到控制器里。

示例二:控制器中的验证 + 发请求(src/controllers/admin.js) ​

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)

}

关键行与易错点

  • document.adminForm.adminName.value:这是“表单元素专用”的取元素方式(document.表单name.控件name),只对表单控件有效,对 div、img 之类不适用。
  • 每处都 .trim(),防止用户首尾多敲空格导致校验误判。
  • search() 匹配失败返回 -1,所以判断是 === -1 而不是 === 0;正则 ^[a-zA-Z]+$ 有开头有结尾,要求“整个字符串都是字母”。
  • 三个校验里每个失败分支都要 return,否则会继续往下走,出现“既提示错误又发了请求”的情况。
  • toastr 是老包,除了 import toastr from 'toastr',还必须单独引入它的 CSS(../../node_modules/toastr/build/toastr.css),只导 JS 是没有任何样式的;同时 config/webpack.base.config.js 里必须有 { test: /\.css$/, use: ['style-loader', 'css-loader'] },否则 CSS 导入直接报错。
  • advServer.post('/admin', {...}) 里只写 /admin,前缀 /api 由实例的 baseURL 负责拼接;这里如果又写成 /api/admin,最终会变成 /api/api/admin。
  • 装了包记得用产品依赖:npm install toastr、npm install axios;改了 webpack 配置必须重启 devServer 才生效。

示例三:请求实例 + 响应拦截器 + 代理配置 ​

src/request/advserver.js

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;

config/webpack.dev.config.js

js
const {merge} = require('webpack-merge');
const baseConfig = require('./webpack.base.config');

module.exports = merge(baseConfig, {

    // 模式
    mode: 'development',

    // 自动开启服务
    devServer: {
        host: 'shirly.com',
        port: '80',
        open: true,
        // 开发中,允许前端路由使用 history 模式
        historyApiFallback: true,

        proxy: {
            // /api/admin  -> http://127.0.0.1:8088/api/admin
            // '/api': 'http://127.0.0.1:8088'

            // /api/admin  -> http://127.0.0.1:8088/admin
            '/api': {
                target: 'http://127.0.0.1:8088',
                pathRewrite: { '^/api': ''},
            }

        }
    }
})

关键行与易错点

  • 用 axios.create() 建实例而不是直接用 axios,好处是 baseURL、timeout、拦截器都只配一次;项目里若有多个后端,就一个后端建一个实例。
  • 后端约定“响应体里的 ok 为 1 才算成功”,所以判断写在拦截器的成功回调里,用 res.data.ok !== 1;注意必须先 .data 再 .ok,axios 返回的是包裹了一层的响应对象,ok、msg 都在 res.data 里。
  • 拦截器里 return res.data,后续 .then(res => ...) 拿到的就直接是响应体,不用再点 .data。
  • 出错分支返回 new Promise(() => {})(一个永远不改变状态的 Promise),这样执行链就中断了,后面的 .then / .catch 都不会再执行,也就不会出现“提示了错误还提示成功”的尴尬;这里不要返回 Promise.reject(),否则还会走 .catch,不处理就是未捕获异常。
  • 实例要用 export default advServer 暴露,控制器里 import advServer from '../request/advserver' 才拿得到,漏了这句会报“没有暴露数据”。
  • 代理里 target 是真正的后端地址,pathRewrite: { '^/api': '' } 负责把 /api 前缀去掉再转发,不写 pathRewrite 就会把 /api 一起带过去,后端收不到对应路由。
  • 改完 webpack.dev.config.js 必须重启服务,否则代理不生效;跨域是浏览器限制,代理之所以能解决,是因为页面和代理服务器同协议同域名同端口,而服务器与服务器之间不存在跨域。

常见错误 ​

  1. 现象:账号输入 abc 仍提示“只能由英文字母组成”。
    原因:写成 if (adminName.search(/^[a-zA-Z]+$/)),匹配成功返回 0,被当成 false。
    修法:显式判断 === -1,或使用 if (!/^[a-zA-Z]+$/.test(adminName))。

  2. 现象:点击“添加”时报 Cannot read properties of undefined。
    原因:document.adminForm.adminName 取不到元素,input 未设置 name,或 form 的 name 写错。
    修法:给 form 设置 name="adminForm",给三个 input 分别设置 name="adminName" / "passWord" / "rePassWord",并先 trim() 再校验。

  3. 现象:toastr 能弹文字但没有样式,或构建时报 CSS 解析错误。
    原因:只导入了 toastr 的 JS,未导入其 CSS;或 webpack 未配置 style-loader、css-loader。
    修法:导入 ../../node_modules/toastr/build/toastr.css,并在 webpack.base.config.js 的 module.rules 中配置 .css 的 loader;改配置后重启 dev server。

  4. 现象:前端请求 /api/admin 返回 404,后端实际接口是 /admin。
    原因:代理只写了 target,没有用 pathRewrite 去掉 /api 前缀,转发成 http://127.0.0.1:8088/api/admin。
    修法:配置 pathRewrite: { '^/api': '' },前端统一以 /api 作为代理识别前缀。

  5. 现象:修改代理配置后仍跨域或仍走旧地址。
    原因:webpack devServer 配置变更后没有重启服务。
    修法:停止并重新启动 dev server;确认请求地址是 /api/... 而不是直接写 http://127.0.0.1:8088/...。

  6. 现象:后端返回 { ok: -1, msg: '管理员账号已经存在' },页面却提示“添加成功”。
    原因:HTTP 状态是 200,axios 走 then;控制器只判断了网络成功,没有判断业务字段 ok。
    修法:在响应拦截器中判断 res.data.ok !== 1,用 toastr 提示 res.data.msg,并 return new Promise(() => {}) 中断后续 then。

  7. 现象:请求地址变成 /api/api/admin。
    原因:axios 实例已设置 baseURL: '/api',调用时又写了 /api/admin。
    修法:实例统一加 /api 前缀后,业务请求只写 /admin,例如 advServer.post('/admin', {...})。

练习 ​

  1. 给添加管理员表单补齐 name,在控制器中获取三个字段并 trim(),完成账号、密码、确认密码三项校验,全部通过后才发请求。
  2. 安装并接入 toastr,把原来的 alert 全部替换为 toastr.error;补充 webpack 的 CSS loader 配置,并说明为什么 toastr 需要单独导入 CSS。
  3. 配置 devServer.proxy,实现 /api/admin 转发到 http://127.0.0.1:8088/admin;用浏览器 Network 面板验证请求地址和实际转发地址。
  4. 新建 src/request/advserver.js,创建 axios 实例:baseURL: '/api'、timeout: 5000,编写响应拦截器统一处理网络错误和 ok !== 1 的业务失败,成功时返回 res.data。
  5. 改造 src/controllers/admin.js,改用 advServer.post('/admin', { adminName, passWord }),只在 then 中提示“管理员添加成功”;验证业务失败时不会执行该成功提示。

原视频对照 ​

#视频时长
1管理员列表功能分析03:40
2添加管理员验证数据21:30
3使用toastr提示信息13:07
4设置代理并调用添加管理员接口21:29
5代理服务器的实现原理11:05
6axios使用拦截器统一处理可能的错误23:36

小结 ​

  • 表单取数依赖 name,取到后先 trim();正则校验用 search(...) === -1 或 test() 取反,避免把 0 当假值。
  • toastr 属于产品依赖,JS 与 CSS 要分别处理;老包尤其要注意手动导入 CSS,并确保 webpack 能处理 CSS。
  • 开发阶段跨域优先用 devServer 代理解决:前端请求统一加 /api 前缀,代理用 pathRewrite 去掉前缀再转发。
  • 代理配置修改后必须重启 dev server;代理只是开发手段,生产环境应由同源部署或后端 CORS 解决。
  • axios 实例集中管理 baseURL、超时和响应拦截器,业务代码只写相对路径,避免重复拼接。
  • 响应拦截器要区分“网络失败”和“业务失败”:业务失败提示后返回 pending Promise,中断 then 链,防止误报成功。
  • 拦截器成功分支返回 res.data 后,控制器拿到的就是响应体,不再反复写 res.data.data。
  • 表单校验、提示组件、请求实例三层分离后,后续删除、修改、列表查询可以直接复用同一套请求与错误处理逻辑。