六维教程

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、方法、状态码设计规范统一起来。

上一篇
Node.js Express 入门
下一篇
Node.js RESTful API 设计