3、中间件机制:中间件概念、应用级中间件、路由级中间件、错误处理中间件、内置中间件(express.static)

说到 Express,我最想跟你聊的就是中间件。这玩意儿说白了,就是 Express 的灵魂。没有中间件,Express 就是个空壳子。

我记得刚接触 Node.js 那会儿,对中间件的理解特别肤浅——不就是个函数吗?后来在项目中踩了不少坑,才真正明白它的威力。今天咱们就把这块掰开揉碎了讲清楚。

3.1 中间件到底是什么?

中间件,英文叫 Middleware。你可以把它想象成一道流水线上的工人。

每个请求进来,就像一件待加工的产品。它要经过一道道工序:有人负责检查身份(认证中间件),有人负责记录日志(日志中间件),有人负责解析数据(body-parser),最后才到真正的业务处理。

每个中间件都能做三件事:

  • 执行一些代码
  • 修改请求和响应对象
  • 结束请求-响应循环
  • 调用下一个中间件

嗯,这里要注意——如果你不调用 next(),请求就卡在那儿了。我刚开始写代码时就犯过这错,页面一直转圈,查了半天才发现是忘了调 next。

核心概念:中间件函数的签名是 (req, res, next)。req 是请求对象,res 是响应对象,next 是下一个中间件函数。

3.2 应用级中间件

应用级中间件,就是绑定在 app 实例上的中间件。用 app.use()app.METHOD() 来挂载。

我个人习惯把全局通用的中间件都放在入口文件最前面。比如日志记录、CORS 处理、请求体解析这些。

const express = require('express');
const app = express();

// 应用级中间件 - 记录请求日志
app.use((req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next(); // 别忘了调 next
});

// 应用级中间件 - 解析 JSON 请求体
app.use(express.json());

// 应用级中间件 - 只对 /api 开头的路由生效
app.use('/api', (req, res, next) => {
  console.log('API 请求来了');
  next();
});

app.get('/hello', (req, res) => {
  res.send('Hello World');
});

app.listen(3000);

你想想看,如果每个路由都写一遍日志逻辑,那得多痛苦?应用级中间件就是用来解决这种重复劳动的。

我的经验:应用级中间件的顺序很重要。比如 CORS 中间件一定要放在最前面,否则跨域请求在早期就被拦截了。我曾经因为这个排查了整整一下午。

3.3 路由级中间件

路由级中间件跟应用级中间件用法几乎一样,唯一的区别是——它绑定在 express.Router() 实例上。

说白了,就是给一组路由单独配置中间件。这在大型项目中特别有用。

const express = require('express');
const router = express.Router();

// 路由级中间件 - 验证用户是否登录
router.use((req, res, next) => {
  if (!req.headers.authorization) {
    return res.status(401).json({ error: '未授权' });
  }
  next();
});

// 这个路由会被上面的中间件拦截检查
router.get('/profile', (req, res) => {
  res.json({ name: '张三', age: 28 });
});

// 这个路由不需要登录,所以放在中间件之前
router.get('/public', (req, res) => {
  res.json({ message: '公开信息' });
});

module.exports = router;

我在项目中遇到过这样的场景:后台管理系统有几十个接口,其中大部分需要管理员权限。用路由级中间件,我只需要在 admin 路由上挂一个权限校验中间件,所有子路由自动继承。省事多了。

注意:路由级中间件只对当前 Router 实例下的路由生效。如果你在 app.js 里用 app.use('/admin', adminRouter),那 adminRouter 里的中间件只处理 /admin 开头的请求。

3.4 错误处理中间件

错误处理中间件比较特殊。它有四个参数:(err, req, res, next)。多了一个 err 参数。

Express 会识别参数个数。只要中间件函数有四个参数,它就自动变成错误处理中间件。

// 普通路由
app.get('/user/:id', (req, res, next) => {
  const user = findUser(req.params.id);
  if (!user) {
    // 手动触发错误
    const err = new Error('用户不存在');
    err.status = 404;
    return next(err); // 把错误传给错误处理中间件
  }
  res.json(user);
});

// 错误处理中间件 - 必须放在所有路由之后
app.use((err, req, res, next) => {
  console.error('发生错误:', err.message);
  
  // 设置状态码
  const status = err.status || 500;
  
  res.status(status).json({
    error: {
      message: err.message,
      status: status
    }
  });
});

我曾经犯过一个低级错误——把错误处理中间件放在了路由前面。结果呢?错误根本捕获不到,直接返回了 Express 默认的 HTML 错误页面。嗯,顺序真的很重要。

最佳实践:错误处理中间件一定要放在所有路由和中间件的最后面。它就像一张安全网,兜住所有漏网之鱼。

3.5 内置中间件 - express.static

Express 自带了一些中间件,其中最常用的就是 express.static。它用来托管静态文件。

说白了,就是让用户能直接访问你服务器上的图片、CSS、JS 文件。

const express = require('express');
const path = require('path');
const app = express();

// 托管 public 目录下的静态文件
app.use(express.static('public'));

// 也可以指定虚拟路径前缀
app.use('/static', express.static(path.join(__dirname, 'public')));

// 现在访问 http://localhost:3000/static/logo.png
// 就能拿到 public/logo.png 文件

你想想看,如果没有这个中间件,你得手动写路由来处理每个静态文件的请求。那得写到猴年马月去?

我的建议:生产环境中,最好用 Nginx 来托管静态文件,性能比 Express 好得多。express.static 更适合开发环境或小型项目。

3.6 中间件的执行顺序

这个问题我经常被问到。中间件的执行顺序其实很简单——按照挂载顺序从上到下执行。

但有几个细节要注意:

  • 如果中间件不调 next(),后面的中间件就不会执行
  • 如果中间件调了 res.send()res.json(),请求就结束了
  • 错误处理中间件只在调用了 next(err) 时才会触发
中间件类型 挂载方式 执行时机
应用级 app.use() 所有请求
路由级 router.use() 匹配的路由
错误处理 app.use(err, req, res, next) 发生错误时
内置中间件 express.static() 静态文件请求

好了,中间件这块就聊到这儿。说白了,中间件就是 Express 的插件机制。理解了这个,你就能随心所欲地扩展你的 API 服务了。下一章咱们聊聊路由的高级用法,到时候你会更深刻地体会到中间件的妙处。