{T}

Express 中间件

中间件(Middleware)是 Express 框架的核心机制。它是在请求到达路由处理函数之前或之后执行的函数,可以访问请求对象 (req)、响应对象 (res) 和下一个中间件函数 (next)。


中间件架构

执行流程

code
客户端请求
    │
    ▼
┌─────────────────────────────────────────────────────┐
│                   中间件链                           │
├─────────────────────────────────────────────────────┤
│                                                     │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐     │
│  │ 中间件 1  │───▶│ 中间件 2  │───▶│ 中间件 3  │     │
│  │          │    │          │    │          │     │
│  │ next()   │    │ next()   │    │ res.send │     │
│  └──────────┘    └──────────┘    └──────────┘     │
│                                                     │
└─────────────────────────────────────────────────────┘
    │
    ▼
  响应客户端

核心特性

特性说明
可访问性可以访问 req(请求对象)和 res(响应对象)
可修改性可以修改 reqres 对象的属性和方法
可控性可以通过 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)
})

配置参数

参数类型默认值说明
limitString/Number'100kb'请求体最大大小
strictBooleantrue只接受数组和对象
typeString/Function'json'解析的 MIME 类型
inflateBooleantrue是否处理压缩的请求体
reviverFunctionnullJSON.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 参数对比

解析库特点适用场景
trueqs支持嵌套对象 user[name]=John复杂表单
falsequerystring不支持嵌套,性能更好简单表单

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

配置参数详解

参数类型默认值说明
dotfilesString'ignore'点文件处理:ignoreallowdeny
etagBooleantrue生成 ETag
extensionsArray[]文件扩展名回退
fallthroughBooleantrue404 是否传递给下一个中间件
immutableBooleanfalse添加 immutable 到 Cache-Control
indexString/Boolean'index.html'默认首页文件
lastModifiedBooleantrue设置 Last-Modified 头
maxAgeNumber/String0Cache-Control max-age
redirectBooleantrue目录路径重定向到尾部带 /
setHeadersFunction-自定义响应头函数

静态文件访问示例

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.css

Post 数据类型与请求体解析(深入)

Express 的请求体解析本质上是Content-Type 选择对应的内置/第三方解析器。下表汇总常见的 Post 数据类型与对应的解析方式:

Content-Type解析器req.body 结果典型场景
application/jsonexpress.json()JS 对象前后端分离接口
application/x-www-form-urlencodedexpress.urlencoded()对象(或嵌套对象)传统表单提交
text/plainexpress.text()字符串纯文本上报
application/octet-streamexpress.raw()Buffer二进制流、签名校验
multipart/form-datamulterreq.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 为空的常见排查

  1. 未挂载对应解析中间件(express.json() / express.urlencoded())。
  2. 客户端 Content-Type 与解析器不匹配(如发了 JSON 但没带 application/json 头)。
  3. 请求体超限被丢弃(limit 配置过小)。
  4. 使用了 multipart/form-data 却用 express.json() 解析(必须用 multer)。
  5. 中间件顺序错误:解析器必须在路由处理函数之前注册。

小结:请求体解析是「按 Content-Type 匹配解析器」的管道,选对中间件并放在正确顺序是核心。


第三方中间件

常用中间件概览

中间件用途npm 周下载量推荐度
cors跨域资源共享⭐⭐⭐⭐⭐
helmet安全 HTTP 头⭐⭐⭐⭐⭐
morgan请求日志⭐⭐⭐⭐
compressionGzip 压缩⭐⭐⭐⭐⭐
cookie-parserCookie 解析⭐⭐⭐⭐
express-session会话管理⭐⭐⭐⭐
multer文件上传⭐⭐⭐⭐⭐
express-validator数据验证⭐⭐⭐⭐⭐
express-rate-limit请求限流⭐⭐⭐⭐⭐

helmet - 安全 HTTP 头

bash
npm install helmet
javascript
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-ProtectionXSS 过滤器
Referrer-Policy控制 Referrer 信息

cors - 跨域资源共享

bash
npm install cors
javascript
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 小时
}))

配置参数

参数类型默认值说明
originString/Array/Function*允许的源
methodsString/ArrayGET,HEAD,PUT,PATCH,POST,DELETE允许的方法
allowedHeadersArray-允许的请求头
exposedHeadersArray-暴露给客户端的响应头
credentialsBooleanfalse是否允许发送 Cookie
maxAgeNumber-预检请求缓存时间(秒)
optionsSuccessStatusNumber204预检请求成功状态码

compression - Gzip 压缩

bash
npm install compression
javascript
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 morgan
javascript
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开发友好,彩色输出
combinedApache 标准格式
commonApache 简化格式
short比 common 更短
tiny最简格式
bash
npm install cookie-parser
javascript
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 选项

选项类型说明
maxAgeNumber有效期(毫秒)
expiresDate过期时间
pathString路径限制
domainString域名限制
secureBoolean仅 HTTPS
httpOnlyBoolean禁止 JS 访问
sameSiteStringstrict/lax/none
signedBoolean是否签名

express-session - 会话管理

bash
npm install express-session
javascript
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-redis
javascript
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 multer
javascript
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-limit
javascript
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-validator
javascript
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)  // 上传的文件
})

参考资源