Node系列 · Express:中间件

Node系列 · Express:中间件

中间件(Middleware)是 Express 的灵魂------每个请求都会经过一连串中间件处理。理解中间件的"链式调用"和 next() 的控制权移交,就理解了 Express 90% 的工作方式。

一、什么是中间件

中间件是请求处理链上的一个环节------它能:

  • 读取 req(请求对象)并加工
  • 改写 res(响应对象)
  • 决定是否调用 next() 移交控制权
  • 提前 res.send() 结束响应

#mermaid-svg-P8tvvxE18tbVlpAc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-P8tvvxE18tbVlpAc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-P8tvvxE18tbVlpAc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-P8tvvxE18tbVlpAc .error-icon{fill:#552222;}#mermaid-svg-P8tvvxE18tbVlpAc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-P8tvvxE18tbVlpAc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-P8tvvxE18tbVlpAc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-P8tvvxE18tbVlpAc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-P8tvvxE18tbVlpAc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-P8tvvxE18tbVlpAc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-P8tvvxE18tbVlpAc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-P8tvvxE18tbVlpAc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-P8tvvxE18tbVlpAc .marker.cross{stroke:#333333;}#mermaid-svg-P8tvvxE18tbVlpAc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-P8tvvxE18tbVlpAc p{margin:0;}#mermaid-svg-P8tvvxE18tbVlpAc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-P8tvvxE18tbVlpAc .cluster-label text{fill:#333;}#mermaid-svg-P8tvvxE18tbVlpAc .cluster-label span{color:#333;}#mermaid-svg-P8tvvxE18tbVlpAc .cluster-label span p{background-color:transparent;}#mermaid-svg-P8tvvxE18tbVlpAc .label text,#mermaid-svg-P8tvvxE18tbVlpAc span{fill:#333;color:#333;}#mermaid-svg-P8tvvxE18tbVlpAc .node rect,#mermaid-svg-P8tvvxE18tbVlpAc .node circle,#mermaid-svg-P8tvvxE18tbVlpAc .node ellipse,#mermaid-svg-P8tvvxE18tbVlpAc .node polygon,#mermaid-svg-P8tvvxE18tbVlpAc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-P8tvvxE18tbVlpAc .rough-node .label text,#mermaid-svg-P8tvvxE18tbVlpAc .node .label text,#mermaid-svg-P8tvvxE18tbVlpAc .image-shape .label,#mermaid-svg-P8tvvxE18tbVlpAc .icon-shape .label{text-anchor:middle;}#mermaid-svg-P8tvvxE18tbVlpAc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-P8tvvxE18tbVlpAc .rough-node .label,#mermaid-svg-P8tvvxE18tbVlpAc .node .label,#mermaid-svg-P8tvvxE18tbVlpAc .image-shape .label,#mermaid-svg-P8tvvxE18tbVlpAc .icon-shape .label{text-align:center;}#mermaid-svg-P8tvvxE18tbVlpAc .node.clickable{cursor:pointer;}#mermaid-svg-P8tvvxE18tbVlpAc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-P8tvvxE18tbVlpAc .arrowheadPath{fill:#333333;}#mermaid-svg-P8tvvxE18tbVlpAc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-P8tvvxE18tbVlpAc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-P8tvvxE18tbVlpAc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-P8tvvxE18tbVlpAc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-P8tvvxE18tbVlpAc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-P8tvvxE18tbVlpAc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-P8tvvxE18tbVlpAc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-P8tvvxE18tbVlpAc .cluster text{fill:#333;}#mermaid-svg-P8tvvxE18tbVlpAc .cluster span{color:#333;}#mermaid-svg-P8tvvxE18tbVlpAc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-P8tvvxE18tbVlpAc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-P8tvvxE18tbVlpAc rect.text{fill:none;stroke-width:0;}#mermaid-svg-P8tvvxE18tbVlpAc .icon-shape,#mermaid-svg-P8tvvxE18tbVlpAc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-P8tvvxE18tbVlpAc .icon-shape p,#mermaid-svg-P8tvvxE18tbVlpAc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-P8tvvxE18tbVlpAc .icon-shape .label rect,#mermaid-svg-P8tvvxE18tbVlpAc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-P8tvvxE18tbVlpAc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-P8tvvxE18tbVlpAc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-P8tvvxE18tbVlpAc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} next
next
不 next 直接 res.send
HTTP 请求
中间件 1

日志
中间件 2

解析 body
路由处理

业务
HTTP 响应

二、三种中间件

2.1 普通中间件(3 参数)

javascript:normal-middleware.js 复制代码
function logger(req, res, next) {
  console.log(`${req.method} ${req.url}`);
  next();   // 必须调用,否则请求会卡住
}

app.use(logger);

参数 (req, res, next)------next 是函数,调用它把控制权交给下一个中间件。

2.2 错误处理中间件(4 参数)

javascript:error-middleware.js 复制代码
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: '服务器内部错误' });
});

4 个参数 (多一个 err)让 Express 把它识别为错误中间件。要触发错误中间件,需要在前面调 next(err)

