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(),请求就卡住了,后面的中间件和路由都不会执行。

我曾经踩过的坑: 有一次写了一个权限校验中间件,忘了在通过校验后调用 next()。结果用户登录成功了,页面却一直转圈加载。排查了半天才发现是 next() 没调用。所以记住:要么调用 next(),要么自己处理响应(比如 res.send()),二选一。

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 根本不会执行。

核心原则: 通用中间件(比如日志、解析body)放在前面,路由处理放在中间,错误处理中间件放在最后。

你还可以用 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))
});
我个人习惯: 写异步中间件时,我会用 try-catch 包裹整个逻辑。这样代码更直观,调试也方便。另外,我建议在项目里封装一个 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 放行正常请求。而且它是异步友好的——没有阻塞操作,性能很好。

总结一下: 自定义中间件其实就是写函数。记住三个参数的作用,控制好执行顺序,处理好异步逻辑。你就能写出高质量的中间件了。