路由基础

路由是 Web 框架的核心功能之一,它决定了应用程序如何响应客户端对特定端点(URI)的请求。Express 提供了强大而灵活的路由系统,能够轻松处理各种 HTTP 方法和复杂的 URL 模式。本章将深入讲解路由的定义、参数获取、模块化组织等基础知识。

1. 什么是路由?

路由由三部分组成:HTTP 方法(如 GET、POST)、路径(如 /users)和处理函数(也称为控制器)。当客户端发送一个请求,Express 会根据请求的方法和路径匹配到对应的处理函数来生成响应。

在 Express 中,路由的基本形式如下:

app.METHOD(PATH, HANDLER);

其中:

  • app 是 Express 应用实例。
  • METHOD 是小写的 HTTP 方法,例如 getpostputdelete 等。
  • PATH 是服务器上的路径(可以是字符串、字符串模式或正则表达式)。
  • HANDLER 是匹配到路由时执行的函数。

2. 基本路由方法

Express 为常见的 HTTP 方法提供了对应的方法,最常用的包括:

// GET 请求 - 获取资源
app.get('/users', (req, res) => {
  res.send('获取用户列表');
});

// POST 请求 - 创建资源
app.post('/users', (req, res) => {
  res.send('创建新用户');
});

// PUT 请求 - 更新资源(全量替换)
app.put('/users/:id', (req, res) => {
  res.send(`更新用户 ${req.params.id}`);
});

// DELETE 请求 - 删除资源
app.delete('/users/:id', (req, res) => {
  res.send(`删除用户 ${req.params.id}`);
});

// PATCH 请求 - 部分更新资源
app.patch('/users/:id', (req, res) => {
  res.send(`部分更新用户 ${req.params.id}`);
});

// 还有 app.all() 用于匹配所有 HTTP 方法
app.all('/secret', (req, res) => {
  res.send('所有方法都会响应');
});

3. 路由路径

路由路径定义了请求的 URL 匹配规则。Express 支持三种方式定义路径:字符串、字符串模式和正则表达式。

3.1 字符串路径

最简单的形式,直接使用字符串匹配完整路径。

// 匹配根路径
app.get('/', (req, res) => { res.send('root'); });

// 匹配 /about
app.get('/about', (req, res) => { res.send('about'); });

3.2 字符串模式

路径字符串中可以包含一些特殊字符,如 ?+*(),它们基于正则表达式的子集。

// 匹配 acd 或 abcd
app.get('/ab?cd', (req, res) => { res.send('ab?cd'); });

// 匹配 abcd、abbcd、abbbcd 等
app.get('/ab+cd', (req, res) => { res.send('ab+cd'); });

// 匹配 abcd、abxcd、abRANDOMcd 等
app.get('/ab*cd', (req, res) => { res.send('ab*cd'); });

// 匹配 /abe 或 /abcde
app.get('/ab(cd)?e', (req, res) => { res.send('ab(cd)?e'); });

3.3 正则表达式路径

可以使用正则表达式进行更复杂的匹配,例如:

// 匹配任何路径中包含 "fly" 的请求
app.get(/fly/, (req, res) => { res.send('包含 fly'); });

// 匹配 /user/12345 或 /user/abcde(4个或更多数字/字母?)
app.get(/^\/user\/[a-zA-Z0-9]{4,}$/, (req, res) => { res.send('匹配用户模式'); });

4. 路由参数

路由参数是 URL 路径中的命名段,用于捕获在 URL 中的动态值。捕获的值会填充到 req.params 对象中,参数名作为其键。

定义路由参数时,在路径中使用 : 加参数名:

app.get('/users/:userId', (req, res) => {
  res.send(`用户 ID: ${req.params.userId}`);
});

// 多个参数
app.get('/users/:userId/books/:bookId', (req, res) => {
  res.json(req.params); // 例如 { userId: '123', bookId: '456' }
});

路由参数只能由字母、数字、下划线组成,它们的值只能是字符串。如果需要更精确的类型限制,可以在处理函数中进行类型转换或验证。

此外,连字符 - 和点 . 也可以被解释为普通字符,所以参数可以这样使用:

app.get('/flights/:from-:to', (req, res) => {
  res.send(`从 ${req.params.from} 到 ${req.params.to}`);
});

5. 查询参数

查询参数是 URL 中 ? 后面的键值对,Express 会自动解析它们并放入 req.query 对象中。

