第7章 Node框架实战篇 - Express 中间件与RESTful API 接口规范
Express 不同中间件类别的使用方式
应用级中间件
通过
app.use([path], middleware)或app.METHOD([path], middleware)注册。path是可选的,省略时对所有路径生效。
const express = require('express');
const app = express();// 全局中间件:对所有请求生效
app.use((req, res, next) => {console.log('Request URL:', req.originalUrl);next(); // 必须调用 next() 将控制权交给下一个中间件
});// 局部中间件:只对 /user 路径的 GET 请求生效
const specificMiddleware = (req, res, next) => {console.log('Accessing /user endpoint');next();
};
app.get('/user', specificMiddleware, (req, res) => {res.send('User Info');
});路由级中间件
使用
express.Router()创建一个路由实例,然后像使用app一样使用这个router实例。
const express = require('express');
const router = express.Router(); // 创建路由实例// 在路由实例上使用中间件
router.use((req, res, next) => {console.log('Time: ', Date.now());next();
});router.get('/list', (req, res) => {res.send('User List');
});// 最后将路由实例挂载到应用的某个路径下(如 /api)
const app = express();
app.use('/api', router); // 现在访问 /api/user/list 会触发上述中间件和路由错误处理中间件
必须放在所有路由和其他中间件之后
// 在路由中抛出错误(同步)
app.get('/problematic', (req, res) => {throw new Error('Something went wrong!'); // 同步错误直接 throw
});// 在路由中传递错误(异步)
app.get('/async-problematic', (req, res, next) => {someAsyncFunction().then(data => res.send(data)).catch(error => next(error)); // 异步错误通过 next(error) 传递
});// 定义在最后的错误处理中间件
app.use((err, req, res, next) => { // 注意4个参数console.error(err.stack);res.status(500).send('Something broke!');
});内置中间件
Express 提供了几个开箱即用的中间件,最常见的是解析请求体和托管静态文件。
const express = require('express');
const app = express();// 解析 application/json 格式的请求体数据
app.use(express.json());
// 解析 application/x-www-form-urlencoded 格式的请求体数据
app.use(express.urlencoded({ extended: false }));
// 托管静态文件,例如 public 目录下的图片、CSS、HTML
app.use(express.static('public'));// 使用解析后的数据
app.post('/profile', (req, res) => {// 配置了中间件后,req.body 才有值console.log(req.body);res.json({ message: 'Data received', data: req.body });
});第三方中间件
首先通过 npm 安装,然后引入并使用。
// 1. 终端运行:npm install cookie-parser
const express = require('express');
const cookieParser = require('cookie-parser'); // 2. 引入
const app = express();app.use(cookieParser()); // 3. 注册app.get('/', (req, res) => {// 现在可以读取请求中的 cookie 了console.log('Cookies: ', req.cookies);res.send('Hello World');
});关键注意事项
执行顺序至关重要
中间件的执行顺序取决于它们在代码中注册(app.use)的顺序。请求会依次经过匹配的中间件。除了错误处理中间件,其他所有中间件(如解析中间件)必须在路由之前配置。
调用 next()
如果中间件函数不结束请求-响应周期(比如通过 res.send()),必须调用 next() 将控制权传递给下一个中间件,否则请求会被挂起。
错误处理中间件的位置
这是一个特例,必须放在所有其他 app.use()和路由调用之后,以确保能捕获到前面所有环节抛出的错误
Express路由与响应方法
类别 | 方法 | 主要用途 | 关键特点 |
|---|---|---|---|
路由定义 |
| 处理特定HTTP方法(GET、POST等)的请求 | 精准匹配HTTP动词和路径 |
| 处理所有HTTP方法对某路径的请求 | 适用于全局中间件或路径预处理 | |
| 为同一路径创建可链式调用的路由处理程序 | 避免路径重复书写,代码更清晰 | |
| 创建模块化的路由处理程序 | 实现路由的模块化和管理 | |
响应方法 |
| 发送多种类型数据(Buffer、String、Object、Array) | 自动设置Content-Type等响应头 |
| 发送JSON响应 | 自动设置Content-Type为application/json | |
| 快速结束响应,不发送数据 | 适用于无响应体的场景 | |
| 设置HTTP状态码 | 通常与其他响应方法链式调用 | |
| 重定向请求 | 可设置状态码(默认302) |
路径匹配示例
// 字符串路径:精确匹配
app.get('/about', (req, res) => {res.send('About us');
});// 字符串模式:使用特殊字符
app.get('/ab?cd', (req, res) => { // 匹配 acd 或 abcdres.send('Pattern matched');
});
app.get('/ab+cd', (req, res) => { // 匹配 abcd, abbcd, abbbcd 等res.send('Pattern matched with plus');
});// 正则表达式:更灵活的匹配
app.get(/.*fly$/, (req, res) => { // 匹配 butterfly, dragonfly 等res.send('Regex matched');
});// 路由参数:捕获路径中的值
app.get('/users/:userId/books/:bookId', (req, res) => {// 通过 req.params 访问捕获的值res.send(`User ID: ${req.params.userId}, Book ID: ${req.params.bookId}`);
});高级路由技巧
链式路由
使用
app.route()为同一路径定义多个方法
app.route('/book').get((req, res) => { res.send('Get a random book'); }).post((req, res) => { res.send('Add a book'); }).put((req, res) => { res.send('Update the book'); });模块化路由
使用
express.Router()创建可挂载的模块化路由处理程序
// birds.js
const express = require('express');
const router = express.Router();router.get('/', (req, res) => { res.send('Birds home page'); });
router.get('/about', (req, res) => { res.send('About birds'); });module.exports = router;// 主文件 app.js
const birdsRouter = require('./birds');
app.use('/birds', birdsRouter); // 现在可以处理 /birds 和 /birds/about 的请求响应方法详解
核心响应方法使用场景
res.send([body]):最通用的响应方法
// 发送字符串(Content-Type 自动设为 "text/html")
res.send('<p>Some HTML</p>');// 发送对象或数组(Content-Type 自动设为 "application/json")
res.send({ user: 'tobi' });
res.send([1, 2, 3]);// 发送 Buffer(Content-Type 自动设为 "application/octet-stream")
res.send(new Buffer('whoop'));// 结合状态码
res.status(404).send('Sorry, we cannot find that!');res.json([body]):专门发送JSON响应,会自动将非对象(如null、undefined)转换为JSON
res.json({ user: 'tobi' });
res.status(500).json({ error: 'message' });
res.json(null); // 有效,尽管null不是有效JSONres.end([data][, encoding]):快速结束响应,不发送数据。除非需要无响应体结束,否则优先用 res.send()或 res.json()
res.status(404).end(); // 结束响应,不发送数据res.redirect([status,] path):重定向
res.redirect('/new-page'); // 默认302
res.redirect(301, '/permanent-new-page'); // 永久重定向其他实用响应方法
res.status(code):设置HTTP状态码,常链式调用res.set(field [, value])或res.header(field [, value]):设置响应头res.type(type):设置 Content-Typeres.cookie(name, value [, options]):设置 Cookie
进阶应用与性能考量
中间件与路由结合
const logTime = (req, res, next) => {console.log('Time:', Date.now());next(); // 必须调用 next() 将控制权传递给下一个中间件或路由
};app.use(logTime); // 应用级中间件,对所有路由生效
app.get('/specific', logTime, (req, res) => { // 路由级中间件res.send('Specific route with logging');
});错误处理
定义错误处理中间件时,函数参数为四个 (
err, req, res, next)
app.get('/problematic', (req, res, next) => {try {// 可能出错的操作} catch (error) {next(error); // 将错误传递给错误处理中间件}
});// 错误处理中间件(定义在路由之后)
app.use((err, req, res, next) => {console.error(err.stack);res.status(500).send('Something broke!');
});项目基础设计搭建优化
安装 express
pnpm i express -Srouter\user.js
定义用户路由
const express = require('express');
const router = express.Router();
const userConroller = require('../controller/user');router.get('/list', userConroller.list);
router.get('/:id', userConroller.getUserById);module.exports = router;
controller\user.js
定义用户 controller
// 获取用户列表
const list = (req, res) => {res.send('get user list');
};// 获取指定id的用户
const getUserById = (req, res) => {res.send(`get user ${req.params.id}`);
};module.exports = {list,getUserById,
};
router\index.js
路由主入口,一次性导出所有路由
const express = require('express');
const router = express.Router();router.use('/user', require('./user'));
router.use('/video', require('./video'));module.exports = router;
app.js
应用主入口,进行一些全局配置,例如路由、日志、中间件等
const express = require('express');
const app = express();// 设置相应数据类型
app.use(express.json());
app.use(express.urlencoded({ extended: true }));// 设置路由
app.use(require('./router'));// 设置静态文件目录
app.use(express.static('public'));// 监听3000端口
app.listen(3000, () => {console.log('服务器已启动,监听端口3000');
});
数据处理模块-mongoose
安装 mongoose
pnpm i mongoose@6.3.1 -S基本使用
model\userModel.js
const mongoose = require('mongoose');const userSchema = new mongoose.Schema({username: {type: String,required: true,},email: {type: String,required: true,},password: {type: String,required: true,},phone: {type: String,required: true,},image: {type: String,default: null,},createdAt: {type: Date,default: Date.now(),},updateAt: {type: Date,default: Date.now(),},
});module.exports = userSchema;
model\index.js
使用 mongoose 连接数据库,导出所有数据库 Model
const mongoose = require('mongoose');
async function mian() {await mongoose.connect('mongodb://localhost:27017/express-video');
}mian().then((res) => {console.log('mongo链接成功');}).catch((err) => {console.log(err);console.log('mongo链接失败');});module.exports = {User: mongoose.model('User', require('./userModel')),
};
用户注册数据入库
router\user.js
用户注册路由
const express = require('express');
const router = express.Router();
const userConroller = require('../controller/user');router.post('/register', userConroller.register); module.exports = router;
controller\user.js
处理与用户路由相关的业务逻辑
const { User } = require('../model');
// 用户注册
const register = async (req, res) => {// 创建用户 model 并存储const user = new User(req.body);try {await user.save();res.status(200).send('注册成功');} catch (error) {res.status(400).send(error.message);}
};module.exports = {register,
};
使用 postman 测试用户注册功能


用户注册的密码加密问题
使用node内置的crypto库封装 md5 加密
util\md5.js
const crypto = require('crypto')
module.exports = str => {return crypto.createHash('md5').update('by' + str).digest('hex')
}
model\userModel.js
使用封装好的 md5 加密
const md5 = require('../util/md5');
password: {type: String,required: true,set: (val) => {return md5(val);},}
客户端提交数据安全校验
安装 express-validator 库,用于提交参数校验
pnpm i express-validator@6.14 -Smiddleware\validator\errorBack.js
封装统一错误请求处理函数
const { validationResult } = require('express-validator');// 错误处理中间件
module.exports = (validator) => {return async (req, res, next) => {// 执行验证器await Promise.all(validator.map((valid) => valid.run(req)));const errors = validationResult(req);if (!errors.isEmpty()) {return res.status(400).send(errors.array()[0].msg);}next();};
};
middleware\validator\userValidator.js
const { body } = require('express-validator');
const validate = require('./errorBack');// 用户注册验证规则
module.exports.register = validate([body('username').notEmpty().withMessage('用户名不能为空').bail().isLength({ min: 3 }).withMessage('用户名长度最小为3').bail(),body('password').notEmpty().withMessage('密码不能为空').bail().isLength({ min: 6, max: 16 }).withMessage('密码长度最小为6,最大为16').bail(),body('email').notEmpty().withMessage('邮箱不能为空').bail().isEmail().withMessage('邮箱格式不正确').bail(),
]);
router\user.js
在用户路由使用上述封装好的用户参数校验工具
const express = require('express');
const router = express.Router();
const userConroller = require('../controller/user');
const validator = require('../middleware/validator/userValidator');router.post('/register', validator.register, userConroller.register);module.exports = router;

数据唯一性验证
middleware\validator\userValidator.js
通过自定义校验规则验证邮箱和手机号是否已注册
body('email').notEmpty().withMessage('邮箱不能为空').bail().isEmail().withMessage('邮箱格式不正确').custom((value) => {return User.findOne({ email: value }).then((user) => {if (user) {return Promise.reject('邮箱已被注册');}});}).bail(),body('phone').notEmpty().withMessage('手机不能为空').bail().custom((value) => {return User.findOne({ phone: value }).then((user) => {if (user) {return Promise.reject('手机号已被注册');}});}).bail()用户登录信息对比
验证登录用户信息:邮箱是否已注册
middleware\validator\userValidator.js
// 用户登录验证规则
module.exports.login = validate([body('email').notEmpty().withMessage('邮箱不能为空').bail().isEmail().withMessage('邮箱格式不正确').custom((value) => {return User.findOne({ email: value }).then((user) => {if (!user) {return Promise.reject('邮箱未注册');}});}).bail(),body('password').notEmpty().withMessage('密码不能为空').bail().isLength({ min: 6, max: 16 }).withMessage('密码长度最小为6,最大为16').bail(),
]);JWT用户身份认证
安装 jsonwebtoken
pnpm i jsonwebtoken - Sutil\jwt.js
封装生成token以及校验token工具函数
const jwt = require('jsonwebtoken');
const { promisify } = require('util');
const { uuid } = require('../config/config.default');
// jwt.sign 转换为promise
const toJwt = promisify(jwt.sign);
// jwt.verify 转换为promise
const verifyJwt = promisify(jwt.verify);// 生成 token
module.exports.createToken = async (user) => {const token = await toJwt({ user }, uuid, {expiresIn: 60 * 60,});return token;
};// jwt 验证
module.exports.verifyToken = async (req, res, next) => {//从 header 读取 token, 并解析 tokenconst authorization = req.headers.authorization;const token = authorization ? authorization.split('Bearer ')[1] : null;if (!token) {return res.status(401).send('token 不存在');}try {const user = await verifyJwt(token, uuid);next();} catch (error) {res.status(401).send('token 验证失败');}
};
controller\user.js
用户登录成功后生成token并返回
const { createToken } = require('../util/jwt');
const login = async (req, res) => {try {const dbBack = await User.findOne({email: req.body.email,password: req.body.password,});if (dbBack) {// 将数据库返回的数据转换为json格式const user = dbBack.toJSON();// 生成 tokenconst token = await createToken(user);user.token = token;res.status(200).json(user);} else {res.status(400).json('账号密码错误');}} catch (error) {res.status(400).json(error.message);}
};
router\user.js
在其他需要登录状态下访问的接口添加校验token 中间件
const { verifyToken } = require('../util/jwt');
// 用户列表
router.get('/list', verifyToken, userConroller.list);

