您现在的位置是:网站首页 > 路由系统与URL处理文章详情

路由系统与URL处理

路由系统与URL处理是Express框架的核心功能之一,它决定了如何响应客户端对不同URL的请求。通过路由系统,开发者可以定义应用程序的端点(URI)以及对这些端点的响应方式。URL处理则涉及解析、匹配和响应请求的路径和参数,是构建动态Web应用的基础。

路由基础

Express的路由系统基于HTTP方法和URL路径进行匹配。每个路由可以包含一个或多个处理函数,这些函数在路由匹配时执行。基本的路由定义如下:

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

// 基本GET路由
app.get('/', (req, res) => {
  res.send('首页');
});

// 基本POST路由
app.post('/submit', (req, res) => {
  res.send('表单已提交');
});

路由方法对应于HTTP方法,常见的有getpostputdelete等。路由路径可以是字符串、字符串模式或正则表达式。

路由路径与参数

路由路径可以包含动态参数,这些参数会被捕获并存储在req.params对象中。动态参数通过冒号:标识:

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

// 多个参数
app.get('/posts/:year/:month', (req, res) => {
  res.send(`文章发布于 ${req.params.year}年${req.params.month}月`);
});

参数名称只能包含字母、数字和下划线。Express还支持通过正则表达式定义更复杂的路径匹配规则:

// 使用正则表达式限制参数格式
app.get('/products/:id(\\d+)', (req, res) => {
  res.send(`产品ID必须是数字: ${req.params.id}`);
});

路由处理程序

路由处理程序可以是单个函数,也可以是多个函数组成的数组或链式调用。多个处理函数会按顺序执行,除非某个函数终止了请求-响应周期:

// 多个处理函数
app.get('/example',
  (req, res, next) => {
    console.log('第一个处理函数');
    next(); // 控制权传递给下一个函数
  },
  (req, res) => {
    res.send('第二个处理函数响应');
  }
);

// 使用数组定义处理函数
const middleware1 = (req, res, next) => { /* ... */ };
const middleware2 = (req, res, next) => { /* ... */ };
app.get('/array', [middleware1, middleware2]);

路由模块化

随着应用规模扩大,可以将路由组织到单独的模块中。Express的Router类可以创建模块化的路由处理程序:

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

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

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

module.exports = router;

// 主文件
const userRouter = require('./routes/users');
app.use('/users', userRouter);

app.use()方法将路由挂载到特定路径前缀下,使得路由结构更加清晰。

URL查询参数

除了路径参数,Express还自动解析URL中的查询字符串,结果存储在req.query对象中:

// 处理/search?q=express&page=2
app.get('/search', (req, res) => {
  const { q, page } = req.query;
  res.send(`搜索: ${q}, 页码: ${page || 1}`);
});

查询参数不需要在路由路径中声明,它们总是可选的。对于复杂的查询参数,可以使用qs模块进行深度解析:

// 启用复杂查询解析
app.set('query parser', 'extended');

路由中间件

路由可以应用特定的中间件,这些中间件只对特定路由生效。这在权限控制、数据验证等场景非常有用:

// 认证中间件
const authenticate = (req, res, next) => {
  if (req.headers.authorization) {
    next();
  } else {
    res.status(401).send('未授权');
  }
};

// 应用中间件到特定路由
app.get('/admin', authenticate, (req, res) => {
  res.send('管理员面板');
});

中间件可以精确控制到HTTP方法和路径,实现细粒度的请求处理。

路由路径匹配规则

Express的路由路径匹配遵循特定规则:

  • 字符串路径必须完全匹配(除了大小写不敏感)
  • ?+*()不是正则表达式字符,而是字符串的一部分
  • 连字符-和点号.按字面解释
  • 可以通过正则表达式实现更灵活的匹配
// 匹配/acd和/abcd
app.get('/ab?cd', (req, res) => {
  res.send('匹配/acd或/abcd');
});

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

// 匹配/abcd、/abxcd、/abANYcd等
app.get('/ab*cd', (req, res) => {
  res.send('匹配ab和cd之间的任意字符');
});

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

错误处理路由

Express提供了特殊的路由处理错误情况。错误处理中间件需要四个参数:

// 错误处理中间件
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('服务器错误');
});

// 异步错误处理示例
app.get('/async', async (req, res, next) => {
  try {
    await someAsyncOperation();
    res.send('成功');
  } catch (err) {
    next(err); // 传递给错误处理中间件
  }
});

