Express 中间件与控制器
1. 中间件
中间件概念
中间件 (Middleware) 是 Express 框架的基石和灵魂。它本质上是一个函数,可以访问请求对象 (req
)、响应对象 (res
) 以及请求-响应周期中的下一个中间件函数 (next
)。
中间件可以执行以下核心任务:
- 执行任何代码。
- 修改请求和响应对象(例如,为
req
对象附加用户信息)。 - 结束请求-响应周期(例如,通过
res.send()
发送响应)。 - 调用栈中的下一个中间件(通过
next()
)。
中间件的执行顺序
Express 会按照我们使用 app.use()
或 app.METHOD()
定义它们的顺序,依次执行它们。
const express = require('express');
const app = express();
// 中间件 1: 日志记录
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next(); // 传递给下一个中间件
});
// 中间件 2: 身份验证
app.use((req, res, next) => {
console.log('Authentication check...');
// 实际应用中这里会有复杂的验证逻辑
next();
});
app.get('/', (req, res) => {
res.send('Hello World!');
});
// 请求到达时,控制台会先打印日志,然后打印身份验证信息,最后才由路由处理器发送响应。
next()
函数
next()
函数是连接中间件链条的关键。调用它会将控制权传递给下一个中间件。如果不调用 next()
也不发送响应,请求将被“挂起”,客户端会一直等待直到超时。
next()
的调用方式决定了控制权的流向:
next()
: 不带参数调用,将控制权传递给下一个非错误处理的中间件。next(error)
: 传递一个Error
对象或其他值,将跳过所有后续的常规中间件,直接将控制权交给第一个错误处理中间件。next('route')
: 仅在router.METHOD()
或app.METHOD()
加载的中间件中有效。它会跳过当前路由实例中所有剩余的处理器,将控制权交给下一个匹配的路由。
下面是使用
next('route')
的一个例子:
const express = require("express");
const app = express();
// 请求 GET /user/123
app.get(
"/user/:id",
(req, res, next) => {
console.log("第一条流水线,工位A: 检查ID");
if (req.params.id === "0") {
console.log("ID是0,这是特殊产品,转交下一条流水线处理");
next("route"); // 跳出当前 app.get 的处理链
} else {
console.log("ID有效,在本流水线继续");
next(); // 正常传递到工位B
}
},
(req, res, next) => {
// 这个处理器是 工位B
console.log("第一条流水线,工位B: 处理常规用户");
res.send(`Regular User: ${req.params.id}`);
}
);
对于请求
GET /user/123
,req.params.id
不等于 “0”,next
正常调用,控制权交给同一路由内的 “工位B”。
对于请求
GET /user/0
,req.params.id
是 “0”。调用 next(‘route’)。Express 立即跳过 “工位B”。Express 继续向后查找,发现还有另一个app.get('/user/:id', ...)
也能匹配该请求。于是控制权交给第二个app.get
的处理器。
2. 中间件类型
应用层中间件
应用层中间件通过 app.use()
或 app.METHOD()
绑定到 app
对象实例上。如果没有指定路径,它会对应用的每一个请求生效。
// 这个中间件会对所有请求生效
app.use((req, res, next) => {
console.log('This runs for every request!');
next();
});
// 这个中间件仅对路径以 /api 开头的请求生效
app.use('/api', apiSpecificMiddleware);
路由层中间件
路由层中间件绑定到 express.Router()
的实例上。它只对通过该路由的请求生效,是实现模块化的基础。
const express = require('express');
const router = express.Router();
// 这个中间件仅对 user-router 中的所有路由生效
router.use((req, res, next) => {
console.log('Accessing user section...');
next();
});
// ... router.get(), router.post() 等
module.exports = router;
内置中间件
Express 提供了一些开箱即用的关键中间件:
express.json()
: 解析Content-Type
为application/json
的请求体,并将解析后的数据填充到req.body
。express.urlencoded({ extended: true })
: 解析Content-Type
为application/x-www-form-urlencoded
的请求体(即表单提交的数据)。express.static('public')
: 托管静态文件。例如,public
目录下的style.css
文件可以通过http://localhost:3000/style.css
访问。
第三方中间件
Express 拥有庞大的生态系统,可以通过 npm
安装各种第三方中间件来快速添加功能:
cors
: 处理跨域资源共享(CORS)。helmet
: 通过设置各种 HTTP 头来提高应用的安全性。morgan
: 强大的 HTTP 请求日志记录工具。cookie-parser
: 解析Cookie
头,并通过req.cookies
对象提供。
3. 控制器
基本概念
在 MVC (Model-View-Controller) 设计模式中,控制器是核心协调者。它的职责是:
- 接收请求: 作为路由的直接处理器,接收来自客户端的请求。
- 与模型交互: 调用模型(Model,通常是数据库操作层)来获取或修改数据。
- 准备响应: 处理业务逻辑,并将最终结果传递给视图(View)进行渲染,或直接作为 API 响应(如 JSON)发送回客户端。
在 Express 中实现控制器
在 Express 中,控制器就是作为路由处理器的中间件函数。最佳实践是将这些控制器函数从路由定义中分离出来,放到单独的文件中,以保持代码的清晰和可维护性。
- 创建控制器文件:
// controllers/authorController.js
const db = require('../db'); // 假设这是我们的模拟数据库模块
// 控制器函数:根据 ID 获取作者
const getAuthorById = async (req, res, next) => {
const { authorId } = req.params;
const author = await db.getAuthorById(Number(authorId));
if (!author) {
// 如果找不到作者,可以发送 404 响应
return res.status(404).json({ message: "Author not found" });
}
// 发送 JSON 格式的响应
res.status(200).json(author);
};
module.exports = { getAuthorById };
- 在路由文件中使用控制器:
// routes/authorRouter.js
const { Router } = require('express');
const { getAuthorById } = require('../controllers/authorController');
const authorRouter = Router();
// 将路由和控制器清晰地绑定在一起
authorRouter.get('/:authorId', getAuthorById);
module.exports = authorRouter;
响应客户端
在 Express 中,每个请求处理函数的 res
对象代表了服务器对客户端请求的响应。控制器必须调用 res
上的某个方法来结束请求——响应周期,否则客户端将一直等待直到超时。
下面是常用的 res 方法:
res.send()
: 通用方法,可以发送字符串、Buffer 或对象。它会根据内容自动设置Content-Type
。res.json()
: API 开发首选。它确保响应是application/json
格式,并能正确处理null
和undefined
等值。res.status(code)
: 设置 HTTP 状态码,可以链式调用,如res.status(404).send('Not Found')
。res.redirect([status,] path)
: 重定向请求到另一个 URL。res.sendFile(path)
: 读取文件并将其作为响应发送。
4. 优雅的错误处理
异步错误处理
从 Express v5 开始,框架原生支持异步错误处理。如果在一个 async
的路由处理器或中间件中抛出错误,Express 会自动捕获它并将其传递给错误处理中间件。这极大地简化了代码,我们不再需要在每个异步函数中都写 try...catch
块来调用 next(error)
。
// Express 5+ 中,这个错误会被自动捕获
app.get('/async-error', async (req, res, next) => {
throw new Error('Something went wrong in async land!');
});
错误处理中间件
错误处理用于集中处理应用中发生的所有错误。它有以下特点:
- 它的函数签名必须是
(err, req, res, next)
,多出来的第一个参数err
用于接收错误对象。 - 它必须在所有其他
app.use()
和路由调用之后定义,否则它无法捕获在它之后定义的路由中发生的错误。
// ... 其他所有路由和中间件
// 错误处理中间件
app.use((err, req, res, next) => {
console.error(err.stack); // 在服务器控制台记录完整的错误堆栈
// 向客户端发送一个通用的、对用户友好的错误消息
res.status(500).json({ message: 'Internal Server Error' });
});
创建自定义错误
为了在错误处理器中能根据错误类型做出不同响应(例如,返回 404 而不是 500),我们可以创建自定义错误类。
// errors/NotFoundError.js
class NotFoundError extends Error {
constructor(message) {
super(message);
this.name = 'NotFoundError';
this.statusCode = 404;
}
}
module.exports = NotFoundError;
使用自定义错误处理逻辑
现在,我们可以在控制器中抛出自定义错误,并在错误处理器中检查它:
- 控制器
const NotFoundError = require('../errors/NotFoundError');
const getAuthorById = async (req, res, next) => {
const author = await db.getAuthorById(Number(req.params.authorId));
if (!author) {
// 抛出自定义错误,Express 5 会自动捕获并传递给 next()
throw new NotFoundError('Author not found');
}
res.json(author);
};
- 增强的错误处理器
app.use((err, req, res, next) => {
console.error(err);
// 检查我们自定义的 statusCode,如果没有则默认为 500
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
res.status(statusCode).json({ message });
});