Dark Dwarf Blog background

Express 路由

Express.js 路由

1. 路由基本语法

a.a. 路由的结构

一个基本的路由定义由三部分组成:app.METHOD(PATH, HANDLER)

  1. app: express 的一个实例。
  2. METHOD: 一个小写的 HTTP 请求方法,如 get, post, put, delete 等。
  3. PATH: 服务器上的路径(端点)。
  4. HANDLER: 当路由匹配时执行的处理器函数。
const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Homepage!');
});

b.b. 路由路径

i.i. 字符串路径

这是最简单的形式,要求 URL 路径与定义的字符串完全匹配。

// 仅匹配 /about
app.get('/about', handler);

// 仅匹配 /random.text
app.get('/random.text', handler);

ii.ii. 路径模式

Express 借助 path-to-regexp 库,允许在路径字符串中使用一些特殊字符来实现更灵活的匹配:

  • ?: 将其前面的字符标记为可选。/ab?cd 会匹配 acdabcd
  • +: 匹配其前面字符的一次或多次重复。/ab+cd 会匹配 abcd, abbcd, abbbcd 等。
  • *: 匹配其前面字符的任意次数(包括零次)重复,或作为通配符匹配任意字符序列。ab*cd 会匹配 abcd, abxcd, abRANDOMcd 等。
  • (): 将一组字符标记为群组。例如,/fly(ing)?/ 会匹配 /fly/flying
// 匹配 /users 和 /user
app.get('/users?', handler);

// 匹配所有路径,通常用作 404 错误处理,必须放在最后
app.get('* ', (req, res) => {
  res.status(404).send('Page Not Found');
});

iii.iii. 正则表达式

为了实现最复杂的匹配逻辑,可以直接使用正则表达式作为路径。

// 匹配任何包含 "a" 的路径
app.get(/a/, handler);

// 匹配 butterfly 和 dragonfly,但不匹配 butterflyman, dragonflyman 等
app.get(/.*fly$/, handler);

iv.iv. 路由顺序

Express 按顺序匹配路由。请求将由第一个匹配其路径和方法的规则来处理。因此,路由的定义顺序至关重要。

// 错误示范:通配符路由在前
// 所有 GET 请求都会被这个路由捕获,后面的 /about 永远不会被匹配
app.get('/:generic', (req, res) => {
    res.send(`Generic page for: ${req.params.generic}`)
});

app.get('/about', (req, res) => {
    res.send('About page'); // 这个路由永远不会执行
});

// 正确示范:将更具体的路由放在前面
app.get('/about', (req, res) => {
    res.send('About page');
});

app.get('/:generic', (req, res) => {
    res.send(`Generic page for: ${req.params.generic}`)
});

c.c. 路由参数

路由参数是 URL 中用于捕获动态值的命名段。这些值会被填充到 req.params 对象中。通过在路径段前加上冒号 : 来定义路由参数。

// :userId 是一个路由参数
app.get('/users/:userId', (req, res) => {
  // 如果 URL 是 /users/123, req.params 将是 { userId: '123' }
  res.send(`User profile for user ID: ${req.params.userId}`);
});

一个路径中可以包含多个路由参数。

// 匹配 /users/123/books/456
app.get('/users/:userId/books/:bookId', (req, res) => {
  // req.params 将是 { userId: '123', bookId: '456' }
  const { userId, bookId } = req.params;
  res.send(`User ${userId} requested book ${bookId}.`);
});

d.d. 查询字符串

查询字符串是 URL 中 ? 之后的部分,用于传递可选的键值对。它们不属于路由路径的一部分,而是作为附加数据。

Express 会自动解析查询字符串,并将其填充到 req.query 对象中。

// 请求 URL: /search?q=express&limit=10
app.get('/search', (req, res) => {
  // req.query 将是 { q: 'express', limit: '10' }
  const { q, limit } = req.query;
  res.send(`Searching for "${q}" with a limit of ${limit}.`);
});

如果同一个键在查询字符串中出现多次,Express 会将这些值收集到一个数组中。

// 请求 URL: /articles?sort=date&sort=likes
app.get('/articles', (req, res) => {
  // req.query 将是 { sort: ['date', 'likes'] }
  res.send(`Sorting by: ${req.query.sort.join(', ')}`);
});

2. 路由处理器

处理器是可以处理请求的函数。一个路由可以有一个或多个处理器。

a.a. 多个处理器

我们可以为一个路由提供多个回调函数作为处理器。它们就像微型中间件,必须调用 next() 才能将控制权传递给下一个处理器。

const handler1 = (req, res, next) => {
  console.log('First handler: validating request...');
  req.isValid = true; // 可以向 req 对象添加属性
  next();
}

const handler2 = (req, res) => {
  console.log('Second handler: sending response...');
  res.send(`Request is valid: ${req.isValid}`);
}

app.get('/chained', [handler1, handler2]);

b.b. app.route() 方法

这是一个非常有用的工具,可以为单个路由路径创建可链接的处理器。它避免了为同一个路径重复书写多次 app.get, app.post 等。

app.route('/book')
  .get((req, res) => {
    res.send('Get a random book');
  })
  .post((req, res) => {
    res.send('Add a book');
  })
  .put((req, res) => {
    res.send('Update the book');
  });

3. 模块化路由

a.a. 引入

随着应用规模的增长,将所有路由都放在一个文件中会变得难以管理。express.Router 是一个可插拔的、迷你的 Express 应用,它允许我们将路由逻辑拆分到不同的文件中,实现模块化。这种架构方式有如下优点:

  • 组织性: 将相关的路由(如所有与用户相关的路由)分组到同一个文件中。
  • 可重用性: 可以在不同的主应用中挂载和重用路由模块。
  • 关注点分离: 主文件 app.js 只负责配置和挂载,而路由文件只关心具体的路由逻辑。

b.b. 创建和使用 Router

  1. 创建路由文件:
// routes/userRouter.js
const express = require('express');
const router = express.Router();

// 中间件,仅对该 router 生效
router.use((req, res, next) => {
  console.log('Time: ', Date.now());
  next();
});

// 定义用户相关的路由
// 注意:这里的路径是相对于挂载点的
router.get('/', (req, res) => {
  res.send('Users homepage');
});

router.get('/:userId', (req, res) => {
  res.send(`Profile for user ${req.params.userId}`);
});

module.exports = router;
  1. 在主文件中挂载 Router:
// app.js
const express = require('express');
const app = express();
const userRouter = require('./routes/userRouter');

// 将所有 /users 开头的请求都交给 userRouter 处理
app.use('/users', userRouter);

app.listen(3000);

挂载后,路由的最终路径是挂载点路径路由文件内路径的组合。

  • app.use('/users', userRouter)
  • router.get('/:userId', ...)

… 两者结合起来,最终匹配的完整路径是 /users/:userId