2.3 路由中间件(绑定到特定路径)

javascript:route-middleware.js 复制代码
// 仅 /api/* 路径生效
app.use('/api', (req, res, next) => {
  console.log('API 请求');
  next();
});

也可以直接用 app.get(path, handler) / app.post(path, handler)------本质也是中间件。

三、next() 的三种行为

3.1 正常 next

javascript:normal-middleware.js 复制代码
app.use((req, res, next) => {
  console.log('中间件 A');
  next();   // 移交给下一个
});

3.2 next(err) 跳到错误处理

javascript:error-middleware.js 复制代码
app.use((req, res, next) => {
  if (!req.headers.authorization) {
    return next(new Error('未授权'));
  }
  next();
});

app.use((err, req, res, next) => {
  res.status(401).json({ error: err.message });
});

3.3 不调 next(直接结束)

javascript:terminate-middleware.js 复制代码
app.use((req, res, next) => {
  if (req.path === '/favicon.ico') {
    return res.status(204).end();   // 直接结束,不 next
  }
  next();
});

::: warning

不调 next() 也不 res.send() ------请求会卡住直到超时。Express 默认 0 字节响应 + 超时。一定要保证每个分支都有出口。

:::

四、中间件顺序

Express 按 app.use() 注册顺序执行:

javascript:middleware-order.js 复制代码
app.use(logger);            // 1. 日志
app.use(express.json());    // 2. 解析 JSON body
app.use('/api', authCheck); // 3. /api/* 鉴权
app.use('/api/users', userRoutes);  // 4. 用户路由
app.use(notFound);          // 5. 404
app.use(errorHandler);      // 6. 错误处理

每条规则:

顺序 中间件 作用
1 日志 / 性能监控 记录每个请求
2 body 解析 express.json / express.urlencoded
3 CORS / 安全头 跨域、安全策略
4 会话 / 鉴权 检查登录状态
5 业务路由 实际 API
6 404 处理 未匹配路径
7 错误处理 捕获前面抛出的异常

五、实战:日志中间件

javascript:custom-logger.js 复制代码
function requestLogger(req, res, next) {
  const start = Date.now();

  // 在响应结束时打印耗时
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.url} ${res.statusCode} - ${duration}ms`);
  });

  next();
}

app.use(requestLogger);

六、实战:鉴权中间件

javascript:auth-middleware.js 复制代码
function requireAuth(req, res, next) {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) {
    return res.status(401).json({ error: '未登录' });
  }

  try {
    const payload = jwt.verify(token, SECRET);
    req.user = payload;   // 挂到 req 上供后续中间件用
    next();
  } catch (err) {
    next(err);   // 触发错误中间件
  }
}

// 用法:保护 /api/private/*
app.use('/api/private', requireAuth);

七、async 中间件

Express 4.x 不会自动捕获 async 中间件抛出的错误。Express 5+ 才原生支持:

javascript:async-middleware.js 复制代码
// Express 5(即将 GA):async 直接抛错会自动被错误中间件捕获
app.get('/api/users', async (req, res) => {
  const users = await User.findAll();
  res.json(users);
});

// Express 4.x:需要包装一层 catch
function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

app.get('/api/users', asyncHandler(async (req, res) => {
  const users = await User.findAll();
  res.json(users);
}));

八、最佳实践

场景 推荐
通用中间件 抽成单独文件,统一在 app.js 注册
鉴权中间件 挂在 /api/* 路径下,业务路由才生效
错误处理 始终放在最后注册
async 中间件 Express 5 直接写 async;4.x 用 wrapper
第三方中间件 app.use() 引入前先看文档
调试中间件 morgan(HTTP 日志)/ helmet(安全头)

九、小结

  • 中间件是请求处理链的一个环节;签名 (req, res, next)
  • 错误中间件 特殊:4 个参数 (err, req, res, next)
  • next() 移交;next(err) 跳错误处理;不调 next 直接 res.send 结束
  • 注册顺序就是执行顺序:日志 → body → CORS → 鉴权 → 路由 → 404 → 错误
  • async 中间件:Express 5 原生支持;4.x 用 asyncHandler wrapper
  • 中间件的本质是函数组合(chain of responsibility),是 Express 灵活性的来源
相关推荐
2601_962122971 小时前
Node.js的解释
node.js
屋昂仼2 小时前
NestJS 从入门到企业级实战(一):环境搭建与项目结构全解析
node.js
coderCN16 小时前
Nodejs 响应头和请求头
后端·node.js
__zRainy__18 小时前
Node系列 · ORM:log4js 日志记录
node.js·log4j
LayZhangStrive21 小时前
后端开发中间件 - Nacos下载并快速使用教程
中间件·nacos
念何架构之路1 天前
Gin内置中间件详解
中间件·gin
秋秋小事1 天前
node 导入与导出
node.js
秋秋小事1 天前
node event模块
node.js
晴天161 天前
Node.js 中 `npm install` 命令分析-Day30
前端·npm·node.js