Express 中间件原理

如果你说的是 Node.js / Express 的中间件(Middleware)原理,核心其实就一句话:

Express 把一系列函数按顺序串起来,每个函数通过 next() 把控制权交给下一个函数。

1. 最简单的模型

javascript 复制代码
const express = require('express');
const app = express();

app.use((req, res, next) => {
  console.log('middleware 1');
  next();
});

app.use((req, res, next) => {
  console.log('middleware 2');
  next();
});

app.get('/', (req, res) => {
  console.log('route handler');
  res.send('Hello');
});

app.listen(3000);

请求 / 时:

scss 复制代码
Request
   ↓
middleware 1
   ↓ next()
middleware 2
   ↓ next()
route handler
   ↓
Response

所以中间件本质上就是:

lua 复制代码
(req, res, next) => {}

2. next() 到底是什么?

可以把 Express 的内部机制简化成:

ini 复制代码
const middlewares = [
  middleware1,
  middleware2,
  middleware3
];

function handle(req, res) {
  let index = 0;

  function next() {
    const middleware = middlewares[index++];

    if (!middleware) return;

    middleware(req, res, next);
  }

  next();
}

于是:

lua 复制代码
function middleware1(req, res, next) {
  console.log('1');
  next();
}

function middleware2(req, res, next) {
  console.log('2');
  next();
}

function middleware3(req, res, next) {
  console.log('3');
  next();
}

实际上就是:

scss 复制代码
next()
  ↓
找到下一个 middleware
  ↓
middleware(req, res, next)
  ↓
middleware 内部再次 next()
  ↓
继续找下一个

这就是 Express 中间件最核心的原理。


3. 为什么中间件可以修改 req

因为所有中间件拿到的是同一个 req 对象

例如:

ini 复制代码
app.use((req, res, next) => {
  req.user = {
    id: 123,
    name: 'Tom'
  };

  next();
});

app.get('/', (req, res) => {
  console.log(req.user);
  res.send('OK');
});

执行过程:

ini 复制代码
        ┌──────────────────┐
Request │ req              │
        │ res              │
        └────────┬─────────┘
                 ↓
          middleware 1
                 │
           req.user = ...
                 ↓
          middleware 2
                 │
          读取 req.user
                 ↓
             response

所以很多 Express 功能本质上都是:

markdown 复制代码
前置 middleware
      ↓
修改 req / res
      ↓
后续 middleware
      ↓
业务 handler

4. next() 不调用会怎样?

例如:

javascript 复制代码
app.use((req, res, next) => {
  console.log('hello');
});

这里既没有:

scss 复制代码
next();

也没有:

ini 复制代码
res.send(...);

那么请求就会停在这里。

vbscript 复制代码
Request
   ↓
middleware
   ↓
   ❌ 卡住

所以中间件通常有两个选择:

继续执行

scss 复制代码
next();

结束请求

arduino 复制代码
res.send('Hello');

5. Express 中间件为什么能处理异常?

还有一个非常重要的设计:

javascript 复制代码
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).send('Server Error');
});

这种有 4 个参数

lua 复制代码
(err, req, res, next)

Express 会把它识别为错误处理中间件

例如:

javascript 复制代码
app.use((req, res, next) => {
  try {
    throw new Error('Something wrong');
  } catch (err) {
    next(err);
  }
});

执行:

vbscript 复制代码
middleware
    │
    │ next(error)
    ↓
跳过普通 middleware
    ↓
错误处理中间件
    ↓
response

也就是说:

scss 复制代码
next()

表示:

正常进入下一个 middleware

而:

scss 复制代码
next(err)

表示:

出错了,请进入错误处理链。


6. 中间件其实是一个"责任链"

从设计模式角度看,Express Middleware 很像:

Chain of Responsibility(责任链模式)

例如一个真实项目:

php 复制代码
app.use(cors());

app.use(logger);

app.use(auth);

app.use(rateLimit);

app.use(express.json());

app.use('/api', router);

请求:

