Skip to content

路由匹配:从 @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 的路由匹配机制是一个复杂而精巧的系统,主要包括以下几个关键部分:

  1. 路由装饰器解析:通过元数据存储路由信息
  2. 路径到正则表达式转换:将路径模式转换为可匹配的正则表达式
  3. 路由注册:将路由信息注册到路由管理系统
  4. 路由匹配:在请求到来时找到匹配的路由处理器
  5. 参数提取:从请求中提取路径参数、查询参数、请求体等

理解这些机制有助于我们:

  1. 编写更高效的路由
  2. 理解参数装饰器的工作原理
  3. 调试路由匹配问题
  4. 优化应用性能

在下一篇文章中,我们将探讨参数装饰器原理:@Body()@Param()@Query() 如何工作,深入了解它们如何通过元数据告诉框架"这个参数从哪来"。