// 请求 GET /search?q=express&page=2
app.get('/search', (req, res) => {
  console.log(req.query); // { q: 'express', page: '2' }
  res.send(`搜索: ${req.query.q}, 页码: ${req.query.page}`);
});

如果没有提供查询参数,req.query 是一个空对象 {}。查询参数的值始终是字符串,如果需要数字需要手动转换。

6. 处理多种请求方法的链式路由

使用 app.route() 可以为同一个路径定义多个请求方法的处理,避免重复写路径,使代码更简洁。

app.route('/users')
  .get((req, res) => {
    res.send('获取用户列表');
  })
  .post((req, res) => {
    res.send('创建用户');
  })
  .put((req, res) => {
    res.send('更新用户');
  });

7. 路由模块化:express.Router

当应用变得复杂时,将所有路由定义在同一个文件中会变得难以维护。Express 提供了 express.Router 类来创建模块化、可挂载的路由处理程序,类似于“迷你应用”。

7.1 创建路由模块

例如,创建一个 routes/users.js 文件:

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

// 该路由级别的中间件,仅在 /users 下生效
router.use((req, res, next) => {
  console.log('Time: ', Date.now());
  next();
});

// 定义路由
router.get('/', (req, res) => {
  res.send('用户列表');
});

router.post('/', (req, res) => {
  res.send('创建用户');
});

router.get('/:id', (req, res) => {
  res.send(`用户详情: ${req.params.id}`);
});

module.exports = router;

7.2 在主应用中挂载路由模块

在主文件(如 app.js)中引入并使用该路由模块:

const userRouter = require('./routes/users');
app.use('/users', userRouter); // 将路由挂载到 /users 路径

现在,访问 /users/ 就会由 userRouter 处理。这种模式让代码结构清晰,便于团队协作和维护。

8. 路由处理函数与 next()

路由处理函数可以接受第三个参数 next,用于将控制权传递给下一个中间件或路由。这在需要多个处理函数处理同一个路由时非常有用。

const cb0 = (req, res, next) => {
  console.log('第一个处理函数');
  next(); // 调用下一个
};

const cb1 = (req, res, next) => {
  console.log('第二个处理函数');
  next();
};

app.get('/example', [cb0, cb1], (req, res) => {
  res.send('最后响应');
});

也可以将多个处理函数作为数组传递,或者混合数组和函数。

9. 404 处理

由于 Express 会按照定义顺序匹配路由,如果没有找到任何匹配的路由,应该返回 404。通常在所有路由之后添加一个中间件来处理 404:

// 放在所有路由之后
app.use((req, res, next) => {
  res.status(404).send('Sorry, cannot find that!');
});

10. 完整示例:用户路由模块

下面是一个结合了上述知识点的完整示例,包含路由参数、查询参数、链式路由和模块化。

app.js

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

app.use(express.json()); // 解析 JSON 请求体
app.use('/users', userRouter); // 挂载用户路由

// 根路由
app.get('/', (req, res) => {
  res.send('Home Page');
});

// 404 处理
app.use((req, res) => {
  res.status(404).send('404 Not Found');
});

app.listen(3000, () => console.log('Server running on port 3000'));

routes/users.js

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

// 模拟用户数据
let users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
];

// 获取所有用户,支持查询参数过滤(例如 ?name=Alice)
router.get('/', (req, res) => {
  const { name } = req.query;
  if (name) {
    const filtered = users.filter(u => u.name.includes(name));
    res.json(filtered);
  } else {
    res.json(users);
  }
});

// 获取单个用户
router.get('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const user = users.find(u => u.id === id);
  if (user) {
    res.json(user);
  } else {
    res.status(404).json({ error: 'User not found' });
  }
});

// 创建用户
router.post('/', (req, res) => {
  const newUser = {
    id: users.length + 1,
    name: req.body.name
  };
  users.push(newUser);
  res.status(201).json(newUser);
});

// 更新用户(全量更新)
router.put('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const user = users.find(u => u.id === id);
  if (!user) return res.status(404).json({ error: 'User not found' });

  user.name = req.body.name;
  res.json(user);
});

// 删除用户
router.delete('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const index = users.findIndex(u => u.id === id);
  if (index === -1) return res.status(404).json({ error: 'User not found' });

  users.splice(index, 1);
  res.status(204).send();
});

module.exports = router;
小结: 路由是 Express 应用的骨架。掌握路由的定义、参数获取、模块化组织,能够让你构建清晰且易于扩展的 Web 应用。下一章我们将深入探讨 Express 的中间件机制,了解如何在请求-响应周期中插入自定义逻辑。