Skip to content
Express.js 基本概念:中间件与路由
请求-响应循环中的中间件执行顺序
在 Express 里,每个到达应用的请求都会触发一连串函数的依次调用,这一连串函数就是中间件和路由处理器。整个流程从接收到 HTTP 请求开始,到发送响应结束,被称为请求-响应循环。
中间件函数的签名是 (req, res, next)。它可以做三件事之一:
- 执行自己的逻辑(比如日志记录、权限检查、解析请求体);
- 通过修改
req/res向后面的中间件或路由传递数据; - 结束响应,或者调用
next()把控制权交给下一个中间件。
如果中间件既不结束响应,也不调用 next(),请求就会一直挂起,客户端最终会等到超时。
一个最直观的例子是连续注册几个中间件,从控制台输出可以看出它们的执行顺序:
javascript
const express = require('express');
const app = express();
app.use((req, res, next) => {
console.log('1 — 第一个中间件');
next();
});
app.use((req, res, next) => {
console.log('2 — 第二个中间件');
res.send('OK');
});
app.listen(3000);对任意路径发起请求,终端总先输出 1 — 第一个中间件,再输出 2 — 第二个中间件,然后响应返回。如果去掉第一个中间件里的 next(),第二个中间件就永远不会执行。
这个顺序完全由 app.use()(以及后面会提到的 app.get、app.post 等)在代码中出现的先后顺序决定。
使用 app.use 挂载中间件:路径与跳过规则
app.use() 是最通用的中间件挂载方式。它可以接受一个可选的路径前缀作为第一个参数,也可以只传函数。
- 不带路径(或者路径写成
'/')时,中间件对所有请求都会执行。 - 带路径时,只有请求 URL 以该路径开头,中间件才会被触发,否则直接跳过。
例如:
javascript
app.use('/api', (req, res, next) => {
console.log('API 请求:', req.method, req.url);
next();
});那么 GET /api/users、POST /api/login、PUT /api/ 都会进入这个中间件,而 GET /about 则不会。这也是“跳过规则”:路径前缀不匹配时,Express 继续尝试下一个注册的中间件或路由。
注意,这里的行为是前缀匹配,不是精确匹配。哪怕请求路径是 /apiRouter 也可能被意外命中——这种情况一般会通过更精确的路由或路径写法来避免。
在中间件中修改 req/res
中间件可以向 req 或 res 对象上附加信息,把它当成一块请求级别的共享状态。下面的例子在中间件里给 req 加上了当前时间戳:
javascript
app.use((req, res, next) => {
req.requestTime = Date.now();
next();
});
app.get('/now', (req, res) => {
res.send(`请求到达时间:${new Date(req.requestTime).toISOString()}`);
});所有在 req 或 res 上新增的属性,都可以被后面执行的中间件、路由处理器直接读取。这也是解析请求体中间件(express.json()、express.urlencoded())把解析结果放到 req.body 的原理。
动手:编写访问日志中间件
一个访问日志中间件需要记录每次请求的方法、URL 和服务器处理完请求所花费的响应时间。这个“处理完”的时机不是连接断开,而是响应数据全部发送到客户端。Node 的 http.ServerResponse 对象在此时会触发 'finish' 事件——在 HTTP keep-alive 模式下,连接可能继续保持,但响应流已经结束。
记录请求方法、URL 与响应时间
一个可复用的日志中间件可以这样写:
javascript
function accessLogger(req, res, next) {
const start = Date.now();
// 在响应发出后记录日志
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl} — ${duration}ms`);
});
next();
}req.originalUrl 保留着请求的原始路径,即使应用被挂载到子路径上也不会被改变,所以用它比 req.url 更可靠。res.on('finish', callback) 注册的回调在响应体完全发送后执行,日志里的耗时就是实际客户端收到响应的延迟。
集成到应用中
这个中间件应该放在所有路由和真正响应请求的中间件之前,而且最好是第一个 app.use(),这样才能计入后续所有处理时间:
javascript
const express = require('express');
const app = express();
app.use(accessLogger);
app.get('/', (req, res) => {
res.json({ message: 'hello' });
});
app.listen(3000);访问 http://localhost:3000/ 时,终端会输出类似:
[2025-03-16T10:12:34.567Z] GET / — 3ms通过浏览器连续刷新几次,能看到每次请求的耗时记录。
定义路由:HTTP 方法 + 路径
路径匹配规则里,路由处理器(由 app.METHOD() 定义)的行为和中间件不太一样。路由处理器要求 HTTP 方法和路径同时匹配才会被调用。
app.get 与 app.post
最常见的两种:
javascript
// 处理 GET 请求
app.get('/users', (req, res) => {
res.json([{ name: 'Alice' }]);
});
// 处理 POST 请求
app.post('/users', (req, res) => {
// 这里如果没有解析请求体,req.body 还是 undefined
res.status(201).json({ created: true });
});app.METHOD 支持所有标准 HTTP 方法:get, post, put, delete, patch, options, head…甚至可以 app.all() 匹配任意方法。
基本路径匹配规则
路由路径默认进行精确匹配(字符串路径时)。下面的例子中:
javascript
app.get('/hello', (req, res) => {
res.send('Hello World');
});GET /hello 会匹配,但 GET /hello/world、GET /hello.html 都不会进入这个路由。要处理带路径参数的情况,需要使用路由参数 :id 或者通配符,但这部分留到后面的文章展开。
与 app.use('/prefix', ...) 的前缀匹配行为相比,路由的精确匹配让开发者可以分别定义 /hello 和 /hello/:name 两个不同的处理器。
next() 的规则与陷阱
next() 是中间件流程控制的关键。在大多数中间件里,调用 next() 就是告诉 Express:我的工作完了,继续处理链里的下一个函数。
以下是几个需要注意的问题:
调用
next()之后不能结束响应,但也不能继续修改已发送的响应。
比如下面的写法会触发 “Cannot set headers after they are sent” 错误:javascriptapp.use((req, res, next) => { res.send('第一段响应'); next(); // 这里调用 next 后,后面的中间件又尝试发响应→错误 });本质上,响应只能结束一次。调用
res.send()或res.json()等之后,就不应该再调用next()让后面的处理器尝试操作响应。next()不是return。
在执行next()后,中间件函数里的剩余代码还是会继续运行:javascriptapp.use((req, res, next) => { console.log('A1'); next(); console.log('A2'); // 仍然会打印 });如果后面还有可能影响响应的代码(比如不小心又写了个
res.send()),就会造成难以排查的错误。推荐的习惯是:如果调用next()后不做任何事情,可以用return next()来显式中断当前函数的执行。next(err)会跳过所有常规中间件,直入错误处理中间件。
只要next的第一个参数不是undefined,Express 就会认为它是个错误,然后丢弃当前处理链,转而寻找错误处理中间件(参数数量为4的函数)。例如:javascriptapp.use((req, res, next) => { const err = new Error('something wrong'); next(err); });这个行为是 Express 错误处理机制的基础。
路由与中间件的协作
中间件和路由处理器不是割裂的两类函数。从 Express 的视角看,一个通过 app.get('/path', handler) 注册的路由,其实就是一种“只匹配特定 HTTP 方法和精确路径”的特殊中间件。因此,当路径不匹配时,路由处理器也会被跳过。
执行顺序依然是它们在代码里出现的顺序。一个常见的错误是把某些需身份校验的路由写在了认证中间件前面:
javascript
app.get('/admin', adminHandler); // 没经过认证
app.use(authMiddleware);
app.get('/dashboard', dashboardHandler); // 经过了认证上面的代码里,/admin 永远不需要认证。要让所有需要保护的路由都经过认证,只需把 authMiddleware 放在它们之前注册即可。
还有一个点:app.use('/prefix', ...) 虽然可以装载中间件,但也可以装载另一个 Router 实例(通过 express.Router() 创建)。这部分模块化拆分会在后续文章详细说明。
错误处理中间件初探
Express 通过参数数量区分错误处理中间件。普通中间件是三个参数 (req, res, next),错误处理中间件是四个参数 (err, req, res, next)。
一个最简单的错误处理中间件:
javascript
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal Server Error' });
});它必须放在所有 app.use()、路由和普通中间件之后。因为 Express 是按注册顺序查找,当某个中间件调用 next(err) 或抛出异常时,会直接跳到第一个错误处理中间件,中间经过的其他中间件(普通中间件和路由)会被跳过。
注意,如果错误处理中间件本身也出错了,或者第四个参数 next 被传入新的错误,那么错误会继续传递给后面的错误处理中间件。所以在定义多个错误处理中间件时,它们之间也构成一条链。
通常一个应用都至少有一个末尾的“兜底”错误处理器,用来捕获未被前面处理掉的异常,并向客户端返回统一格式的错误响应。
下一步
到这里,中间件的执行顺序、next() 的用法、路径匹配规则和路由定义方式都跑了一遍。在访问日志中间件里,还实际观察了 finish 事件和响应时间。中间件的可组合性正是 Express 组织复杂逻辑的基础。
下一篇把目光聚焦到请求和响应对象本身,详细拆解 req.query、req.params、req.body、res.status()、res.json() 等常用能力,让路由处理函数真正写出具体业务来。
