4. 自定义中间件开发:从零开始写你的第一个中间件
好,咱们进入正题。前面几章聊了中间件的概念和Express内置的中间件,今天我来手把手带你写一个自定义中间件。
说实话,我刚学Node.js那会儿,总觉得中间件是个很玄乎的东西。后来自己动手写了一个,才发现——说白了,中间件就是一个函数。嗯,就是一个普通的函数。
4.1 第一个自定义中间件:日志记录器
咱们先写一个最简单的。假设你想记录每个请求的路径和耗时。
// logger.js
function requestLogger(req, res, next) {
const start = Date.now();
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
// 监听响应结束事件
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`耗时: ${duration}ms`);
});
next();
}
module.exports = requestLogger;
然后在你的应用里这样用:
const express = require('express');
const app = express();
const requestLogger = require('./logger');
app.use(requestLogger);
app.get('/', (req, res) => {
res.send('Hello World');
});
你看,就这么简单。这个中间件会在每个请求进来时打印时间戳和方法路径,请求结束后打印耗时。
4.2 中间件的三个参数:req, res, next
这三个参数是中间件的核心。咱们一个一个说。
| 参数 | 说明 | 常见操作 |
|---|---|---|
| req | 请求对象 | req.url, req.method, req.headers, req.body |
| res | 响应对象 | res.send(), res.json(), res.status() |
| next | 下一个中间件函数 | next(), next('route'), next(err) |
req 是客户端发来的请求信息。你可以从中拿到URL、请求头、参数等。
res 是你用来给客户端回数据的。想返回JSON就用 res.json(),想返回HTML就用 res.send()。
next 这个最容易被忽略,但恰恰最重要。你不调用 next(),请求就卡住了,后面的中间件和路由都不会执行。
4.3 中间件执行顺序控制
中间件的执行顺序,完全取决于你注册的顺序。这个很重要,我再说一遍:顺序决定一切。
看个例子:
app.use((req, res, next) => {
console.log('中间件 A');
next();
});
app.use((req, res, next) => {
console.log('中间件 B');
next();
});
app.get('/', (req, res) => {
console.log('路由处理');
res.send('Done');
});
请求进来时,控制台会依次打印:
中间件 A
中间件 B
路由处理
如果你把顺序调换一下:
app.get('/', (req, res) => {
console.log('路由处理');
res.send('Done');
});
app.use((req, res, next) => {
console.log('中间件 A');
next();
});
这时候,路由先匹配到了,直接返回响应。中间件 A 根本不会执行。
你还可以用 next('route') 跳过当前路由,直接匹配下一个路由。这个技巧在写条件路由时特别有用。
app.get('/user/:id', (req, res, next) => {
if (req.params.id === 'admin') {
// 跳过这个路由,交给下一个路由处理
next('route');
} else {
res.send('普通用户');
}
});
app.get('/user/:id', (req, res) => {
res.send('管理员页面');
});
4.4 异步中间件处理
现实项目中,中间件经常要干异步的事情——查数据库、调外部API、读写文件。这时候就得小心处理了。
先看一个错误示范:
// ❌ 这样写会出问题
app.use(async (req, res, next) => {
const user = await db.findUser(req.params.id);
req.user = user;
next();
});
这段代码看着没问题,但如果 db.findUser 抛异常了,Express 不会自动捕获。你的应用会直接崩溃。
正确的做法是:
// ✅ 用 try-catch 包裹
app.use(async (req, res, next) => {
try {
const user = await db.findUser(req.params.id);
req.user = user;
next();
} catch (err) {
next(err); // 把错误传给错误处理中间件
}
});
或者用 Promise 的 catch 方法:
app.use((req, res, next) => {
db.findUser(req.params.id)
.then(user => {
req.user = user;
next();
})
.catch(next); // 等价于 .catch(err => next(err))
});
asyncHandler 工具函数,避免每个中间件都写一遍 try-catch。
封装一个 asyncHandler:
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// 使用
app.use(asyncHandler(async (req, res, next) => {
const user = await db.findUser(req.params.id);
req.user = user;
next();
}));
这样写,代码干净多了。你想想看,每个中间件都少写一个 try-catch,整个项目能清爽不少。
4.5 实战:一个完整的自定义中间件
最后,咱们把今天学的串起来,写一个完整的中间件——请求限流器。
// rateLimiter.js
const requestCounts = new Map();
function rateLimiter(maxRequests = 10, windowMs = 60000) {
return (req, res, next) => {
const ip = req.ip;
const now = Date.now();
if (!requestCounts.has(ip)) {
requestCounts.set(ip, []);
}
const timestamps = requestCounts.get(ip);
// 清理过期的时间戳
const recentTimestamps = timestamps.filter(
timestamp => now - timestamp < windowMs
);
if (recentTimestamps.length >= maxRequests) {
return res.status(429).json({
error: '请求过于频繁,请稍后再试'
});
}
recentTimestamps.push(now);
requestCounts.set(ip, recentTimestamps);
next();
};
}
module.exports = rateLimiter;
使用:
const rateLimiter = require('./rateLimiter');
// 每个IP每分钟最多10个请求
app.use(rateLimiter(10, 60000));
这个中间件用到了今天讲的所有知识点:req 获取客户端IP,res 返回限流响应,next 放行正常请求。而且它是异步友好的——没有阻塞操作,性能很好。