Node.js: 路由与中间件

最后更新:2026-08-26

Charlie 的 API 项目最初只有 5 个路由,全部写在 app.js 里还算清晰。两个月后路由增长到 30 个,文件超过 500 行,改一个接口要翻半天。团队决定用 express.Router() 把路由按模块拆分——用户路由归 users.js,认证路由归 auth.js,自定义日志中间件抽到独立文件。拆分后每个文件职责单一,维护效率大幅提升。

你将学到:


1. 1 express.Router() 模块化路由

当路由数量增多,把所有路由堆在 app.js 里会导致文件臃肿、难以协作。express.Router() 可以创建独立的路由实例,再挂载到应用的某个路径前缀上。

特性 app 直接定义路由 Router 模块化路由
文件组织 全部在 app.js 按模块拆分到独立文件
路径前缀 每条路由写完整路径 挂载时统一设置前缀
团队协作 多人改同一文件 各模块独立维护
可复用性 高,可跨项目复用

▶ 示例:创建独立的用户路由模块

JAVASCRIPT
// routes/users.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
  res.json({ users: ['Alice', 'Bob'] });
});

router.get('/:id', (req, res) => {
  res.json({ user: req.params.id });
});

module.exports = router;
▶ 试一试

▶ 示例:在 app.js 中挂载路由模块

JAVASCRIPT
// app.js
const express = require('express');
const app = express();
const userRouter = require('./routes/users');

app.use('/users', userRouter);

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
▶ 试一试

访问 /users 匹配 router.get('/'),访问 /users/42 匹配 router.get('/:id')



2. 2 路由参数、查询字符串与请求体

Express 提供三种方式接收客户端数据,适用场景各不相同。

数据来源 获取方式 URL 示例 典型用途
路由参数 req.params /users/42 标识具体资源
查询字符串 req.query /users?role=admin 过滤、排序、搜索
请求体 req.body POST body 提交表单/JSON 数据

▶ 示例:路由参数 :id

JAVASCRIPT
app.get('/products/:id', (req, res) => {
  const productId = req.params.id;
  res.json({ productId });
});
▶ 试一试

▶ 示例:查询字符串 req.query

JAVASCRIPT
app.get('/products', (req, res) => {
  const { category, sort, page } = req.query;
  res.json({ category, sort, page: page || 1 });
});
▶ 试一试

访问 /products?category=electronics&sort=price&page=2req.query{ category: 'electronics', sort: 'price', page: '2' }

▶ 示例:请求体 req.body 与 express.json()

JAVASCRIPT
app.use(express.json());

app.post('/products', (req, res) => {
  const { name, price } = req.body;
  res.status(201).json({ name, price });
});
▶ 试一试

注意: req.queryreq.params 的值都是字符串类型,需要手动转换数字。



3. 3 中间件基础与 next() 机制

中间件是 Express 的核心概念——每个请求都经过一条中间件链,每个中间件可以读取请求、修改响应,或把控制权交给下一个中间件。

100%
graph LR
  A[Request] --> B[morgan 日志]
  B --> C[express.json 解析]
  C --> D[自定义 auth 中间件]
  D --> E[路由处理函数]
  E --> F[Response]
  D -->|next error| G[错误处理中间件]

▶ 示例:最简单的自定义中间件

JAVASCRIPT
function logger(req, res, next) {
  console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
  next();
}

app.use(logger);
▶ 试一试
规则 说明
必须调用 next() 否则请求挂起,客户端永远收不到响应
调用顺序即执行顺序 先注册的中间件先执行
app.use() 全局生效 app.use('/api', ...) 只对 /api 路径生效
next('route') 跳过当前路由的剩余中间件


4. 4 第三方中间件集成

社区提供了大量现成中间件,安装后一行代码即可启用。

中间件 用途 安装命令
cors 解决跨域请求问题 npm install cors
morgan HTTP 请求日志记录 npm install morgan
helmet 设置安全响应头 npm install helmet
express-rate-limit 请求频率限制 npm install express-rate-limit
cookie-parser 解析 Cookie npm install cookie-parser

▶ 示例:集成 cors、morgan、helmet

JAVASCRIPT
const cors = require('cors');
const morgan = require('morgan');
const helmet = require('helmet');

app.use(helmet());
app.use(cors());
app.use(morgan('combined'));
app.use(express.json());
▶ 试一试

提示: 安全相关中间件(helmet)建议放在最前面,日志中间件紧随其后。



5. 5 错误处理中间件

Express 约定:拥有 4 个参数 (err, req, res, next) 的函数就是错误处理中间件。只要任意中间件调用 next(err),就会跳过后续普通中间件,直接进入错误处理中间件。

对比项 普通中间件 错误处理中间件
参数个数 3 个 (req, res, next) 4 个 (err, req, res, next)
触发方式 请求到达时按序执行 next(err) 触发
注册位置 任意位置 必须放最后
能否多个 可以 可以,按序执行

▶ 示例:全局错误处理中间件

JAVASCRIPT
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: err.message || 'Internal Server Error'
  });
});
▶ 试一试

