Express 中间件
中间件(Middleware)是 Express 框架的核心机制。它是在请求到达路由处理函数之前或之后执行的函数,可以访问请求对象 (req)、响应对象 (res) 和下一个中间件函数 (next)。
中间件架构
执行流程
code
客户端请求
│
▼
┌─────────────────────────────────────────────────────┐
│ 中间件链 │
├─────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 中间件 1 │───▶│ 中间件 2 │───▶│ 中间件 3 │ │
│ │ │ │ │ │ │ │
│ │ next() │ │ next() │ │ res.send │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────┘
│
▼
响应客户端核心特性
| 特性 | 说明 |
|---|---|
| 可访问性 | 可以访问 req(请求对象)和 res(响应对象) |
| 可修改性 | 可以修改 req 和 res 对象的属性和方法 |
| 可控性 | 可以通过 next() 控制是否继续执行后续中间件 |
| 可终止性 | 可以直接发送响应结束请求-响应周期 |
中间件函数签名
javascript
function middleware(req, res, next) {
// req: 请求对象
// res: 响应对象
// next: 调用后传递给下一个中间件
// 执行逻辑...
next() // 必须调用,否则请求会挂起
}中间件分类
| 类型 | 绑定对象 | 作用范围 | 典型用途 |
|---|---|---|---|
| 应用级中间件 | app | 全局所有请求 | 日志、认证、解析 |
| 路由级中间件 | router | 特定路由组 | 路由特定验证 |
| 错误处理中间件 | app/router | 错误捕获 | 统一错误处理 |
| 内置中间件 | express | 按需使用 | JSON 解析、静态文件 |
| 第三方中间件 | 社区 | 按需使用 | CORS、压缩、安全 |
应用级中间件
绑定到 app 实例上,对所有请求生效。
全局中间件
javascript
const express = require("express")
const app = express()
// 所有请求都会执行
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`)
next()
})
// 访问任何路径都会打印日志
// GET / → 打印日志
// GET /users → 打印日志
// POST /api/data → 打印日志路径匹配中间件
javascript
// 只匹配 /admin 开头的路径
app.use("/admin", (req, res, next) => {
console.log("访问管理后台")
next()
})
// 只匹配 /api 开头的路径
app.use("/api", (req, res, next) => {
res.setHeader("X-API-Version", "1.0")
next()
})多个中间件串联
javascript
// 多个中间件按顺序执行
app.use(
(req, res, next) => {
console.log("中间件 1")
next()
},
(req, res, next) => {
console.log("中间件 2")
next()
},
(req, res, next) => {
console.log("中间件 3")
next()
}
)中间件终止请求
javascript
// 认证中间件可以终止请求
app.use("/admin", (req, res, next) => {
const token = req.headers.authorization
if (!token) {
// 直接返回响应,不调用 next()
return res.status(401).json({ error: "未授权" })
}
// 验证通过,继续执行
req.user = verifyToken(token)
next()
})路由级中间件
绑定到 express.Router() 实例,只对特定路由组生效。
基本用法
javascript
const express = require("express")
const app = express()
const router = express.Router()
// 路由级中间件
router.use((req, res, next) => {
console.log(`访问用户模块: ${Date.now()}`)
next()
})
// 路由定义
router.get("/", (req, res) => {
res.json({ users: [] })
})
router.get("/:id", (req, res) => {
res.json({ userId: req.params.id })
})
// 挂载到应用
app.use("/users", router)
// 只有 /users/* 的请求才会执行路由级中间件模块化认证
javascript
// routes/admin.js
const express = require("express")
const router = express.Router()
// 所有 admin 路由都需要认证
router.use((req, res, next) => {
if (!req.session.isAdmin) {
return res.status(403).json({ error: "需要管理员权限" })
}
next()
})
router.get("/dashboard", (req, res) => {
res.send("管理后台")
})
router.get("/users", (req, res) => {
res.json({ users: [] })
})
module.exports = router错误处理中间件
错误处理中间件有四个参数 (err, req, res, next),专门用于捕获和处理错误。
基本用法
javascript
// 必须放在所有中间件和路由之后
app.use((err, req, res, next) => {
console.error(err.stack)
res.status(500).json({ error: "服务器内部错误" })
})区分错误类型
javascript
app.use((err, req, res, next) => {
// 验证错误
if (err.name === "ValidationError") {
return res.status(400).json({
error: "数据验证失败",
details: err.message
})
}
// JWT 错误
if (err.name === "JsonWebTokenError") {
return res.status(401).json({ error: "无效的令牌" })
}
// 自定义错误
if (err.status) {
return res.status(err.status).json({ error: err.message })
}
// 未知错误
console.error(err)
res.status(500).json({ error: "服务器内部错误" })
})更多错误处理内容:自定义错误类、异步错误捕获、错误日志等,请参阅 错误处理 章节。
内置中间件
Express 4.x 提供了三个内置中间件。
express.json - JSON 解析
解析 Content-Type: application/json 的请求体。
javascript
app.use(express.json())
// 配置选项
app.use(express.json({
limit: "1mb", // 请求体最大限制
strict: true, // 只接受数组和对象
type: "application/json" // 指定 MIME 类型
}))
app.post("/users", (req, res) => {
console.log(req.body) // 已解析的 JSON 对象
res.json(req.body)
})配置参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
limit | String/Number | '100kb' | 请求体最大大小 |
strict | Boolean | true | 只接受数组和对象 |
type | String/Function | 'json' | 解析的 MIME 类型 |
inflate | Boolean | true | 是否处理压缩的请求体 |
reviver | Function | null | JSON.parse 的 reviver 函数 |
express.urlencoded - 表单解析
解析 Content-Type: application/x-www-form-urlencoded 的请求体。
javascript
app.use(express.urlencoded({ extended: true }))
// 配置选项
app.use(express.urlencoded({
extended: true, // 使用 qs 库解析嵌套对象
limit: "1mb", // 请求体最大限制
parameterLimit: 1000 // 参数数量限制
}))
app.post("/login", (req, res) => {
const { username, password } = req.body
res.json({ username })
})extended 参数对比:
| 值 | 解析库 | 特点 | 适用场景 |
|---|---|---|---|
true | qs | 支持嵌套对象 user[name]=John | 复杂表单 |
false | querystring | 不支持嵌套,性能更好 | 简单表单 |
express.static - 静态文件
托管静态资源文件。
javascript
// 基本用法
app.use(express.static("public"))
// 带路径前缀
app.use("/static", express.static("public"))
// 完整配置
app.use(express.static("public", {
dotfiles: "ignore", // 忽略点文件
etag: true, // 启用 ETag
extensions: ["html"], // 默认扩展名
fallthrough: true, // 让后续中间件处理 404
immutable: true, // Cache-Control immutable
index: "index.html", // 默认首页
lastModified: true, // 启用 Last-Modified
maxAge: "1d", // 缓存时间
redirect: true, // 目录重定向
setHeaders: (res, path) => {
// 自定义响应头
if (path.endsWith(".html")) {
res.setHeader("Cache-Control", "no-cache")
}
}
}))配置参数详解:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
dotfiles | String | 'ignore' | 点文件处理:ignore、allow、deny |
etag | Boolean | true | 生成 ETag |
extensions | Array | [] | 文件扩展名回退 |
fallthrough | Boolean | true | 404 是否传递给下一个中间件 |
immutable | Boolean | false | 添加 immutable 到 Cache-Control |
index | String/Boolean | 'index.html' | 默认首页文件 |
lastModified | Boolean | true | 设置 Last-Modified 头 |
maxAge | Number/String | 0 | Cache-Control max-age |
redirect | Boolean | true | 目录路径重定向到尾部带 / |
setHeaders | Function | - | 自定义响应头函数 |
静态文件访问示例:
javascript
// 目录结构
// public/
// ├── images/
// │ └── logo.png
// ├── css/
// │ └── style.css
// └── index.html
app.use(express.static("public"))
// 访问方式:
// http://localhost:3000/ → index.html
// http://localhost:3000/images/logo.png
// http://localhost:3000/css/style.cssPost 数据类型与请求体解析(深入)
Express 的请求体解析本质上是按 Content-Type 选择对应的内置/第三方解析器。下表汇总常见的 Post 数据类型与对应的解析方式:
| Content-Type | 解析器 | req.body 结果 | 典型场景 |
|---|---|---|---|
application/json | express.json() | JS 对象 | 前后端分离接口 |
application/x-www-form-urlencoded | express.urlencoded() | 对象(或嵌套对象) | 传统表单提交 |
text/plain | express.text() | 字符串 | 纯文本上报 |
application/octet-stream | express.raw() | Buffer | 二进制流、签名校验 |
multipart/form-data | multer | req.body + req.file(s) | 文件上传 |
raw / text 解析(内置中间件,常被忽略):
javascript
// 解析纯文本
app.use(express.text({ type: "text/plain" }))
app.post("/log", (req, res) => {
console.log(typeof req.body) // "string"
res.send("ok")
})
// 解析二进制(如微信支付回调的原始报文)
app.use(express.raw({ type: "application/octet-stream" }))
app.post("/binary", (req, res) => {
console.log(req.body) // Buffer
res.send("ok")
})multipart 文件类型校验:通过 file.mimetype 与扩展名双重校验(详见上文「第三方中间件 - multer」)。
req.body 为空的常见排查:
- 未挂载对应解析中间件(
express.json()/express.urlencoded())。 - 客户端
Content-Type与解析器不匹配(如发了 JSON 但没带application/json头)。 - 请求体超限被丢弃(
limit配置过小)。 - 使用了
multipart/form-data却用express.json()解析(必须用multer)。 - 中间件顺序错误:解析器必须在路由处理函数之前注册。
小结:请求体解析是「按 Content-Type 匹配解析器」的管道,选对中间件并放在正确顺序是核心。
第三方中间件
常用中间件概览
| 中间件 | 用途 | npm 周下载量 | 推荐度 |
|---|---|---|---|
cors | 跨域资源共享 | 高 | ⭐⭐⭐⭐⭐ |
helmet | 安全 HTTP 头 | 高 | ⭐⭐⭐⭐⭐ |
morgan | 请求日志 | 高 | ⭐⭐⭐⭐ |
compression | Gzip 压缩 | 高 | ⭐⭐⭐⭐⭐ |
cookie-parser | Cookie 解析 | 高 | ⭐⭐⭐⭐ |
express-session | 会话管理 | 高 | ⭐⭐⭐⭐ |
multer | 文件上传 | 高 | ⭐⭐⭐⭐⭐ |
express-validator | 数据验证 | 高 | ⭐⭐⭐⭐⭐ |
express-rate-limit | 请求限流 | 高 | ⭐⭐⭐⭐⭐ |
helmet - 安全 HTTP 头
bash
npm install helmetjavascript
const helmet = require("helmet")
// 启用所有安全头
app.use(helmet())
// 选择性启用
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true
},
noSniff: true,
xssFilter: true,
referrerPolicy: { policy: "strict-origin-when-cross-origin" }
}))helmet 设置的安全头:
| HTTP 头 | 作用 |
|---|---|
Content-Security-Policy | 防止 XSS 攻击 |
X-Frame-Options | 防止点击劫持 |
X-Content-Type-Options | 防止 MIME 类型嗅探 |
Strict-Transport-Security | 强制 HTTPS |
X-XSS-Protection | XSS 过滤器 |
Referrer-Policy | 控制 Referrer 信息 |
cors - 跨域资源共享
bash
npm install corsjavascript
const cors = require("cors")
// 允许所有跨域
app.use(cors())
// 基本配置
app.use(cors({
origin: "https://example.com",
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
optionsSuccessStatus: 204
}))
// 动态白名单
const whitelist = ["http://localhost:3000", "https://example.com"]
app.use(cors({
origin: (origin, callback) => {
// 允许无 origin 的请求(如移动应用、Postman)
if (!origin) return callback(null, true)
if (whitelist.includes(origin)) {
callback(null, true)
} else {
callback(new Error("不允许的来源"))
}
},
credentials: true
}))
// 单个路由启用
app.get("/public", cors(), (req, res) => {
res.json({ message: "公开数据" })
})
// 预检请求缓存
app.use(cors({
origin: "https://example.com",
maxAge: 86400 // 预检请求缓存 24 小时
}))配置参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
origin | String/Array/Function | * | 允许的源 |
methods | String/Array | GET,HEAD,PUT,PATCH,POST,DELETE | 允许的方法 |
allowedHeaders | Array | - | 允许的请求头 |
exposedHeaders | Array | - | 暴露给客户端的响应头 |
credentials | Boolean | false | 是否允许发送 Cookie |
maxAge | Number | - | 预检请求缓存时间(秒) |
optionsSuccessStatus | Number | 204 | 预检请求成功状态码 |
compression - Gzip 压缩
bash
npm install compressionjavascript
const compression = require("compression")
// 基本用法
app.use(compression())
// 配置选项
app.use(compression({
filter: (req, res) => {
// 自定义过滤函数
if (req.headers["x-no-compression"]) {
return false
}
return compression.filter(req, res)
},
threshold: 1024, // 大于 1KB 才压缩
level: 6, // 压缩级别 0-9
memLevel: 8 // 内存使用级别
}))morgan - 请求日志
bash
npm install morganjavascript
const morgan = require("morgan")
const fs = require("fs")
const path = require("path")
// 开发环境
if (process.env.NODE_ENV === "development") {
app.use(morgan("dev"))
}
// 生产环境 - 写入文件
if (process.env.NODE_ENV === "production") {
const accessLog = fs.createWriteStream(
path.join(__dirname, "logs/access.log"),
{ flags: "a" }
)
app.use(morgan("combined", { stream: accessLog }))
}
// 自定义格式
morgan.token("user-id", (req) => req.user?.id || "anonymous")
app.use(morgan(":method :url :status :user-id - :response-time ms"))预定义格式:
| 格式 | 说明 |
|---|---|
dev | 开发友好,彩色输出 |
combined | Apache 标准格式 |
common | Apache 简化格式 |
short | 比 common 更短 |
tiny | 最简格式 |
cookie-parser - Cookie 解析
bash
npm install cookie-parserjavascript
const cookieParser = require("cookie-parser")
// 使用签名密钥
app.use(cookieParser("secret-key"))
// 设置 Cookie
app.get("/set-cookie", (req, res) => {
// 普通 Cookie
res.cookie("username", "john", {
maxAge: 900000, // 有效期(毫秒)
httpOnly: true, // 防止 XSS
secure: true, // 仅 HTTPS
sameSite: "strict" // CSRF 防护
})
// 签名 Cookie(防篡改)
res.cookie("token", "abc123", { signed: true })
res.send("Cookie 已设置")
})
// 读取 Cookie
app.get("/get-cookie", (req, res) => {
const username = req.cookies.username // 普通 Cookie
const token = req.signedCookies.token // 签名 Cookie
res.json({ username, token })
})
// 清除 Cookie
app.get("/clear-cookie", (req, res) => {
res.clearCookie("username")
res.send("Cookie 已清除")
})Cookie 选项:
| 选项 | 类型 | 说明 |
|---|---|---|
maxAge | Number | 有效期(毫秒) |
expires | Date | 过期时间 |
path | String | 路径限制 |
domain | String | 域名限制 |
secure | Boolean | 仅 HTTPS |
httpOnly | Boolean | 禁止 JS 访问 |
sameSite | String | strict/lax/none |
signed | Boolean | 是否签名 |
express-session - 会话管理
bash
npm install express-sessionjavascript
const session = require("express-session")
app.use(session({
secret: "your-secret-key",
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 小时
}
}))
// 登录
app.post("/login", (req, res) => {
const { username, password } = req.body
if (authenticate(username, password)) {
req.session.user = { username }
req.session.save()
res.json({ message: "登录成功" })
} else {
res.status(401).json({ error: "认证失败" })
}
})
// 检查登录状态
const requireAuth = (req, res, next) => {
if (req.session.user) {
next()
} else {
res.status(401).json({ error: "请先登录" })
}
}
app.get("/profile", requireAuth, (req, res) => {
res.json({ user: req.session.user })
})
// 登出
app.post("/logout", (req, res) => {
req.session.destroy((err) => {
res.clearCookie("connect.sid")
res.json({ message: "已登出" })
})
})Redis 存储(生产环境推荐):
bash
npm install connect-redisjavascript
const RedisStore = require("connect-redis").default
const { createClient } = require("redis")
const redisClient = createClient({
url: process.env.REDIS_URL
})
redisClient.connect()
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: "your-secret-key",
resave: false,
saveUninitialized: false,
cookie: { secure: true, maxAge: 86400000 }
}))multer - 文件上传
bash
npm install multerjavascript
const multer = require("multer")
const path = require("path")
// 内存存储(适合小文件)
const uploadMemory = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 5 * 1024 * 1024 }
})
// 磁盘存储(适合大文件)
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "uploads/")
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9)
cb(null, file.fieldname + "-" + uniqueSuffix + path.extname(file.originalname))
}
})
const upload = multer({
storage,
limits: {
fileSize: 10 * 1024 * 1024, // 10MB
files: 5
},
fileFilter: (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|gif|webp/
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase())
const mimetype = allowedTypes.test(file.mimetype)
if (extname && mimetype) {
cb(null, true)
} else {
cb(new Error("只支持图片文件"))
}
}
})
// 单文件上传
app.post("/upload/single", upload.single("avatar"), (req, res) => {
res.json({
message: "上传成功",
file: req.file
})
})
// 多文件上传(同一字段)
app.post("/upload/multiple", upload.array("photos", 5), (req, res) => {
res.json({
message: "上传成功",
files: req.files
})
})
// 多字段上传
app.post("/upload/fields", upload.fields([
{ name: "avatar", maxCount: 1 },
{ name: "gallery", maxCount: 8 }
]), (req, res) => {
res.json({
avatar: req.files["avatar"],
gallery: req.files["gallery"]
})
})express-rate-limit - 请求限流
bash
npm install express-rate-limitjavascript
const rateLimit = require("express-rate-limit")
// 全局限流
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 每个 IP 最多 100 次请求
message: { error: "请求过于频繁,请稍后再试" },
standardHeaders: true, // 返回 RateLimit 头
legacyHeaders: false
})
app.use(limiter)
// 登录限流(更严格)
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true
})
app.post("/login", loginLimiter, (req, res) => {
// 登录逻辑
})express-validator - 数据验证
bash
npm install express-validatorjavascript
const { body, param, query, validationResult } = require("express-validator")
// 验证中间件
const validate = (req, res, next) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
next()
}
// 用户注册验证
app.post("/users",
body("email").isEmail().normalizeEmail(),
body("password").isLength({ min: 8 }).withMessage("密码至少 8 位"),
body("username").trim().notEmpty().escape(),
validate,
(req, res) => {
res.status(201).json({ message: "注册成功" })
}
)
// 路由参数验证
app.get("/users/:id",
param("id").isMongoId().withMessage("无效的用户 ID"),
validate,
(req, res) => {
res.json({ userId: req.params.id })
}
)
// 查询参数验证
app.get("/search",
query("q").trim().notEmpty().withMessage("搜索关键词不能为空"),
query("page").optional().isInt({ min: 1 }).toInt(),
query("limit").optional().isInt({ min: 1, max: 100 }).toInt(),
validate,
(req, res) => {
const { q, page = 1, limit = 10 } = req.query
res.json({ query: q, page, limit })
}
)常用验证器:
| 验证器 | 说明 |
|---|---|
isEmail() | 邮箱格式 |
isURL() | URL 格式 |
isInt() | 整数 |
isFloat() | 浮点数 |
isLength({ min, max }) | 字符串长度 |
isIn(['a', 'b']) | 枚举值 |
isDate() | 日期格式 |
isMongoId() | MongoDB ObjectId |
notEmpty() | 非空 |
optional() | 可选字段 |
自定义中间件开发
基本模式
javascript
// 简单中间件
function logger(req, res, next) {
console.log(`${req.method} ${req.url}`)
next()
}
app.use(logger)带配置的中间件
javascript
// 工厂函数模式
function auth(options = {}) {
const { token, message = "未授权" } = options
return (req, res, next) => {
const authHeader = req.headers.authorization
if (authHeader === `Bearer ${token}`) {
next()
} else {
res.status(401).json({ error: message })
}
}
}
app.use(auth({ token: "secret-token", message: "请先登录" }))异步中间件
javascript
// 异步中间件包装器
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next)
}
// 使用
app.get("/users/:id", asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id)
if (!user) {
const error = new Error("用户不存在")
error.status = 404
throw error
}
res.json(user)
}))请求计时中间件
javascript
function requestTimer(options = {}) {
const { threshold = 1000 } = options
return (req, res, next) => {
const start = Date.now()
res.on("finish", () => {
const duration = Date.now() - start
if (duration > threshold) {
console.warn(`慢请求: ${req.method} ${req.url} - ${duration}ms`)
}
})
next()
}
}
app.use(requestTimer({ threshold: 500 }))IP 黑名单中间件
javascript
function ipBlacklist(blacklist = []) {
return (req, res, next) => {
const clientIp = req.ip || req.connection.remoteAddress
if (blacklist.includes(clientIp)) {
return res.status(403).json({ error: "访问被拒绝" })
}
next()
}
}
app.use(ipBlacklist(["192.168.1.100", "10.0.0.50"]))请求 ID 中间件
javascript
const { v4: uuidv4 } = require("uuid")
function requestId() {
return (req, res, next) => {
req.id = uuidv4()
res.setHeader("X-Request-ID", req.id)
next()
}
}
app.use(requestId())中间件最佳实践
1. 正确的加载顺序
javascript
const express = require("express")
const app = express()
// 1. 安全相关(最先)
app.use(helmet())
// 2. 日志记录
app.use(morgan("dev"))
// 3. 请求体解析
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
// 4. Cookie 和 Session
app.use(cookieParser())
app.use(session({ ... }))
// 5. 压缩
app.use(compression())
// 6. 静态文件
app.use(express.static("public"))
// 7. CORS
app.use(cors())
// 8. 限流
app.use(rateLimiter)
// 9. 路由
app.use("/api", apiRouter)
// 10. 404 处理
app.use((req, res) => {
res.status(404).json({ error: "资源未找到" })
})
// 11. 错误处理(最后)
app.use(errorHandler)2. 局部中间件
javascript
// ❌ 不推荐:全局使用不必要的中间件
app.use(multer().single("file"))
// ✅ 推荐:只在需要的路由使用
app.post("/upload", upload.single("file"), (req, res) => {
res.json({ file: req.file })
})
// ✅ 推荐:路由级中间件
router.use(authMiddleware)
router.get("/profile", getProfile)
router.get("/settings", getSettings)3. 错误处理
javascript
// ✅ 异步中间件使用 next(error)
app.get("/users", async (req, res, next) => {
try {
const users = await User.find()
res.json(users)
} catch (error) {
next(error)
}
})
// ✅ 或使用包装函数
app.get("/users", asyncHandler(async (req, res) => {
const users = await User.find()
res.json(users)
}))4. 条件加载
javascript
// 根据环境加载中间件
if (process.env.NODE_ENV === "development") {
app.use(morgan("dev"))
}
if (process.env.NODE_ENV === "production") {
app.use(compression())
app.use(helmet())
}5. 中间件复用
javascript
// 提取公共中间件
const commonMiddleware = [
helmet(),
cors(),
express.json()
]
app.use(commonMiddleware)
// 路由特定中间件
const authMiddleware = [verifyToken, checkPermission]
router.get("/admin", ...authMiddleware, adminHandler)常见问题
Q1: 为什么中间件没有执行?
javascript
// ❌ 错误:中间件定义在路由之后
app.get("/", (req, res) => res.send("Hello"))
app.use((req, res, next) => {
console.log("这个不会执行")
next()
})
// ✅ 正确:中间件定义在路由之前
app.use((req, res, next) => {
console.log("这会执行")
next()
})
app.get("/", (req, res) => res.send("Hello"))Q2: 为什么 req.body 是 undefined?
javascript
// ❌ 错误:忘记添加解析中间件
app.post("/users", (req, res) => {
console.log(req.body) // undefined
})
// ✅ 正确:添加 JSON 解析中间件
app.use(express.json())
app.post("/users", (req, res) => {
console.log(req.body) // { name: "John" }
})Q3: next 和 next(err) 的区别?
javascript
// next() - 传递给下一个中间件
app.use((req, res, next) => {
console.log("中间件 1")
next() // 继续执行后续中间件
})
// next(err) - 跳转到错误处理中间件
app.use((req, res, next) => {
const err = new Error("出错了")
next(err) // 跳过所有普通中间件,直接进入错误处理
})
// next('route') - 跳过当前路由的其他处理函数
app.get("/users/:id",
(req, res, next) => {
if (req.params.id === "special") {
return next("route") // 跳到下一个匹配的路由
}
next()
},
(req, res) => {
res.send("普通处理")
}
)
app.get("/users/:id", (req, res) => {
res.send("特殊处理")
})Q4: 如何跳过某些路由的中间件?
javascript
// 方案一:条件判断
app.use((req, res, next) => {
if (req.path === "/health") {
return next() // 跳过健康检查
}
// 执行认证逻辑
})
// 方案二:路径排除
app.use((req, res, next) => {
const publicPaths = ["/login", "/register", "/health"]
if (publicPaths.includes(req.path)) {
return next()
}
authMiddleware(req, res, next)
})
// 方案三:路由级中间件
const publicRouter = express.Router()
const protectedRouter = express.Router()
protectedRouter.use(authMiddleware)
publicRouter.post("/login", loginHandler)
protectedRouter.get("/profile", getProfile)
app.use(publicRouter)
app.use(protectedRouter)Q5: 中间件如何传递数据?
javascript
// 通过 req 对象传递
app.use((req, res, next) => {
req.user = { id: 1, name: "Admin" }
req.startTime = Date.now()
next()
})
app.get("/profile", (req, res) => {
res.json({
user: req.user,
duration: Date.now() - req.startTime
})
})Q6: 如何处理 multipart/form-data?
javascript
// 使用 multer 中间件
const multer = require("multer")
const upload = multer()
// 解析表单数据(不含文件)
app.post("/form", upload.none(), (req, res) => {
console.log(req.body) // 表单字段
})
// 解析文件和表单数据
app.post("/upload", upload.single("file"), (req, res) => {
console.log(req.body) // 其他表单字段
console.log(req.file) // 上传的文件
})