Express 的核心设计基于三个紧密配合的模块:中间件机制、路由系统和错误处理。它们是构建 Express 应用的骨架,只要理解了这三者,就能读懂绝大多数的 Express 代码,也能写出结构清晰的 Web 服务。
中间件核心机制
在 Express 中,中间件就是一个函数,它在请求(request)和响应(response)对象之间按顺序执行。每个中间件都可以对请求或响应对象进行加工,或者选择将请求传递给下一个中间件,也可以直接结束响应(如返回数据或重定向),从而中断链条。
一个标准的中间件函数签名为:
function middleware(req, res, next) {
// 处理逻辑
next(); // 传递给下一个中间件
}
其中 next 是一个函数,调用它表示“我处理完了,交给下一位”。如果不调用 next(),请求就会悬挂在当前位置,客户端会一直等待,直到超时。如果直接调用 res.send() 或 res.json() 结束了响应,则不需要(也不应该)再调用 next。
Express 的中间件是线性串行的,与传统 Koa 的洋葱模型不同。Express 没有内置的“回溯”机制——中间件执行完毕后,控制权不会自动回到上一个中间件。虽然可以通过一些惯用方法(如在响应结束后执行 next)实现类似效果,但通常这不是必要的,反而不如直接使用 res.send 终结请求来得清晰。
一个典型的 Express 应用其实就是一连串中间件的组合:
const express = require('express');
const app = express();
// 应用级别的中间件:解析 JSON 请求体
app.use(express.json());
// 自定义日志中间件
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
// 处理 /users 路径下所有请求的中间件
app.use('/users', usersRouter);
// 兜底中间件:404
app.use((req, res) => {
res.status(404).json({ error: 'Not Found' });
});
app.listen(3000);
中间件可以挂载到不同类型的“路径”上,Express 会按照代码书写的顺序和路径匹配情况依次尝试执行。主要分为以下几种:
- 应用级中间件:通过
app.use()或app.METHOD()挂载,作用于整个应用或指定前缀的路径。 - 路由器级中间件:通过
express.Router()创建,作用范围是当前路由器,写法与app一样。 - 错误处理中间件:后面单独说明,通过四个参数签名定义。
- 内置中间件:Express 从 4.x 开始内置了
express.json()(解析 JSON 请求体)和express.urlencoded()(解析 URL 编码的表单数据),代替了早期需要 body-parser 的方式。 - 第三方中间件:如
cors、helmet、morgan等,直接通过app.use()引入。
在实际开发中,通常会把中间件分为请求预处理(解析、日志、跨域)、业务路由、和错误处理三层,保证代码组织和职责清晰。
路由系统
Express 的路由系统本质上是中间件的一种特殊形式:根据 HTTP 方法和路径来筛选请求,并执行对应的处理函数。它内置在 app 和 Router 对象中,使用方式非常直观。
基本路由定义:
// 定义 GET /api/users 的处理
app.get('/api/users', (req, res) => {
res.json([{ id: 1, name: 'Alice' }]);
});
// POST 方法
app.post('/api/users', (req, res) => {
// 创建用户
res.status(201).json({ id: 2, name: req.body.name });
});
// 动态路由参数
app.get('/api/users/:id', (req, res) => {
const { id } = req.params; // req.params 包含路径参数
res.json({ id, name: 'User ' + id });
});
// 链式处理一个路径上的多个方法
app.route('/api/users')
.get((req, res) => { /* 查询列表 */ })
.post((req, res) => { /* 新增用户 */ });
当应用规模扩大时,把所有路由都堆在 app.js 中会变得难以维护。这时使用 express.Router() 可以将路由按功能模块拆分:
// routes/users.js
const router = require('express').Router();
router.get('/', (req, res) => {
res.json({ list: [] });
});
router.get('/:id', (req, res) => {
res.json({ id: req.params.id });
});
module.exports = router;
// app.js
const usersRouter = require('./routes/users');
app.use('/api/users', usersRouter);
这种方式将路由组织成资源树,每个模块内部的路径都是相对路径,模块间不会互相干扰。路由级中间件也可以搭配使用,例如为某组路由添加统一的身份校验:
// authMiddleware 验证 token
router.use(authMiddleware);
router.get('/profile', (req, res) => {
// 这里已经经过 authMiddleware 鉴权
res.json(req.user);
});
路由匹配遵循“先定义先匹配”原则。一旦一个路由处理函数调用了 res.send 结束响应,后续的路由就不会再被执行。因此要注意路由顺序,尤其是动态路由可能“吞掉”后面的静态路由名称。例如,如果将 /:id 放在 /new 之前,那么 /new 会被当作一个 id 参数处理。解决办法就是把静态路径写在前面。
错误处理
Express 的错误处理同样基于中间件,只是它的函数签名不同——必须包含四个参数:(err, req, res, next)。Express 会将其识别为错误处理中间件,只有在发生错误时才会调用它。
错误的发生方式主要包括:
- 抛出异常:同步代码中直接
throw new Error('...'),Express 会自动捕获并交给错误处理中间件。 - 调用
next(err):在异步操作中捕获到错误后,通过next(err)显式传递给错误处理中间件。这是异步场景下推荐的错误传递方式。 - Promise 拒绝未捕获:如果在
async路由中直接throw而没有 try/catch,Express 从 5.x 版本开始会捕获,但 4.x 中可能不会。实际开发中建议包裹异步路由或使用错误捕获工具函数。
通常会在所有业务路由之后,定义错误处理中间件:
// 业务路由
app.use('/api', apiRouter);
// 404 处理(也算一种错误)
app.use((req, res, next) => {
res.status(404).json({ error: 'Not Found' });
});
// 统一错误处理中间件
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: err.message || 'Internal Server Error'
});
});
注意:这里的 err 对象可以从上游通过 next(err) 传入,并且可以扩展额外属性(如 err.status)供错误处理中间件使用。常见做法是封装一个自定义错误类,携带 HTTP 状态码和消息。
在实际业务路由中处理异步错误时,可以这样:
app.get('/users/:id', async (req, res, next) => {
try {
const user = await findUser(req.params.id);
if (!user) {
const err = new Error('User not found');
err.status = 404;
throw err; // 被 catch 捕获
}
res.json(user);
} catch (err) {
next(err); // 传递给错误处理中间件
}
});
为了减少模板代码,可以写一个高阶函数自动包装异步路由:
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await findUser(req.params.id);
if (!user) {
const err = new Error('User not found');
err.status = 404;
throw err;
}
res.json(user);
}));
这样业务代码中就不需要手动 try/catch 和 next(err),代码更加专注。
Express 的错误处理中间件可以有多个,按顺序链式执行。如果一个错误处理中间件调用了 next(err)(参数非空),则交给下一个错误处理中间件继续处理;如果调用 next() 无参数,则跳出错误处理链,进入正常中间件流程(但一般不这么做,容易造成混乱)。
从 Express 4.x 升级到 5.x 时,要注意 Promise 拒绝和同步抛错的捕获行为的改进,以及错误处理中间件用法的一致性。但核心设计思想不变:通过中间件链来处理错误,而非在每个路由里分别处理,从而实现集中化的错误日志记录和统一响应格式。
小结:Express 的中间件机制是它的灵魂,通过 (req, res, next) 串联起整个请求生命周期;路由系统将 URL 和方法映射到具体的处理逻辑;错误处理给了开发者一个兜底的网,保证异常不会直接导致进程崩溃并给客户端返回一致的错误形状。掌握这三者,就能得心应手地用 Express 构建清晰、健壮的 Web 服务。