错误处理路由应该定义在所有其他路由之后,作为最后的处理环节。

路由的响应方法

Express的响应对象res提供了多种发送响应的方法:

app.get('/response', (req, res) => {
  // 发送文本
  res.send('普通文本');
  
  // 发送JSON
  res.json({ message: 'JSON响应' });
  
  // 发送文件
  res.sendFile('/path/to/file.pdf');
  
  // 重定向
  res.redirect('/new-location');
  
  // 设置状态码
  res.status(404).send('未找到');
  
  // 设置响应头
  res.set('Content-Type', 'text/plain');
});

路由的链式调用

对于同一路径的不同HTTP方法,可以使用route()方法进行链式调用:

app.route('/book')
  .get((req, res) => {
    res.send('获取书籍信息');
  })
  .post((req, res) => {
    res.send('添加新书籍');
  })
  .put((req, res) => {
    res.send('更新书籍信息');
  });

这种方法使代码更加紧凑,特别适合RESTful API设计。

静态文件路由

Express内置了express.static中间件用于处理静态文件:

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

// 带虚拟路径前缀
app.use('/static', express.static('public'));

// 多个静态目录
app.use(express.static('public'));
app.use(express.static('files'));

静态文件中间件会按顺序查找文件,直到找到匹配的文件为止。

路由的加载顺序

Express按照路由定义的顺序进行匹配,先定义的路由优先匹配。这一点在处理通配符路由时特别重要:

// 这个路由会拦截所有/users开头的请求
app.get('/users*', (req, res) => {
  res.send('通用用户路由');
});

// 这个路由永远不会被匹配到
app.get('/users/:id', (req, res) => {
  res.send('特定用户路由');
});

正确的做法是将特定路由放在通用路由之前:

app.get('/users/:id', (req, res) => {
  res.send('特定用户路由');
});

app.get('/users*', (req, res) => {
  res.send('通用用户路由');
});

路由的性能考虑

路由匹配的性能对应用整体性能有重要影响:

  • 尽量减少通配符路由的使用
  • 将最常访问的路由放在前面
  • 对静态路由使用字符串匹配而非正则表达式
  • 避免在路由处理函数中进行同步阻塞操作
// 性能较差的写法
app.get(/^\/([a-z0-9-]+)$/, (req, res) => {
  // 复杂的正则匹配
});

// 性能更好的写法
app.get('/:slug([a-z0-9-]+)', (req, res) => {
  // 使用命名参数限制格式
});

路由与RESTful API设计

Express的路由系统非常适合实现RESTful API。典型的资源路由设计如下:

// 用户资源路由
app.route('/api/users')
  .get((req, res) => { /* 获取用户列表 */ })
  .post((req, res) => { /* 创建新用户 */ });

app.route('/api/users/:id')
  .get((req, res) => { /* 获取单个用户 */ })
  .put((req, res) => { /* 更新用户 */ })
  .delete((req, res) => { /* 删除用户 */ });

这种设计遵循了REST原则,使API结构清晰、易于理解。

路由的版本控制

对于长期维护的API,路由版本控制是常见需求。可以通过路径前缀或请求头实现:

// 路径前缀版本控制
app.use('/v1/users', v1UserRouter);
app.use('/v2/users', v2UserRouter);

// 请求头版本控制
app.use('/api/users', (req, res, next) => {
  const version = req.headers['api-version'] || 'v1';
  if (version === 'v1') return v1UserRouter(req, res, next);
  if (version === 'v2') return v2UserRouter(req, res, next);
  next();
});

版本控制策略应根据项目需求选择,保持一致性最重要。

路由的测试

测试路由时,可以使用supertest等库模拟HTTP请求:

const request = require('supertest');
const app = require('../app');

describe('GET /users', () => {
  it('应返回用户列表', async () => {
    const res = await request(app).get('/users');
    expect(res.statusCode).toEqual(200);
    expect(res.body).toBeInstanceOf(Array);
  });
});

测试应该覆盖各种边界情况,包括无效参数、缺失参数等场景。

路由的安全考虑

路由设计需要考虑多种安全因素:

  • 验证所有输入参数
  • 防范SQL注入
  • 限制敏感路由的访问
  • 使用HTTPS传输敏感数据
// 参数验证示例
app.get('/products', (req, res, next) => {
  const page = parseInt(req.query.page) || 1;
  if (page < 1) {
    return res.status(400).send('页码必须大于0');
  }
  next();
});

安全措施应该作为路由设计的一部分,而非事后补充。

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步