Express 中的中间件是一个签名为 (req, res, next) 的函数,运行在请求到达和响应发出之间。每个中间件都可以读取或修改 req 和 res,可以发送响应来结束请求,也可以调用 next() 把请求交给下一个函数。app.use(fn) 为每个请求添加中间件,Express 会按你注册的顺序依次运行它们。
每个请求到来时,控制台都会按顺序打印这三步,而路由读取的 req.startedAt 是日志中间件添加的属性。发送 GET /missing:两个中间件仍然会打印,因为 app.use() 匹配所有路径,之后由于没有匹配的路由,Express 返回 404。
忘记调用 next()
一个既不发送响应、也不调用 next() 的中间件会让请求一直挂起。客户端会一直等到超时,日志里也什么都看不到,所以这是一个常见的中间件 bug。把上面 logger 中的 next(); 删掉,再发送一次 GET /:请求永远得不到响应。
规则很简单:中间件里的每条执行路径都必须以下面三者之一结束,而且只能是一个:
next(),继续往下走;next(err),跳到错误处理器(见 Express 错误处理);- 一个响应,例如
res.json()或res.status(401).send()。
发送响应之后再调用 next() 也是 bug。下一个处理函数会尝试发送第二个响应,Express 就会抛出 ERR_HTTP_HEADERS_SENT。写成 return res.status(401).json(...),让函数在那里结束。
拦截请求的鉴权检查
拒绝请求的中间件会自己发送响应,并且不调用 next()。把它作为额外参数传给某个路由,就只保护这一个路由:
第一个请求得到 401 和 {"error":"Missing or wrong API key"}。在请求栏中添加请求头 x-api-key: secret123 后再发送一次:这次的响应是 {"users":42,"caller":"reporting-app"}。/public 永远不会运行这项检查。
中间件在哪里运行
| 写法 | 作用于 |
|---|---|
app.use(fn) | 所有请求,所有方法 |
app.use('/api', fn) | 以 /api 开头的路径 |
app.get('/admin', fn, handler) | 仅 GET /admin,在 handler 之前运行 |
router.use(fn) | 该路由器的所有路由 |
顺序本身就有含义。注册在路由之后的日志中间件永远记录不到那些路由已经响应过的请求,因为发送了响应的路由不会调用 next()。先注册日志、请求体解析、CORS 这类全局中间件,然后是路由,最后是 404 和错误处理器。
内置中间件
Express 自带几个中间件函数,用 app.use() 添加即可。请求体解析器会填充 req.body;没有它们,req.body 就是 undefined。
| 中间件 | 作用 |
|---|---|
express.json() | 解析 JSON 请求体(Content-Type: application/json) |
express.urlencoded() | 解析 HTML 表单提交(name=Ada&age=36) |
express.text() | 把纯文本请求体以字符串形式放进 req.body |
express.static('public') | 提供某个文件夹中的文件 |
发送 POST /echo,请求体为 {"name": "Ada"},响应中会包含 "body":{"name":"Ada"}。删掉 express.json() 这一行再发送一次,响应里就没有 body 了:req.body 是 undefined,而 JSON.stringify 会省略值为 undefined 的属性。cors、helmet、morgan 这些包也是中间件:用 npm install 安装后,同样传给 app.use()。提供静态文件的内容见 express.static 页面。
常见错误
调用了中间件,而不是传入它。 app.use(logger()) 会在启动时运行一次 logger,并注册它的返回值。应该写 app.use(logger)。(express.json() 带括号,是因为它返回一个中间件函数。)
在路由之后才添加请求体解析器。 路由先运行,看到的 req.body 是 undefined。
以为 app.use('/admin', fn) 只匹配一个路径。 它也会对 /admin/users 以及 /admin 下的所有其他路径运行,而且不分请求方法。要只保护一个路由,把中间件传给 app.get('/admin', fn, handler)。
常见问题
中间件和路由处理函数有什么区别?
从技术上说没有区别:两者都是 (req, res, next) 函数。按照惯例,路由处理函数用一个响应结束请求,而中间件通常做一件公共的事情,然后调用 next()。
中间件可以是 async 函数吗?
可以。在 Express 5 中,抛出错误或返回被拒绝 Promise 的 async 中间件会自动把错误交给你的错误处理器。先 await 你需要的东西,再调用 next()。
next('route') 有什么作用?
它会跳过当前路由剩下的处理函数,转到下一个匹配的路由。它只在用 app.get()、app.post() 等请求方法函数添加的处理函数中有效,在 app.use() 中无效。
如何让某个路由跳过一个中间件?
把这个路由注册在中间件之前,让它先响应;或者在中间件里检查路径(if (req.path === '/health') return next();)。把中间件按路由器或按路由挂载,而不是挂在整个应用上,通常就不需要这样做了。