Express.js 深度剖析 / Express.js Deep Dive
📅 创建时间:2026-07-28 🏷️ 标签:#Express #Middleware #OnionModel #Router #ErrorHandling #TypeScript #Backend 📚 前置知识:[[00-overview]]
📋 本章目标
- 理解 Express "最小化抽象,最大化灵活性"的核心哲学,明确它与 Koa/Fastify 的定位差异
- 深度掌握洋葱模型——中间件执行顺序、
next()的本质、next(err)的错误跳转机制 - 追踪完整的请求/响应生命周期:从
app.listen到res.json,理解req/res的增强过程 - 掌握路由系统的三个层级:
app.use、app.METHOD、Router,以及子路由挂载与模块化 - 熟练配置生产级中间件栈:cors、helmet、compression、morgan、rate-limit、body parser
- 掌握自定义中间件的设计模式:认证、日志、验证、错误处理、工厂函数
- 理解 Express 4 与 Express 5 的错误处理差异,设计健壮的全局错误处理器
- 能够使用 TypeScript 编写类型安全的 Express 应用,包括声明合并与 Zod 验证的类型推导
第1部分:Express 的核心哲学
1.1 "最小化抽象,最大化灵活性"
Express 的设计哲学可以用一句话概括:它只做两件事——路由匹配和中间件调度,其余全部交给社区。
┌─────────────────────────────────────────────────────────────┐
│ Express 的核心能力边界 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Express 负责: │ │
│ │ • 路由匹配 —— URL + HTTP Method → Handler │ │
│ │ • 中间件调度 —— 洋葱模型串联 req → res 流程 │ │
│ │ • req/res 增强 —— 在原生对象上添加便捷方法 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Express 不负责: │ │
│ │ • 数据验证(交给 Zod / Joi) │ │
│ │ • 身份认证(交给 Passport / 自定义中间件) │ │
│ │ • 数据库 ORM(交给 Prisma / Drizzle) │ │
│ │ • 文件上传(交给 multer / busboy) │ │
│ │ • 模板渲染(交给 EJS / Pug,Express 只提供胶水) │ │
│ │ • 日志、限流、队列、缓存……(全部交给中间件) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1.2 Express vs Koa vs Fastify 定位对比
┌─────────────────────────────────────────────────────────────┐
│ Node.js HTTP 框架光谱 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 轻量/灵活 ◄────────────────────────────► 功能/约束 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ http │ │ Koa │ │ Express │ │ NestJS │ │
│ │ module │ │ │ │ │ │ │ │
│ ├──────────┤ ├──────────┤ ├──────────┤ ├──────────┤ │
│ │ 零抽象 │ │ async │ │ 中间件 │ │ 装饰器 │ │
│ │ 手工一切 │ │ 洋葱模型 │ │ 生态最大 │ │ 依赖注入 │ │
│ │ 无生态 │ │ 生态小 │ │ 3700万 │ │ 全栈框架 │ │
│ │ │ │ 需要组装 │ │ 周下载 │ │ 企业级 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Fastify │ │
│ ├──────────────────────────────────────────────────┤ │
│ │ • 性能优先(比 Express 快 2-3 倍) │ │
│ │ • 内置 Schema 验证(JSON Schema) │ │
│ │ • 插件系统(封装性好,类似 Koa 的 ctx) │ │
│ │ • 适合:高性能 API、微服务、Serverless │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ 选型建议: │
│ • Express → 生态优先、团队熟悉、快速原型、存量项目 │
│ • Fastify → 性能优先、新项目、需要内置验证和类型安全 │
│ • Koa → 需要极致灵活、自行组装中间件层 │
│ • NestJS → 大型企业应用、需要 OOP 架构和依赖注入 │
│ │
└─────────────────────────────────────────────────────────────┘1.3 最小示例:Express 的 Hello World
import express, { Request, Response } from "express";
const app = express();
app.get("/", (req: Request, res: Response) => {
res.json({ message: "Hello World" });
});
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});这 7 行代码背后隐藏着 Express 的全部核心机制——路由注册、中间件执行、请求处理、响应发送。接下来我们将逐层拆解。
第2部分:洋葱模型深度图解
2.1 什么是洋葱模型?
洋葱模型是 Express 中间件执行顺序的形象描述:请求从外层中间件进入,逐层穿透到达核心处理逻辑,响应再逐层穿出。
┌─────────────────────────────────────────────────────────────┐
│ 洋葱模型执行顺序 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 请求进来 响应出去 │
│ │ ▲ │
│ ▼ │ │
│ ┌──────────────────────────────────────────┐ │
│ │ middleware1 前置逻辑 │ │
│ │ ┌────────────────────────────────────┐ │ │
│ │ │ middleware2 前置逻辑 │ │ │
│ │ │ ┌──────────────────────────────┐ │ │ │
│ │ │ │ middleware3 前置逻辑 │ │ │ │
│ │ │ │ ┌────────────────────────┐ │ │ │ │
│ │ │ │ │ 核心 Handler │ │ │ │ │
│ │ │ │ │ (route handler) │ │ │ │ │
│ │ │ │ └────────────────────────┘ │ │ │ │
│ │ │ │ middleware3 后置逻辑 │ │ │ │
│ │ │ └──────────────────────────────┘ │ │ │
│ │ │ middleware2 后置逻辑 │ │ │
│ │ └────────────────────────────────────┘ │ │
│ │ middleware1 后置逻辑 │ │
│ └──────────────────────────────────────────┘ │
│ │
│ 代码表示: │
│ app.use((req, res, next) => { │
│ console.log("1: 进入 middleware1") // ← 前置 │
│ next() // ← 进入下一层 │
│ console.log("1: 离开 middleware1") // ← 后置 │
│ }) │
│ │
└─────────────────────────────────────────────────────────────┘2.2 next() 的本质
next() 不是"执行下一个中间件",而是**"把控制权交还给 Express 调度器,调度器决定下一个要执行的中间件"**。
// next() 的本质——伪代码还原
function createNext(
currentMiddlewareIndex: number,
middlewareStack: Middleware[],
req: Request,
res: Response
): NextFunction {
return (err?: any) => {
if (err) {
// 跳过普通中间件,直接找到错误处理中间件(4 参数签名)
return processError(err, middlewareStack, req, res);
}
const nextIndex = currentMiddlewareIndex + 1;
if (nextIndex >= middlewareStack.length) {
// 栈结束,Express 内部发送 404 或完成响应
return;
}
const nextMiddleware = middlewareStack[nextIndex];
try {
// 递归调用下一个中间件
nextMiddleware(req, res, createNext(nextIndex, middlewareStack, req, res));
} catch (syncError) {
// Express 5 同时捕获同步和异步错误
processError(syncError, middlewareStack, req, res);
}
};
}2.3 next(err) —— 跳过洋葱层
┌─────────────────────────────────────────────────────────────┐
│ next(err) 错误短路机制 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 正常流程: │
│ mw1 → mw2 → mw3 → handler → mw3后 → mw2后 → mw1后 │
│ │
│ 错误短路(mw2 中 next(err)): │
│ mw1 → mw2 → next(err) │
│ │ │
│ ├── 跳过 mw3 │
│ ├── 跳过 handler │
│ ├── 跳过 mw3后、mw2后、mw1后 │
│ │ │
│ └── 直接进入 errorHandler(4参数中间件) │
│ │
│ 关键规则: │
│ • next(err) 跳过所有后续普通中间件 │
│ • 只执行具有 (err, req, res, next) 签名的错误处理中间件 │
│ • 如果没有任何错误处理器 → Express 内置默认错误处理 │
│ • 错误处理器中调用 next() 无参数 → 回到正常流程 │
│ │
└─────────────────────────────────────────────────────────────┘// 洋葱模型完整演示
import express from "express";
const app = express();
// 中间件 1:最外层
app.use((req, res, next) => {
console.log("1: 进入 mw1");
res.locals.trace = ["mw1-enter"];
next();
console.log("1: 离开 mw1");
});
// 中间件 2:可能产生错误的中间件
app.use((req, res, next) => {
console.log("2: 进入 mw2");
res.locals.trace.push("mw2-enter");
// 模拟:未认证用户触发短路
if (!req.headers.authorization) {
return next(new Error("未认证")); // 跳过后续所有正常中间件
}
next();
console.log("2: 离开 mw2"); // 错误时这行不会执行
});
// 中间件 3:正常情况下会执行的中间件
app.use((req, res, next) => {
console.log("3: 进入 mw3");
res.locals.trace.push("mw3-enter");
next();
console.log("3: 离开 mw3");
});
// 路由处理器
app.get("/", (req, res) => {
res.json({ trace: res.locals.trace });
});
// 错误处理中间件(4 参数签名)
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error("错误:", err.message);
res.status(401).json({
error: err.message,
trace: res.locals.trace, // 只包含 mw1-enter, mw2-enter
});
});
app.listen(3000);第3部分:请求/响应生命周期
3.1 req / res 对象的增强过程
┌─────────────────────────────────────────────────────────────┐
│ req / res 对象的层层增强 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 第0层:node:http 原生对象 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ req: http.IncomingMessage │ │
│ │ • req.url, req.method, req.headers (只读流) │ │
│ │ res: http.ServerResponse │ │
│ │ • res.writeHead(), res.end() (底层写流) │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ 第1层:Express 内置增强 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ req 新增: │ │
│ │ • req.params —— 路由参数 {id: "123"} │ │
│ │ • req.query —— 查询字符串 {page: "1"} │ │
│ │ • req.path —— 路径 "/users/123" │ │
│ │ • req.baseUrl —— 挂载前缀 "/api" │ │
│ │ • req.route —— 当前匹配的路由对象 │ │
│ │ • req.ip —— 客户端 IP(支持 trust proxy) │ │
│ │ • req.get() —— 获取请求头(大小写不敏感) │ │
│ │ • req.xhr —— 是否 AJAX 请求 │ │
│ │ res 新增: │ │
│ │ • res.json() —— JSON 响应 + 自动设置头 │ │
│ │ • res.send() —— 智能响应(自动推断 Content-Type)│ │
│ │ • res.status() —— 设置状态码(链式调用) │ │
│ │ • res.redirect()—— 重定向 │ │
│ │ • res.download()—— 文件下载 │ │
│ │ • res.format() —— 内容协商 │ │
│ │ • res.locals —— 请求级变量容器 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ 第2层:中间件注入的属性 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ express.json() → req.body (已解析的 JSON) │ │
│ │ cookie-parser → req.cookies, req.signedCookies│ │
│ │ multer → req.file, req.files │ │
│ │ express-session → req.session │ │
│ │ passport → req.user, req.isAuthenticated()│ │
│ │ 自定义中间件 → req.requestId, req.tenant │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘3.2 完整生命周期:从 app.listen 到 res.json
// 1. app.listen(3000) 内部做了什么
// → 调用 http.createServer(app) —— app 本身就是一个 requestListener
// → server.listen(3000) —— 监听 TCP 端口
// → 当请求到达时,app(req, res) 被调用
// 2. app(req, res) 内部流程
// → 创建内部 next 函数(闭包捕获 middleware stack 和当前 index)
// → 从第 0 个中间件开始执行
// → 洋葱模型逐层穿透
// 3. 路由匹配
// → 按注册顺序遍历路由
// → URL 匹配 + HTTP Method 匹配
// → 找到 handler,执行
// 4. res.json({ data: "hello" })
// → 内部调用 JSON.stringify()
// → 设置 Content-Type: application/json; charset=utf-8
// → 设置 Content-Length
// → 调用底层 res.end(jsonString)
// → 响应发送到客户端第4部分:路由系统
4.1 三个层级:app.use vs app.METHOD vs Router
┌─────────────────────────────────────────────────────────────┐
│ Express 路由的三个层级 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Level 1: app.use(path, ...middlewares) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 前缀匹配:/api 匹配 /api/users, /api/posts, ... │ │
│ │ • 忽略 HTTP Method(所有方法都进入) │ │
│ │ • 通常用于:全局中间件、子路由挂载、静态文件 │ │
│ │ app.use("/api", apiRouter) │ │
│ │ app.use(express.static("public")) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Level 2: app.METHOD(path, ...handlers) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 精确匹配:/users/:id 只匹配 /users/123 │ │
│ │ • 限定 HTTP Method:get/post/put/delete/patch │ │
│ │ • 通常用于:具体的 API 端点 │ │
│ │ app.get("/users/:id", getUser) │ │
│ │ app.post("/users", createUser) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Level 3: Router() —— 微型 Express 应用 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 独立的中间件栈和路由表 │ │
│ │ • 可以挂载到 app 的任意路径前缀下 │ │
│ │ • 支持自身的 .use() 和 .METHOD() │ │
│ │ • 通常用于:按领域拆分路由模块 │ │
│ │ const userRouter = Router() │ │
│ │ userRouter.get("/:id", getUser) │ │
│ │ userRouter.post("/", createUser) │ │
│ │ app.use("/api/users", userRouter) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘4.2 路由匹配算法
Express 使用 path-to-regexp 库将路由字符串编译为正则表达式。匹配时按注册顺序线性遍历,找到第一个匹配的路由即执行。
// 路由字符串 → 正则表达式(path-to-regexp 内部机制)
// "/users/:id" → /^\/users\/(?:([^\/]+?))\/?$/i → req.params = { id: "123" }
// "/posts/:postId?" → /^\/posts(?:\/([^\/]+?))?\/?$/i → id 可选
// "/files/*" → /^\/files\/(.*)\/?$/i → 通配符
// "/api/v:version" → /^\/api\/v(?:([^\/]+?))\/?$/i → 数字、字母均可
// 示例
import { Router } from "express";
const userRouter = Router();
// 精确匹配
userRouter.get("/profile", getProfile);
// 路径参数
userRouter.get("/:id", getUserById); // GET /users/123 → req.params.id = "123"
// 可选参数
userRouter.get("/:id/posts/:postId?", getUserPosts); // postId 可选
// 正则约束
userRouter.get("/:id(\\d+)", getUserById); // id 必须是数字
// 通配符
userRouter.all("*", notFoundHandler); // 兜底 404
app.use("/users", userRouter);4.3 app.route() 链式写法
// 同一个路径、不同方法的链式定义
app.route("/books/:id")
.all(authenticate) // 所有方法都先鉴权
.get(getBook) // GET /books/123
.put(updateBook) // PUT /books/123
.delete(deleteBook) // DELETE /books/123
.patch(partialUpdateBook); // PATCH /books/123
// 等价于传统写法,但更紧凑,意图更清晰第5部分:常用中间件栈详解
5.1 生产级中间件配置全景
┌─────────────────────────────────────────────────────────────┐
│ 生产环境 Express 中间件栈 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 请求进入 │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ ① helmet → 设置安全 HTTP 头 │ │
│ │ ② cors → 跨域资源共享控制 │ │
│ │ ③ compression → gzip/brotli 压缩响应体 │ │
│ │ ④ morgan → HTTP 访问日志 │ │
│ │ ⑤ rate-limit → 请求频率限制 │ │
│ │ ⑥ express.json → 解析 JSON 请求体 │ │
│ │ ⑦ express.urlencoded → 解析表单请求体 │ │
│ │ ⑧ 自定义中间件 → 认证、日志、验证 │ │
│ │ ⑨ 路由处理 → 业务逻辑 │ │
│ │ ⑩ 错误处理 → 全局错误兜底 │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 响应返回 │
│ │
│ 顺序原则: │
│ • 安全类中间件最先(helmet, cors) │
│ • 日志类尽早(morgan),确保记录所有请求 │
│ • 压缩类在响应前即可(实际靠 res 事件,位置影响小) │
│ • 解析类在路由之前(express.json 等) │
│ • 限流类在解析之后(可判断 body 大小做分层限流) │
│ │
└─────────────────────────────────────────────────────────────┘5.2 cors —— 跨域配置策略
import cors from "cors";
// 开发环境:宽松配置
app.use(cors({
origin: "http://localhost:5173", // Vite 默认端口
credentials: true, // 允许携带 Cookie
}));
// 生产环境:白名单策略
const allowedOrigins = [
"https://myapp.com",
"https://admin.myapp.com",
];
app.use(cors({
origin: (origin, callback) => {
// 非浏览器请求(如 curl、Postman)origin 为 undefined
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error("不允许的跨域来源"));
}
},
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
allowedHeaders: ["Content-Type", "Authorization", "X-Request-Id"],
exposedHeaders: ["X-Request-Id", "X-RateLimit-Remaining"],
maxAge: 86400, // 预检请求缓存 24 小时
credentials: true,
}));5.3 helmet —— 安全头设置
import helmet from "helmet";
// 基础用法(设置一组默认安全头)
app.use(helmet());
// 等价于手动设置以下头部:
// X-DNS-Prefetch-Control: off
// X-Frame-Options: SAMEORIGIN ← 防止点击劫持
// Strict-Transport-Security: max-age=...← 强制 HTTPS
// X-Download-Options: noopen ← IE 下载安全
// X-Content-Type-Options: nosniff ← 防止 MIME 嗅探
// X-Permitted-Cross-Domain-Policies: none
// Referrer-Policy: no-referrer
// X-XSS-Protection: 0 ← 禁用旧版 XSS 过滤器
// 针对现代应用的精调
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // SPA 可能需要
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
imgSrc: ["'self'", "https://cdn.myapp.com", "data:"],
connectSrc: ["'self'", "https://api.myapp.com"],
},
},
crossOriginEmbedderPolicy: false, // 如果使用跨域资源
crossOriginResourcePolicy: { policy: "cross-origin" },
}));5.4 compression —— gzip/brotli 压缩
import compression from "compression";
// 基础配置
app.use(compression({
// 只压缩大于 1KB 的响应(太小压缩反而增大体积)
threshold: 1024, // 字节
// 压缩级别:0(不压缩)~ 9(最高压缩比,最慢)
level: 6, // 生产环境推荐的平衡点
// 对哪些 Content-Type 启用压缩
filter: (req, res) => {
if (req.headers["x-no-compression"]) {
return false; // 允许客户端跳过压缩
}
// 使用默认过滤(压缩 text/*, application/json 等)
return compression.filter(req, res);
},
}));
// 注意:Nginx / CDN 通常已经处理压缩
// 如果 Express 前置了反向代理,建议将压缩委托给反向代理5.5 morgan —— 访问日志
import morgan from "morgan";
// 开发环境:彩色、详细信息
app.use(morgan("dev"));
// 输出:GET /api/users 200 12.345 ms - 1024
// 生产环境:combined 格式(Apache 标准)
app.use(morgan("combined"));
// 输出:127.0.0.1 - - [28/Jul/2026:10:30:00 +0000] "GET / HTTP/1.1" 200 2326
// 自定义 token
morgan.token("request-id", (req: any) => req.requestId || "-");
morgan.token("user-id", (req: any) => req.user?.id || "anonymous");
app.use(morgan(
':request-id :remote-addr - :user-id [:date[iso]] ":method :url HTTP/:http-version" :status :res[content-length] - :response-time ms'
));
// 输出:req_abc123 192.168.1.1 - user_456 [2026-07-28T10:30:00+0000] "GET /api/users HTTP/1.1" 200 1024 - 12.345 ms5.6 express-rate-limit —— 限流
import rateLimit from "express-rate-limit";
// 全局限流
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟窗口
max: 100, // 每个 IP 最多 100 次请求
standardHeaders: true, // 返回 RateLimit-* 头
legacyHeaders: false, // 禁用 X-RateLimit-* 头
message: {
error: "请求过于频繁,请稍后再试",
retryAfter: "请等待 15 分钟",
},
// 根据请求特征生成 key(默认使用 IP)
keyGenerator: (req) => {
// 如果有认证用户,按用户限流更精细
return req.user?.id ?? req.ip ?? "unknown";
},
// 跳过某些请求(如健康检查)
skip: (req) => req.path === "/health",
});
app.use("/api", globalLimiter);
// 敏感端点专用限流(登录接口)
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10, // 登录接口 15 分钟内只能尝试 10 次
skipSuccessfulRequests: true, // 登录成功后不计数
});
app.use("/api/auth/login", authLimiter);5.7 Body 解析中间件
// JSON 请求体解析
app.use(express.json({
limit: "1mb", // 防止大 payload 攻击
strict: true, // 只接受数组和对象,拒绝裸字符串/数字
verify: (req, _res, buf) => {
// 可在此验证原始 body 签名(如 Stripe webhook)
(req as any).rawBody = buf.toString();
},
}));
// 表单数据解析(application/x-www-form-urlencoded)
app.use(express.urlencoded({
extended: true, // 使用 qs 库,支持嵌套对象
limit: "1mb",
}));
// 注意:express.raw() 和 express.text() 也可用
// 但 multipart/form-data 需要 multer(Express 不内置处理)第6部分:自定义中间件设计模式
6.1 认证中间件(JWT 验证)
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
// 扩展 Request 类型(声明合并)
declare global {
namespace Express {
interface Request {
user?: { id: string; role: string };
}
}
}
export function authenticate(req: Request, res: Response, next: NextFunction): void {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
res.status(401).json({ error: "缺少认证令牌" });
return;
}
const token = authHeader.slice(7); // 去掉 "Bearer " 前缀
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as { sub: string; role: string };
req.user = { id: payload.sub, role: payload.role };
next();
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
res.status(401).json({ error: "令牌已过期", code: "TOKEN_EXPIRED" });
return;
}
res.status(401).json({ error: "无效的认证令牌" });
}
}6.2 请求日志中间件(requestId 注入)
import { randomUUID } from "crypto";
import { Request, Response, NextFunction } from "express";
import { AsyncLocalStorage } from "async_hooks";
// 使用 AsyncLocalStorage 实现请求级上下文
export const requestContext = new AsyncLocalStorage<{ requestId: string }>();
export function requestIdMiddleware(req: Request, _res: Response, next: NextFunction): void {
// 优先使用传入的 requestId(服务间传递)
const requestId = (req.headers["x-request-id"] as string) || randomUUID();
// 注入到 req 对象(供后续中间件使用)
(req as any).requestId = requestId;
// 注入到响应头(供客户端追踪)
_res.setHeader("X-Request-Id", requestId);
// 注入到 AsyncLocalStorage(使同步流和 microtask 都能访问)
requestContext.run({ requestId }, () => {
next();
});
}6.3 参数验证中间件(Zod 集成)
import { Request, Response, NextFunction } from "express";
import { z, ZodSchema } from "zod";
// 工厂函数模式:返回可配置的验证中间件
export function validate(schemas: {
body?: ZodSchema;
params?: ZodSchema;
query?: ZodSchema;
}) {
return (req: Request, res: Response, next: NextFunction): void => {
try {
if (schemas.body) {
req.body = schemas.body.parse(req.body);
}
if (schemas.params) {
req.params = schemas.params.parse(req.params) as any;
}
if (schemas.query) {
// query 的值都是 string,需要特殊处理
req.query = schemas.query.parse(req.query) as any;
}
next();
} catch (err) {
if (err instanceof z.ZodError) {
res.status(400).json({
error: "请求参数验证失败",
details: err.errors.map((e) => ({
path: e.path.join("."),
message: e.message,
})),
});
return;
}
next(err);
}
};
}
// 使用示例
const createUserSchema = z.object({
body: z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional(),
}),
params: z.object({
tenantId: z.string().uuid(),
}),
});
app.post("/api/tenants/:tenantId/users", validate(createUserSchema), createUser);6.4 错误处理中间件(4 参数签名)
import { Request, Response, NextFunction } from "express";
import { ZodError } from "zod";
// 自定义错误类
export class AppError extends Error {
constructor(
public statusCode: number,
public code: string,
message: string,
public details?: unknown
) {
super(message);
this.name = "AppError";
// 确保 instanceof 在跨模块场景下正常工作
Object.setPrototypeOf(this, AppError.prototype);
}
}
// 全局错误处理中间件
export function globalErrorHandler(
err: Error,
_req: Request,
res: Response,
_next: NextFunction
): void {
// 已知的业务错误
if (err instanceof AppError) {
res.status(err.statusCode).json({
error: err.message,
code: err.code,
details: err.details,
});
return;
}
// Zod 验证错误
if (err instanceof ZodError) {
res.status(400).json({
error: "请求参数验证失败",
code: "VALIDATION_ERROR",
details: err.errors,
});
return;
}
// JSON 解析错误(来自 express.json())
if (err.type === "entity.parse.failed") {
res.status(400).json({
error: "请求体格式错误,期望合法的 JSON",
code: "INVALID_JSON",
});
return;
}
// 未知错误:记录完整信息,返回泛化消息
console.error("未处理的错误:", {
message: err.message,
stack: err.stack,
requestId: (res as any).requestId,
});
res.status(500).json({
error: "服务器内部错误",
code: "INTERNAL_ERROR",
});
}6.5 工厂函数模式——可配置的中间件
工厂函数是 Express 中最常见的中间件设计模式:外层函数接收配置,返回符合 (req, res, next) 签名的中间件函数。
┌─────────────────────────────────────────────────────────────┐
│ 中间件工厂函数模式 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 配置参数 │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ function factory(options) { │ │
│ │ // 闭包:options 在中间件生命周期内持久存在 │ │
│ │ return function middleware(req, res, next) { │ │
│ │ // 每次请求执行的逻辑 │ │
│ │ } │ │
│ │ } │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ 典型应用: │
│ • cors({ origin: "..." }) → 跨域策略可配 │
│ • rateLimit({ windowMs, max }) → 限流参数可配 │
│ • validate({ body: schema }) → 不同 schema 可配 │
│ • authorize("admin") → 角色可配 │
│ • cache({ ttl: 60 }) → 缓存时长可配 │
│ │
└─────────────────────────────────────────────────────────────┘// 实战:基于角色的授权中间件工厂
export function requireRole(...allowedRoles: string[]) {
return (req: Request, res: Response, next: NextFunction): void => {
if (!req.user) {
res.status(401).json({ error: "未认证" });
return;
}
if (!allowedRoles.includes(req.user.role)) {
res.status(403).json({ error: "权限不足", required: allowedRoles });
return;
}
next();
};
}
// 使用
app.delete("/api/users/:id", authenticate, requireRole("admin"), deleteUser);
app.patch("/api/articles/:id", authenticate, requireRole("admin", "editor"), updateArticle);第7部分:错误处理链
7.1 同步错误 vs 异步错误
┌─────────────────────────────────────────────────────────────┐
│ Express 4 vs Express 5 错误捕获对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Express 4: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ throw new Error("sync") → Express 自动捕获 ✓ │ │
│ │ Promise.reject("async") → Express 不捕获 ✗ │ │
│ │ async (req, res, next) => { │ │
│ │ throw new Error("...") → Express 不捕获 ✗ │ │
│ │ } │ │
│ │ → 进程崩溃(unhandledRejection)! │ │
│ │ → 必须手动 try/catch + next(err) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Express 5: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ throw new Error("sync") → Express 自动捕获 ✓ │ │
│ │ Promise.reject("async") → Express 自动捕获 ✓ │ │
│ │ async (req, res, next) => { │ │
│ │ throw new Error("...") → Express 自动捕获 ✓ │ │
│ │ } │ │
│ │ → 所有 rejections 自动传递给 next(err) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Express 4 的安全写法(向后兼容): │
│ const asyncHandler = (fn: RequestHandler) => │
│ (req, res, next) => Promise.resolve(fn(req, res, next)) │
│ .catch(next); │
│ │
└─────────────────────────────────────────────────────────────┘7.2 Express 4 的 asyncHandler 包装器
import { Request, Response, NextFunction, RequestHandler } from "express";
// 将 async 路由处理器包装为安全的 Express 中间件
export function asyncHandler(fn: RequestHandler): RequestHandler {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// 使用
app.get("/api/users/:id", asyncHandler(async (req, res) => {
const user = await db.user.findUnique({ where: { id: req.params.id } });
if (!user) {
throw new AppError(404, "NOT_FOUND", "用户不存在"); // 会被 asyncHandler 捕获
}
res.json(user);
}));7.3 错误响应标准化
// 统一错误响应格式
interface ErrorResponse {
error: string; // 人类可读的错误描述
code: string; // 机器可读的错误代码
details?: unknown; // 可选的详细错误信息(验证失败字段等)
requestId?: string; // 用于关联日志
}
// 全局错误处理器输出标准格式
export function globalErrorHandler(
err: Error & { statusCode?: number; code?: string; type?: string; details?: unknown },
req: Request,
res: Response,
_next: NextFunction
): void {
const response: ErrorResponse = {
error: "服务器内部错误",
code: "INTERNAL_ERROR",
requestId: (req as any).requestId,
};
if (err instanceof AppError) {
response.error = err.message;
response.code = err.code;
response.details = err.details;
res.status(err.statusCode).json(response);
return;
}
// 记录未知错误的完整堆栈
console.error(`[${response.requestId}] 未处理错误:`, err);
res.status(500).json(response);
}第8部分:Express 5 新特性
8.1 Express 5 主要变化
┌─────────────────────────────────────────────────────────────┐
│ Express 4 → Express 5 核心变化 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ✅ 自动捕获 async 错误: │
│ async 中间件中抛出的错误自动转到 next(err),无需包装 │
│ │
│ ✅ 路径匹配语法更新: │
│ • 废弃正则表达式路径(app.get(/regex/, handler) 不再支持) │
│ • 路径参数支持更多修饰符:/:id(\\d+), /:name*, /:file? │
│ • 使用 path-to-regexp 0.x(语法有变化) │
│ │
│ ✅ 废弃的方法和属性: │
│ • req.query 不再可写(Express 4 中可手动设值) │
│ • req.param() 被废弃(用 req.params / req.body / req.query │
│ • res.send(status, body) 签名被废弃(用 res.status().send() │
│ • app.del() 被废弃(用 app.delete()) │
│ • app.param() 回调被废弃 │
│ │
│ ✅ 更好的 Promise 支持: │
│ • app.param() 处理器支持返回 Promise │
│ • 模板引擎 render() 支持返回 Promise │
│ │
│ ⚠️ 迁移注意事项: │
│ • 检查所有路由中的正则表达式用法 │
│ • 检查 app.param(fn) 回调 │
│ • 检查 res.send(status, body) 调用 │
│ │
└─────────────────────────────────────────────────────────────┘// Express 5 路径匹配新语法
const router = Router();
// 命名参数 + 正则约束(与 Express 4 一致,但底层引擎升级)
router.get("/users/:id(\\d+)", handler);
// 零个或多个(*)
router.get("/files/:path*", handler); // /files, /files/a, /files/a/b/c
// 一个或多个(+)
router.get("/sections/:name+", handler); // /sections/a, /sections/a/b
// 可选参数(?)
router.get("/posts/:slug?", handler); // /posts, /posts/hello-world第9部分:Express 性能调优
9.1 中间件顺序优化
┌─────────────────────────────────────────────────────────────┐
│ 中间件顺序对性能的影响 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 黄金法则: │
│ 1. 尽早短路不必要的请求 │
│ 2. 让开销大的中间件处理尽可能少的请求 │
│ 3. 静态文件放在 API 路由之前或之后 │
│ │
│ 反例(每次 API 请求都经过静态文件查找): │
│ app.use(express.static("public")) // public 目录无匹配 │
│ app.use("/api", apiRouter) // 才到 API 路由 │
│ → 每个 API 请求都对 public 目录做 fs.stat │
│ │
│ 正例(API 请求跳过静态文件中间件): │
│ app.use("/api", apiRouter) // API 请求直接匹配 │
│ app.use(express.static("public")) // 只有非 /api 请求到达 │
│ │
│ 推荐的全局中间件顺序: │
│ ┌──────────────────────────────────────────────────┐ │
│ │① 安全类(helmet, cors) │ │
│ │② 请求日志(morgan, requestId) │ │
│ │③ Body 解析(express.json, urlencoded) │ │
│ │④ 压缩(compression) │ │
│ │⑤ 限流(rateLimit) │ │
│ │⑥ 认证(authenticate, 按需) │ │
│ │⑦ API 路由 │ │
│ │⑧ 静态文件(如果 /api 路由已分开) │ │
│ │⑨ 404 兜底 │ │
│ │⑩ 全局错误处理 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘9.2 生产环境核心配置
import express from "express";
const app = express();
// 1. 信任反向代理(获取真实客户端 IP)
app.set("trust proxy", 1); // 信任第一个代理
// app.set("trust proxy", ["127.0.0.1", "10.0.0.0/8"]); // 信任特定 IP
// 2. 关闭 Express 签名头(减少响应体积)
app.disable("x-powered-by");
// 3. 设置合理的超时
import http from "http";
const server = http.createServer(app);
server.timeout = 30_000; // 30 秒不活动超时
server.keepAliveTimeout = 65_000; // Keep-Alive 超时
server.headersTimeout = 66_000; // 头部解析超时(应 > keepAliveTimeout)
// 4. 优雅关闭
process.on("SIGTERM", () => {
console.log("收到 SIGTERM,开始优雅关闭...");
server.close(() => {
console.log("HTTP 服务器已关闭");
// 关闭数据库连接、消息队列连接等
process.exit(0);
});
// 强制退出兜底
setTimeout(() => {
console.error("优雅关闭超时,强制退出");
process.exit(1);
}, 10_000);
});
// 5. 处理未捕获的异常和 rejection
process.on("uncaughtException", (err) => {
console.error("未捕获异常:", err);
process.exit(1);
});
process.on("unhandledRejection", (reason) => {
console.error("未处理的 Promise rejection:", reason);
// Express 5 已自动处理,此处理仅用于非 Express 代码的兜底
});9.3 常见性能陷阱
// ❌ 反模式1:在中间件中同步执行密集计算
app.use((req, res, next) => {
const result = heavyCryptoOperation(req.body); // 阻塞事件循环!
next();
});
// ✅ 正解:将 CPU 密集操作放到 Worker Thread 或独立服务
app.use(asyncHandler(async (req, res, next) => {
const result = await workerPool.exec("heavyCrypto", [req.body]);
next();
}));
// ❌ 反模式2:不设置 Body Parser 的 limit
app.use(express.json()); // 攻击者可以发送无限大的 JSON,OOM
// ✅ 正解:设定合理的上限
app.use(express.json({ limit: "1mb" }));
// ❌ 反模式3:在同步中间件中 await
app.use((req, res, next) => {
// 忘了把回调转成 await,next 在等待前就已经调用
someAsyncCheck(req).then(() => next());
// 如果 someAsyncCheck 抛出异常 → unhandledRejection
});第10部分:Express + TypeScript 最佳实践
10.1 类型化 Request / Response
import { Request, Response, NextFunction } from "express";
// 1. 为路由参数定义类型
interface UserParams {
id: string;
}
interface UserQuery {
page?: string;
limit?: string;
sort?: "asc" | "desc";
}
interface CreateUserBody {
name: string;
email: string;
age?: number;
}
// 使用泛型约束
app.get<{ id: string }>(
"/api/users/:id",
(req: Request<UserParams>, res: Response) => {
const { id } = req.params; // id 的类型是 string
// ...
}
);
// 更完整的泛型用法
app.post<{}, any, CreateUserBody>(
"/api/users",
(req: Request<{}, any, CreateUserBody>, res: Response) => {
const { name, email } = req.body; // ✅ 类型安全
// ...
}
);10.2 扩展 Request 类型(声明合并)
// types/express.d.ts
import "express";
declare global {
namespace Express {
interface Request {
// 认证后注入的用户信息
user?: {
id: string;
role: "admin" | "user" | "editor";
tenantId: string;
};
// 每个请求的唯一 ID
requestId: string;
// 多租户上下文
tenant?: {
id: string;
name: string;
};
// 请求开始时间(用于计算耗时)
startTime: number;
}
}
}
// 在任何地方使用都有完整的类型提示
app.get("/profile", authenticate, (req, res) => {
req.user; // { id: string; role: "admin" | "user" | "editor"; tenantId: string }
});10.3 Zod 验证 + 类型推导形成端到端类型安全
import { z } from "zod";
// 1. 定义 Zod Schema(既是验证器,又是类型来源)
const createUserSchema = z.object({
name: z.string().min(2, "姓名至少 2 个字符").max(50),
email: z.string().email("邮箱格式不正确"),
role: z.enum(["user", "editor"]).default("user"),
});
const updateUserSchema = createUserSchema.partial(); // 所有字段变为可选
const userIdParamsSchema = z.object({
id: z.string().regex(/^\d+$/, "ID 必须是数字").transform(Number),
});
// 2. 从 Schema 推导 TypeScript 类型(单一真相源)
type CreateUserInput = z.infer<typeof createUserSchema>;
type UpdateUserInput = z.infer<typeof updateUserSchema>;
type UserIdParams = z.infer<typeof userIdParamsSchema>;
// 3. 泛型验证中间件(支持类型推导)
import { ZodSchema, ZodTypeAny } from "zod";
interface ValidationSchemas<
TParams extends ZodTypeAny = ZodTypeAny,
TBody extends ZodTypeAny = ZodTypeAny,
TQuery extends ZodTypeAny = ZodTypeAny,
> {
params?: TParams;
body?: TBody;
query?: TQuery;
}
export function validate<
TParams extends ZodTypeAny,
TBody extends ZodTypeAny,
TQuery extends ZodTypeAny,
>(schemas: ValidationSchemas<TParams, TBody, TQuery>) {
return (req: Request, res: Response, next: NextFunction): void => {
try {
if (schemas.body) req.body = schemas.body.parse(req.body);
if (schemas.params) (req as any).params = schemas.params.parse(req.params);
if (schemas.query) (req as any).query = schemas.query.parse(req.query);
next();
} catch (err) {
next(err);
}
};
}
// 4. 使用:Schema → 验证 → 类型自动匹配
app.post(
"/api/users",
validate({ body: createUserSchema }),
(req, res) => {
// req.body 的类型自动推导为 CreateUserInput
const { name, email, role } = req.body; // ✅ 完全类型安全
res.status(201).json({ name, email, role });
}
);
app.patch(
"/api/users/:id",
validate({ params: userIdParamsSchema, body: updateUserSchema }),
(req, res) => {
// req.params.id 的类型是 number(因为 .transform(Number))
const { id } = req.params; // number!
const updates = req.body; // UpdateUserInput
res.json({ id, updates });
}
);10.4 项目结构推荐
src/
├── index.ts # 入口:创建 app,注册中间件,启动 server
├── app.ts # Express 应用工厂(便于测试)
├── config/
│ └── env.ts # 环境变量验证(Zod 验证 process.env)
├── middleware/
│ ├── authenticate.ts # JWT 认证中间件
│ ├── authorize.ts # 角色授权中间件(工厂函数)
│ ├── requestId.ts # requestId 注入
│ ├── validate.ts # Zod 验证中间件
│ └── errorHandler.ts # 全局错误处理器
├── routes/
│ ├── index.ts # 路由聚合(挂载所有子路由到 /api)
│ ├── users.ts # /api/users 路由
│ ├── articles.ts # /api/articles 路由
│ └── auth.ts # /api/auth 路由
├── services/ # 业务逻辑层(不依赖 req/res)
│ ├── userService.ts
│ └── articleService.ts
├── types/
│ └── express.d.ts # Request 声明合并
└── utils/
├── AppError.ts # 自定义错误类
└── asyncHandler.ts # Express 4 兼容包装核心总结
- 洋葱模型:Express 的核心是中间件调度,
next()将控制权交给下一层,后置逻辑在next()之后执行,形成请求「进→出」的洋葱环。 next(err)短路:传递错误会使 Express 跳过所有普通中间件,直接寻找 4 参数签名的错误处理器。- 三层层级:
app.use(前缀匹配,任意方法)>app.METHOD(精确匹配,特定方法)>Router(独立子应用,支持模块化)。 - 中间件顺序:安全类(helmet/cors)> 日志类(morgan)> 解析类(body parser)> 限流类 > 认证类 > 路由 > 错误处理。顺序决定性能和安全性。
- 工厂函数模式:外层接收配置,内层返回中间件函数,是 Express 中最通用的可复用模式。
- Express 5:自动捕获 async 错误的 Promise rejection,移除了路径正则表达式支持,语法更清晰。
- TypeScript 集成:通过声明合并在
Express.Request上添加属性,Zod Schema 既是验证器又是类型来源,实现端到端类型安全。
章节测试
洋葱模型:在中间件 A (前置) → 中间件 B (前置) → handler → 中间件 B (后置) → 中间件 A (后置) 的执行链条中,若中间件 B 的前置逻辑调用了
next(new Error("fail")),请写出实际执行路径。路由匹配:
app.use("/api", router)和app.get("/api/users", handler)分别属于哪个层级?它们的匹配方式有什么区别?错误处理:Express 4 中为什么
async (req, res) => { throw new Error("fail") }不会触发错误处理中间件?Express 5 如何解决这个问题?中间件设计:请写一个工厂函数中间件,它接收
rateLimitOptions配置,返回一个限流中间件(伪代码即可)。声明合并:如何通过 TypeScript 的声明合并为
Express.Request添加user和requestId属性?性能优化:为什么静态文件中间件
express.static("public")应该放在 API 路由之后?这样做的原理是什么?Body 解析:
express.json({ limit: "1mb" })中的limit参数的作用是什么?为什么不设置它会带来安全风险?
参考答案
执行路径:A 前置 → B 前置 →
next(new Error("fail"))→ 跳过 handler、B 后置、A 后置 → 直接进入第一个 (err, req, res, next) 签名的错误处理器。app.use("/api", router)是 Level 1(前缀匹配),任何以/api开头的请求(不限 HTTP 方法)都会匹配并进入 router。app.get("/api/users", handler)是 Level 2(精确路由),只有GET /api/users精确匹配时才执行,其他方法(POST、PUT 等)不会命中。Express 4 的中间件调度器不会自动捕获 Promise rejection。async 函数抛出错误本质是返回一个 rejected Promise,Express 4 没有
.catch()机制来处理它,因此会变成 unhandledRejection。Express 5 在调度器中添加了对 async 函数返回值的.catch(next)处理,自动将 rejection 转入错误处理链。伪代码:
function createRateLimiter(options: { windowMs: number; max: number }) {
const hits = new Map<string, { count: number; resetAt: number }>();
return (req: Request, res: Response, next: NextFunction) => {
const key = req.ip ?? "unknown";
const now = Date.now();
const record = hits.get(key);
if (!record || now > record.resetAt) {
hits.set(key, { count: 1, resetAt: now + options.windowMs });
return next();
}
if (record.count >= options.max) {
return res.status(429).json({ error: "请求过于频繁" });
}
record.count++;
next();
};
}- 在
types/express.d.ts中:
declare global {
namespace Express {
interface Request {
user?: { id: string; role: string };
requestId: string;
}
}
}静态文件中间件对每个请求都会执行文件系统查找(
fs.stat)。如果放在 API 路由之前,每个 API 请求(如/api/users)都会先经历一次无意义的文件查找,造成不必要的 I/O 开销。放在 API 路由之后,API 请求会先被路由精确匹配并处理,不再进入静态文件中间件。limit限制请求体的最大大小(字节数)。不设置意味着攻击者可以发送任意大小的 JSON,导致服务端内存耗尽(OOM),造成拒绝服务攻击。1mb 是常见的安全上限。
相关笔记
- [[../01-javascript-and-typescript/03-async-and-event-loop]] — Node.js 事件循环与异步模型
- [[00-overview]] — Node.js 与后端框架学习路线
- [[01-environment-configuration]] — 环境配置完全指南
- [[../../05-backend-engineering/11-architecture-patterns]] — 后端架构模式
- [[../../05-backend-engineering/18-observability]] — 可观测性(日志、追踪、指标)
下一步学习
- Express 官方文档:深入了解
app.set()配置项和模板引擎集成 - Fastify 快速入门:对比 Express 的插件系统和 Schema 验证,理解性能差异来源
- NestJS 基础:体验装饰器 + 依赖注入的全栈架构
- 阅读 express 源码(~2000 行):理解
Layer、Route、Router的内部实现 - 尝试徒手实现一个最小 Express(洋葱模型 + 路由匹配)
学习状态:🟡 开始学习