路由匹配:从 @Get('/users/:id') 到参数提取
在 NestJS 应用中,路由是连接客户端请求与服务端处理逻辑的桥梁。当我们编写 @Get('/users/:id') 这样的路由装饰器时,背后发生了什么?NestJS 是如何解析动态参数并将其传递给处理函数的?本文将深入探讨 NestJS 路由匹配的内部机制,从装饰器解析到参数提取的完整过程。
1. 路由基础概念
1.1 路由装饰器
NestJS 提供了丰富的路由装饰器来定义 HTTP 端点:
typescript
@Controller('users')
export class UserController {
@Get() // GET /users
findAll() {
return [];
}
@Get(':id') // GET /users/:id
findOne(@Param('id') id: string) {
return { id, name: 'User Name' };
}
@Post() // POST /users
create(@Body() createUserDto: CreateUserDto) {
return { id: '1', ...createUserDto };
}
@Put(':id') // PUT /users/:id
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return { id, ...updateUserDto };
}
@Delete(':id') // DELETE /users/:id
remove(@Param('id') id: string) {
return { id, deleted: true };
}
}1.2 路由注册过程
typescript
// 路由装饰器的实现
function createRouteMapping(method: RequestMethod) {
return function (path?: string | string[]): MethodDecorator {
return function (target: object, key: string | symbol, descriptor: PropertyDescriptor) {
// 存储路由元数据
Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
Reflect.defineMetadata(PATH_METADATA, path || '/', descriptor.value);
// 将路由方法添加到控制器的路由列表中
const routes = Reflect.getMetadata(ROUTE_ARGS_METADATA, target.constructor) || [];
routes.push(descriptor.value);
Reflect.defineMetadata(ROUTE_ARGS_METADATA, routes, target.constructor);
};
};
}
// 具体的 HTTP 方法装饰器
export const Get = (path?: string | string[]): MethodDecorator =>
createRouteMapping(RequestMethod.GET)(path);
export const Post = (path?: string | string[]): MethodDecorator =>
createRouteMapping(RequestMethod.POST)(path);
export const Put = (path?: string | string[]): MethodDecorator =>
createRouteMapping(RequestMethod.PUT)(path);
export const Delete = (path?: string | string[]): MethodDecorator =>
createRouteMapping(RequestMethod.DELETE)(path);2. 路径到正则表达式的转换
2.1 Path-to-RegExp 库
NestJS 内部使用类似 path-to-regexp 的机制将路径模式转换为正则表达式:
typescript
// 简化的路径到正则表达式转换
class RoutePathResolver {
private pathRegexCache = new Map<string, { regex: RegExp; keys: any[] }>();
pathToRegexp(path: string): { regex: RegExp; keys: any[] } {
// 检查缓存
if (this.pathRegexCache.has(path)) {
return this.pathRegexCache.get(path);
}
// 解析路径参数
const keys: any[] = [];
const regexSource = path.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
keys.push({ name: key });
return '([^/]+)'; // 匹配除 / 外的任意字符
});
// 创建正则表达式
const regex = new RegExp(`^${regexSource}$`);
const result = { regex, keys };
// 缓存结果
this.pathRegexCache.set(path, result);
return result;
}
}
// 使用示例
const resolver = new RoutePathResolver();
const { regex, keys } = resolver.pathToRegexp('/users/:id');
// regex: /^\/users\/([^/]+)$/
// keys: [{ name: 'id' }]2.2 动态参数解析
typescript
// 动态参数解析过程
class RouteMatcher {
match(path: string, routePattern: string): Record<string, string> | null {
const { regex, keys } = this.pathToRegexp(routePattern);
const match = path.match(regex);
if (!match) {
return null; // 路径不匹配
}
// 提取参数
const params: Record<string, string> = {};
keys.forEach((key, index) => {
params[key.name] = match[index + 1]; // match[0] 是完整匹配
});
return params;
}
private pathToRegexp(path: string) {
// 实现路径到正则表达式的转换
const keys: any[] = [];
const regexSource = path.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
keys.push({ name: key });
return '([^/]+)';
});
return {
regex: new RegExp(`^${regexSource}$`),
keys,
};
}
}
// 使用示例
const matcher = new RouteMatcher();
const params = matcher.match('/users/123', '/users/:id');
// params: { id: '123' }3. 路由注册与匹配
3.1 路由注册过程
typescript
// 路由注册过程
class RouterExplorer {
registerRoutes(instance: Controller, basePath: string) {
// 获取控制器的所有路由方法
const routes = Reflect.getMetadata(ROUTE_ARGS_METADATA, instance.constructor) || [];
routes.forEach(route => {
// 获取路由方法和路径
const method = Reflect.getMetadata(METHOD_METADATA, route);
const path = Reflect.getMetadata(PATH_METADATA, route);
// 构建完整路径
const fullPath = this.normalizePath(basePath, path);
// 注册路由到 HTTP 服务器
this.registerRoute(method, fullPath, instance, route.name);
});
}
private normalizePath(basePath: string, path: string): string {
// 标准化路径
if (path === '/' && basePath === '/') {
return '/';
}
const normalizedBasePath = basePath.replace(/\/$/, ''); // 移除末尾斜杠
const normalizedPath = path.replace(/^\/|\/$/g, ''); // 移除首尾斜杠
if (!normalizedPath) {
return normalizedBasePath || '/';
}
return `${normalizedBasePath}/${normalizedPath}`;
}
}3.2 路由匹配机制
typescript
// 路由匹配机制
class RoutesResolver {
private routes: RouteInfo[] = [];
addRoute(method: RequestMethod, path: string, handler: Function) {
const { regex, keys } = this.pathToRegexp(path);
this.routes.push({
method,
path,
regex,
keys,
handler,
});
}
resolve(requestMethod: RequestMethod, requestPath: string): RouteMatch | null {
// 查找匹配的路由
for (const route of this.routes) {
if (route.method !== requestMethod) {
continue;
}
const match = route.regex.exec(requestPath);
if (match) {
// 提取参数
const params: Record<string, string> = {};
route.keys.forEach((key, index) => {
params[key.name] = match[index + 1];
});
return {
handler: route.handler,
params,
};
}
}
return null; // 未找到匹配的路由
}
private pathToRegexp(path: string) {
// 路径到正则表达式的转换
const keys: any[] = [];
const regexSource = path
.replace(/\/$/, '') // 移除末尾斜杠
.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
keys.push({ name: key });
return '([^/]+)';
});
return {
regex: new RegExp(`^${regexSource}/?$`), // 可选的末尾斜杠
keys,
};
}
}
interface RouteInfo {
method: RequestMethod;
path: string;
regex: RegExp;
keys: any[];
handler: Function;
}
interface RouteMatch {
handler: Function;
params: Record<string, string>;
}4. 参数提取机制
4.1 @Param() 装饰器
typescript
// @Param() 装饰器实现
function createRouteParamDecorator(paramType: RouteParamtypes) {
return (data?: string): ParameterDecorator => {
return (target: object, key: string | symbol, index: number) => {
// 获取现有的路由参数元数据
const args = Reflect.getMetadata(ROUTE_ARGS_METADATA, target.constructor, key) || {};
// 添加新的参数信息
args[index] = {
index,
factory: (data: any, context: ExecutionContext) => {
const request = context.switchToHttp().getRequest();
switch (paramType) {
case RouteParamtypes.PARAM:
return data ? request.params?.[data] : request.params;
case RouteParamtypes.BODY:
return data ? request.body?.[data] : request.body;
case RouteParamtypes.QUERY:
return data ? request.query?.[data] : request.query;
// ... 其他参数类型
}
},
data,
};
// 存储元数据
Reflect.defineMetadata(ROUTE_ARGS_METADATA, args, target.constructor, key);
};
};
}
// 具体的参数装饰器
export const Param = (property?: string): ParameterDecorator =>
createRouteParamDecorator(RouteParamtypes.PARAM)(property);
export const Body = (property?: string): ParameterDecorator =>
createRouteParamDecorator(RouteParamtypes.BODY)(property);
export const Query = (property?: string): ParameterDecorator =>
createRouteParamDecorator(RouteParamtypes.QUERY)(property);4.2 参数解析过程
typescript
// 参数解析过程
class RouterExecutionContext {
async getMethodArguments(
instance: Controller,
methodName: string,
context: ExecutionContext,
): Promise<any[]> {
// 获取方法参数元数据
const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, instance.constructor, methodName) || {};
// 按索引排序参数
const keys = Object.keys(metadata).map(Number).sort((a, b) => a - b);
// 解析每个参数
const args = [];
for (const index of keys) {
const param = metadata[index];
const { factory, data } = param;
// 使用工厂函数解析参数值
const value = await factory(data, context);
args.push(value);
}
return args;
}
}
// 使用示例
@Controller('users')
export class UserController {
@Get(':id')
findOne(@Param('id') id: string, @Query('include') include: string) {
// id 从 req.params.id 提取
// include 从 req.query.include 提取
return { id, include, name: 'User Name' };
}
}5. 复杂路由匹配
5.1 正则表达式路由
typescript
// 支持正则表达式的路由
@Controller('users')
export class UserController {
@Get(':id(\\d+)') // 只匹配数字 ID
findOne(@Param('id') id: string) {
return { id: parseInt(id), name: 'User Name' };
}
@Get(':name([a-zA-Z]+)') // 只匹配字母名称
findByName(@Param('name') name: string) {
return { name, type: 'alphabetical' };
}
}
// 正则表达式路由解析
class RegexRouteResolver {
pathToRegexp(path: string) {
const keys: any[] = [];
const regexSource = path.replace(/:([a-zA-Z0-9_]+)(\(.*?\))?/g, (_, key, regex) => {
keys.push({ name: key });
return regex || '([^/]+)'; // 使用自定义正则或默认正则
});
return {
regex: new RegExp(`^${regexSource}$`),
keys,
};
}
}5.2 通配符路由
typescript
// 通配符路由
@Controller()
export class FileController {
@Get('files/*') // 匹配 files/ 后的所有路径
getFile(@Param() params: any) {
// params[0] 包含 * 匹配的内容
return { path: params[0] };
}
}
// 通配符路由解析
class WildcardRouteResolver {
pathToRegexp(path: string) {
const keys: any[] = [];
let regexSource = path;
// 处理通配符
if (path.includes('*')) {
keys.push({ name: '0' }); // 通配符参数
regexSource = path.replace(/\*/g, '(.*)');
}
return {
regex: new RegExp(`^${regexSource}$`),
keys,
};
}
}6. 路由性能优化
6.1 路由缓存
typescript
// 路由缓存机制
class RouteCache {
private cache = new Map<string, RouteMatch>();
private readonly MAX_CACHE_SIZE = 1000;
get(method: RequestMethod, path: string): RouteMatch | null {
const key = `${method}:${path}`;
return this.cache.get(key) || null;
}
set(method: RequestMethod, path: string, routeMatch: RouteMatch) {
// 限制缓存大小
if (this.cache.size >= this.MAX_CACHE_SIZE) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
const key = `${method}:${path}`;
this.cache.set(key, routeMatch);
}
}6.2 路由树结构
typescript
// 使用路由树优化匹配性能
class RouteTree {
private root: RouteNode = new RouteNode();
addRoute(method: RequestMethod, path: string, handler: Function) {
const parts = path.split('/').filter(Boolean);
let currentNode = this.root;
// 构建路由树
for (const part of parts) {
if (!currentNode.children.has(part)) {
currentNode.children.set(part, new RouteNode(part));
}
currentNode = currentNode.children.get(part);
}
// 设置叶子节点的处理器
if (!currentNode.handlers.has(method)) {
currentNode.handlers.set(method, handler);
}
}
match(method: RequestMethod, path: string): RouteMatch | null {
const parts = path.split('/').filter(Boolean);
let currentNode = this.root;
// 遍历路由树
for (const part of parts) {
// 精确匹配
if (currentNode.children.has(part)) {
currentNode = currentNode.children.get(part);
continue;
}
// 参数匹配
let paramNode: RouteNode | undefined;
for (const [key, node] of currentNode.children) {
if (key.startsWith(':')) {
paramNode = node;
break;
}
}
if (paramNode) {
currentNode = paramNode;
continue;
}
return null; // 未找到匹配
}
// 返回处理器
if (currentNode.handlers.has(method)) {
return {
handler: currentNode.handlers.get(method),
params: {}, // 实际实现中需要提取参数
};
}
return null;
}
}
class RouteNode {
children = new Map<string, RouteNode>();
handlers = new Map<RequestMethod, Function>();
constructor(public name?: string) {}
}7. 总结
NestJS 的路由匹配机制是一个复杂而精巧的系统,主要包括以下几个关键部分:
- 路由装饰器解析:通过元数据存储路由信息
- 路径到正则表达式转换:将路径模式转换为可匹配的正则表达式
- 路由注册:将路由信息注册到路由管理系统
- 路由匹配:在请求到来时找到匹配的路由处理器
- 参数提取:从请求中提取路径参数、查询参数、请求体等
理解这些机制有助于我们:
- 编写更高效的路由
- 理解参数装饰器的工作原理
- 调试路由匹配问题
- 优化应用性能
在下一篇文章中,我们将探讨参数装饰器原理:@Body()、@Param()、@Query() 如何工作,深入了解它们如何通过元数据告诉框架"这个参数从哪来"。