vbscript 复制代码
HTTP Request
     ↓
   CORS
     ↓
  Logger
     ↓
   Auth
     ↓
 RateLimit
     ↓
 JSON Parser
     ↓
   Router
     ↓
 Controller
     ↓
 Service
     ↓
 Database
     ↓
 HTTP Response

每一层都可以:

  • 修改请求
  • 修改响应
  • 终止请求
  • 继续请求
  • 抛出错误

7. 一个容易忽略的点:next() 之后的代码

看这个:

javascript 复制代码
app.use((req, res, next) => {
  console.log('A');

  next();

  console.log('B');
});

app.use((req, res, next) => {
  console.log('C');

  next();

  console.log('D');
});

结果是:

css 复制代码
A
C
D
B

为什么?

因为调用:

scss 复制代码
next();

实际上相当于进入下一层函数调用

可以想象成:

javascript 复制代码
function middleware1() {
  console.log('A');

  middleware2();

  console.log('B');
}

function middleware2() {
  console.log('C');

  // next 后继续执行
  console.log('D');
}

所以 Middleware 不只是:

复制代码
1 → 2 → 3

更准确地说是:

scss 复制代码
        middleware 1
             ↓
        next()
             ↓
        middleware 2
             ↓
        next()
             ↓
        middleware 3
             ↓
        return
             ↑
        middleware 2
             ↑
        middleware 1

这也是理解 Express 中间件非常关键的一点。


8. 用一个极简版本自己实现 Express

如果你想真正理解源码,可以自己实现一个 Mini Express:

scss 复制代码
function createApp() {
  const middlewares = [];

  function use(fn) {
    middlewares.push(fn);
  }

  function handle(req, res) {
    let index = 0;

    function next(err) {
      const fn = middlewares[index++];

      if (!fn) {
        return;
      }

      if (err) {
        // 错误处理中间件
        if (fn.length === 4) {
          return fn(err, req, res, next);
        }

        return next(err);
      }

      if (fn.length === 4) {
        return next();
      }

      fn(req, res, next);
    }

    next();
  }

  return {
    use,
    handle
  };
}

然后:

scss 复制代码
const app = createApp();

app.use((req, res, next) => {
  console.log('middleware 1');
  next();
});

app.use((req, res, next) => {
  console.log('middleware 2');
  next();
});

这已经抓住了 Express Middleware 最核心的思想

你可以记住这张脑图

scss 复制代码
                 Express
                    │
              Middleware Stack
                    │
        ┌───────────┼───────────┐
        ↓           ↓           ↓
      fn(req)     fn(req)     fn(req)
        │           │           │
        └── next() ─┘           │
                                ↓
                         Route Handler
                                │
                              res

如果你正在准备 Node.js / Express 面试 ,下一步最值得搞懂的是 app.use()、Router、中间件匹配规则,以及 Express 4/5 的 next() / 异步错误处理机制

相关推荐
时速GEO系统1 小时前
初元 AI V1.1 版本升级,首个正式版发布一周・实现生成、部署、上线、优化全流程自动化闭环
人工智能·数据挖掘·node.js
aramae2 小时前
MySQL内置函数(7)
开发语言·笔记·后端·mysql·其他
徐小夕3 小时前
存量.doc 文档怎么办?JitWord 开源转换库打通旧文档到在线协同全链路
后端·架构·github
ee又momo3 小时前
一次 JWT Token 过期续期的踩坑记录
后端
二月龙3 小时前
App 瘦身实战:图片、so 库、资源压缩,安装包体积减小 40%
后端
大勇前进3 小时前
Kotlin 协程实战避坑:90% 的人都会踩的生命周期泄漏问题
后端
苏三说技术3 小时前
Spring AI、LangChain4j、AgentScope、Embabel,哪个AI框架更好?
后端
烽学长4 小时前
(附源码)基于Springboot+vue的图书阅读分享系统的设计与实现
java·spring boot·后端
isfox4 小时前
Python 单例模式:一个类只能有一个实例,到底怎么实现?
后端