Skip to content

中间件机制

1. Express vs Koa 中间件模型

核心区别

特性ExpressKoa
模型线性 (回调)洋葱 (Promise)
next() 返回值Promise
async/await需手动处理原生支持
后置逻辑困难await next() 后执行

Express 线性模型

javascript
app.use((req, res, next) => {
    console.log('1. 进入');
    next();  // 不会等待后续中间件
    console.log('2. 退出');  // 立即执行,不保证顺序
});

app.use(async (req, res, next) => {
    await someAsyncOperation();  // Express 不会等待!
    next();
});

WARNING

Express 中的异步中间件,如果不在回调中显式处理,可能导致响应提前返回。

Koa 洋葱模型

javascript
app.use(async (ctx, next) => {
    console.log('1. 进入 A');
    await next();  // 暂停,进入下一层
    console.log('6. 退出 A');  // 内层全部完成后执行
});

app.use(async (ctx, next) => {
    console.log('2. 进入 B');
    await next();
    console.log('5. 退出 B');
});

app.use(async (ctx, next) => {
    console.log('3. 进入 C');
    console.log('4. 退出 C');
});

// 输出: 1 → 2 → 3 → 4 → 5 → 6
        ┌─────────────────────────────────┐
        │           Middleware A          │
        │   ┌─────────────────────────┐   │
        │   │       Middleware B      │   │
        │   │   ┌─────────────────┐   │   │
Request │   │   │   Middleware C  │   │   │ Response
───────▶│   │   │                 │   │   │────────▶
        │   │   │                 │   │   │
        │   │   └─────────────────┘   │   │
        │   └─────────────────────────┘   │
        └─────────────────────────────────┘

2. Compose 函数实现 🔥

核心原理

javascript
function compose(middleware) {
    return function (context, next) {
        // index 防止多次调用 next()
        let index = -1;
        return dispatch(0);

        function dispatch(i) {
            // 检测重复调用
            if (i <= index) {
                return Promise.reject(new Error('next() called multiple times'));
            }
            index = i;
            
            let fn = middleware[i];
            // 最后一个中间件后调用外部 next
            if (i === middleware.length) fn = next;
            if (!fn) return Promise.resolve();

            try {
                // Promise.resolve 确保返回 Promise (兼容同步函数)
                return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
            } catch (err) {
                return Promise.reject(err);
            }
        }
    };
}

关键考察点

  1. 递归调用: dispatch(i + 1) 实现链式调用
  2. 闭包状态: index 闭包变量追踪执行进度
  3. Promise 包装: Promise.resolve() 确保异步兼容
  4. 防重复调用: i <= index 检测

使用示例

javascript
const middlewares = [
    async (ctx, next) => {
        ctx.data = [];
        ctx.data.push(1);
        await next();
        ctx.data.push(6);
    },
    async (ctx, next) => {
        ctx.data.push(2);
        await next();
        ctx.data.push(5);
    },
    async (ctx, next) => {
        ctx.data.push(3);
        await new Promise(r => setTimeout(r, 100));
        ctx.data.push(4);
    }
];

const fn = compose(middlewares);
const ctx = {};
await fn(ctx);
console.log(ctx.data);  // [1, 2, 3, 4, 5, 6]

3. 实际应用场景

请求耗时统计

javascript
app.use(async (ctx, next) => {
    const start = Date.now();
    await next();  // 等待所有后续中间件
    const ms = Date.now() - start;
    ctx.set('X-Response-Time', `${ms}ms`);
});

错误处理

javascript
app.use(async (ctx, next) => {
    try {
        await next();
    } catch (err) {
        ctx.status = err.status || 500;
        ctx.body = { error: err.message };
        ctx.app.emit('error', err, ctx);
    }
});

条件中间件

javascript
function unless(path, middleware) {
    return async (ctx, next) => {
        if (ctx.path.startsWith(path)) {
            return next();  // 跳过此中间件
        }
        return middleware(ctx, next);
    };
}

app.use(unless('/public', authMiddleware));

前端面试知识库