Node.js Express 中间件
上一篇 Node.js Express 入门 里,express.json() 被当成固定写法直接用了。这篇把它背后的机制讲透。中间件是 Express 的核心设计,路由只是整个链路里的一环,理解了中间件,大部分 Express 应用的代码结构都能一眼看懂。
中间件是什么
中间件就是一个普通函数,接收三个参数。
function myMiddleware(req, res, next) {
// 处理请求
next();
}
请求从进入应用到返回响应,要依次穿过一串中间件,每个中间件可以读取和修改 req、res,做完自己的事后调用 next() 把控制权交给下一个。这就是一层层的处理管道。
浏览器请求
↓
中间件 1,写日志,调用 next()
↓
中间件 2,解析请求体,调用 next()
↓
路由处理函数,发送响应
↓
响应返回浏览器
路由处理函数其实是管道的最末端,它不调用 next,而是直接 res.json 结束整个链路。
next 的作用
next 是中间件之间的接力棒,不调用它,请求就卡在当前中间件里,客户端会一直等不到响应。
app.use((req, res, next) => {
console.log("记录日志");
// 忘了调用 next(),请求到此为止
});
next 也可以接收一个参数,传错误对象表示链路出错。
app.use((req, res, next) => {
next(new Error("出错了"));
});
带错误的 next 会跳过后面所有普通中间件,直接进入错误处理中间件,这个机制在后面的小节单独讲。
执行顺序
中间件严格按注册顺序执行,用两个 console.log 验证。
app.use((req, res, next) => {
console.log("第 1 个中间件");
next();
});
app.use((req, res, next) => {
console.log("第 2 个中间件");
next();
});
app.get("/", (req, res) => {
res.send("首页");
});
访问 / 时终端依次输出第 1 个中间件、第 2 个中间件。把两个 app.use 的顺序对调,输出顺序也跟着变。所以注册顺序就是执行顺序,日志类中间件要放在最前面。
全局中间件和路由级中间件
app.use 注册的是全局中间件,对应用里的所有路由生效。
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
如果只想让某个路由生效,可以把中间件函数作为路由的第二个参数,这称为路由级中间件。
function checkLogin(req, res, next) {
const token = req.query.token;
if (!token) {
return res.status(401).json({ message: "未登录" });
}
next();
}
// 只有 /profile 会检查登录
app.get("/profile", checkLogin, (req, res) => {
res.json({ message: "个人主页" });
});
一个路由可以挂多个中间件,按顺序执行,最后一个是真正的处理函数。后面的 JWT 身份认证篇会大量用到这种写法。
内置中间件
Express 自带几个常用的中间件。
express.json()
解析 JSON 请求体,把结果放到 req.body。客户端请求头要带 Content-Type: application/json 才会生效。
express.urlencoded()
解析表单请求体,对应 Content-Type: application/x-www-form-urlencoded,也就是 HTML 表单默认的提交格式。
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
express.static()
托管静态文件,把 public 目录下的文件直接暴露给浏览器。
app.use(express.static("public"));
public 目录里放一张 logo.png,浏览器直接访问 http://localhost:3000/logo.png 就能拿到图片。前端构建产物、图片、字体这类资源都用它托管。
第三方中间件
npm 生态有大量现成中间件,装好注册即可用。跨域问题在前后端分离项目里几乎必遇到。
npm install cors morgan
const cors = require("cors");
const morgan = require("morgan");
app.use(cors()); // 允许跨域请求
app.use(morgan("dev")); // 输出请求日志,dev 是日志格式
morgan 会在终端打印每次请求的方法、路径、状态码和耗时。cors 解决浏览器跨域拦截,开发阶段直接全局开启,上线前再按域名收紧。
错误处理中间件
错误处理中间件是特殊的中间件,函数签名有 4 个参数,多一个 err。Express 靠参数个数识别它。
app.use((err, req, res, next) => {
console.error(err.message);
res.status(500).json({ code: 500, message: "服务器内部错误" });
});
同步代码里抛出异常,或者调用 next(err),都会跳过普通中间件直接跳到这个函数。它必须注册在所有路由之后,否则前面的请求不会经过它。
404 兜底是另一种常用写法,匹配所有未命中的路由。
// 放在所有路由后面,任何未匹配的请求都到这里
app.use((req, res) => {
res.status(404).json({ code: 404, message: "接口不存在" });
});
错误处理中间件要放在 404 兜底后面,顺序是普通路由、404 兜底、错误处理。
常见坑
错误中间件少一个参数
错误处理中间件必须写满 4 个参数,少写 next 就变成普通中间件,Express 认不出来,错误没人接。
异步错误不会自动捕获
处理函数里 await 数据库操作抛错,在老版本 Express 里不会进入错误中间件,需要在 try/catch 里手动 next(err)。Express 5 会自动捕获异步错误,但习惯性用 try/catch 包住更保险。
中间件注册顺序错了
404 兜底和错误处理必须放在所有路由之后。日志、解析请求体这些中间件要放最前面。顺序问题不会报错,只会表现为某些请求没日志、错误不落兜底,排查起来费时间。
实践
给 Express 应用加上完整的中间件链,请求日志、404 兜底、全局错误处理一次配齐。
// server.js
const express = require("express");
const morgan = require("morgan");
const app = express();
// 全局中间件,按顺序执行
app.use(morgan("dev")); // 请求日志
app.use(express.json()); // 解析 JSON 请求体
// 正常路由
app.get("/", (req, res) => {
res.json({ message: "首页" });
});
// 故意抛错的测试路由
app.get("/boom", (req, res) => {
throw new Error("手动抛错");
});
// 404 兜底,匹配所有未命中的路由
app.use((req, res) => {
res.status(404).json({ code: 404, message: "接口不存在" });
});
// 全局错误处理中间件,必须放最后
app.use((err, req, res, next) => {
console.error("出错了", err.message);
res.status(500).json({ code: 500, message: "服务器内部错误" });
});
app.listen(3000, () => {
console.log("服务器已启动");
});
npm install express morgan
node server.js
# 另开终端,分别触发三种情况
curl http://localhost:3000/
curl http://localhost:3000/boom
curl http://localhost:3000/nothing
终端里能同时看到 morgan 的访问日志和错误处理中间件打的日志。请求 /boom 时错误中间件接管,返回 500。请求 /nothing 时 404 兜底生效。
中间件机制到此讲完,下一篇 Node.js RESTful API 设计 把接口的 URL、方法、状态码设计规范统一起来。