▶ 示例:在路由中触发错误

JAVASCRIPT
app.get('/admin', (req, res, next) => {
  const err = new Error('Access denied');
  err.status = 403;
  next(err);
});
▶ 试一试

6. 6 Mermaid:Router 模块化路由结构

100%
graph TD
  APP[app.js] --> UR["/users → userRouter"]
  APP --> AR["/auth → authRouter"]
  APP --> MW["middleware/logger.js"]
  UR --> U1["GET / → 用户列表"]
  UR --> U2["GET /:id → 用户详情"]
  UR --> U3["POST / → 创建用户"]
  AR --> A1["POST /login → 登录"]
  AR --> A2["POST /register → 注册"]
  MW --> ML["日志中间件"]


7. 7 综合示例:模块化路由 API

本示例把用户路由、认证路由、日志中间件拆到独立文件,最终在 app.js 中整合。

▶ 示例:middleware/logger.js — 自定义日志中间件

JAVASCRIPT
function logger(req, res, next) {
  const start = Date.now();
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`);
  });
  next();
}

module.exports = logger;
▶ 试一试

▶ 示例:routes/users.js — 用户路由模块

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

let users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
];

router.get('/', (req, res) => {
  res.json(users);
});

router.get('/:id', (req, res, next) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) {
    const err = new Error('User not found');
    err.status = 404;
    return next(err);
  }
  res.json(user);
});

router.post('/', (req, res) => {
  const { name } = req.body;
  if (!name) {
    return res.status(400).json({ error: 'Name is required' });
  }
  const newUser = { id: users.length + 1, name };
  users.push(newUser);
  res.status(201).json(newUser);
});

module.exports = router;
▶ 试一试

▶ 示例:routes/auth.js — 认证路由模块

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

router.post('/login', (req, res) => {
  const { username, password } = req.body;
  if (!username || !password) {
    return res.status(400).json({ error: 'Username and password required' });
  }
  res.json({ message: 'Login successful', token: 'mock-jwt-token' });
});

router.post('/register', (req, res) => {
  const { username, email, password } = req.body;
  if (!username || !email || !password) {
    return res.status(400).json({ error: 'All fields are required' });
  }
  res.status(201).json({ message: 'Registration successful' });
});

module.exports = router;
▶ 试一试

▶ 示例:app.js — 整合入口

JAVASCRIPT
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const morgan = require('morgan');

const logger = require('./middleware/logger');
const userRouter = require('./routes/users');
const authRouter = require('./routes/auth');

const app = express();

app.use(helmet());
app.use(cors());
app.use(morgan('combined'));
app.use(express.json());
app.use(logger);

app.use('/users', userRouter);
app.use('/auth', authRouter);

app.use((req, res) => {
  res.status(404).json({ error: 'Route not found' });
});

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: err.message || 'Internal Server Error'
  });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
▶ 试一试

运行测试:

BASH
node app.js
TEXT 📖 仅展示
Server running on port 3000
GET /users 200 3ms
GET /users/1 200 2ms
POST /auth/login 200 5ms

❓ 常见问题

Q next() 不调用会怎样?
A 请求会挂起,客户端永远收不到响应。必须在中间件中调用 next() 将控制权传递,或用 res.end()/res.json() 等方法直接结束响应。
Q 路由参数和查询参数有什么区别?
A 路由参数是 URL 路径的一部分,如 /users/:id 中的 42,用 req.params.id 获取;查询参数是 ? 后的键值对,如 /users?role=admin,用 req.query.role 获取。前者标识资源,后者过滤资源。
Q 中间件的注册顺序重要吗?
A 非常重要。Express 按注册顺序依次执行中间件。例如 express.json() 必须在需要读取 req.body 的路由之前注册,否则 req.body 为 undefined。
Q 如何写一个全局的错误处理?
A 定义一个 4 参数函数 (err, req, res, next),用 app.use() 注册在所有路由和中间件之后。任何中间件调用 next(err) 都会跳转到此处理函数。
Q cors 中间件解决什么问题?
A 浏览器的同源策略会阻止跨域请求。cors 中间件在响应头中添加 Access-Control-Allow-Origin 等字段,允许指定来源的前端访问 API。
Q express.Router() 和直接用 app.get() 有什么区别?
A Router 创建独立路由实例,可挂载到任意路径前缀,适合模块化拆分;app.get() 直接在应用实例上注册,适合简单项目。两者功能等价,区别在于组织方式。
Q 如何在中间件中修改 req 对象传递数据给后续处理?
A 直接在 req 上挂载属性,如 req.user = { id: 1 },后续中间件和路由即可通过 req.user 读取。这是 Express 中间件间通信的常用模式。

📖 小节


📝 作业

  1. 完成本课所有代码示例,确保每个示例都能正确运行
  2. 修改综合示例,添加自己的扩展功能
  3. 查阅官方文档,找出本课未涉及的1-2个API并编写测试代码
  4. 思考:在实际项目中,你会如何应用本课学到的知识?
  5. 尝试将本课知识与前面课程的内容结合,构建一个小项目
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