记录我的NestJS探究历程(四)

在上一篇文章中分析了NestJS的DI的依赖关系解析,依赖注入流程之后,本文开始分析NestJS路由相关的内容。

初探NestJS的运行原理之路由

NestJS宣称的是底层提供了一层抽象,没有跟特定的框架绑定,但是默认是使用的是Express

Nest provides a level of abstraction above these common Node.js frameworks (Express/Fastify), but also exposes their APIs directly to the developer. This gives developers the freedom to use the myriad of third-party modules which are available for the underlying platform.

在我们的项目中,调用NestFactory.create的时候,我们不指定底层的http实现,NestJS就如上图所示选择了默认的Express框架。 于是,我们的目光就可以移步到@nestjs/platform-express这个包中了。

打开到pacakges/platform-express/adapters/express-adapter.ts,此时我们发现一切熟悉的东西都来了,因为NestJS在这个Adapter里仅仅是对Express进行了包装。

NestJS首先用一个适配器来抹平ExpressAPI本身和NestJS通用实现的差异。

到这个时候,Express这边看起来活儿就搞完了,但是我们的Controller是怎么跟Express的路由绑定起来的呢,还需要一些必要的操作才行。为了简单起见,我们仍然采用施加断点来查看系统的调用堆栈。 上图的listen方法,就是我们在项目里的bootstrap方法里面调用的,然后这个init方法里面调用了NestApplication的init方法,准备开始进行路由的注册。 在之前的时候,IoC容器,已经帮我们把所有的Controller全部都创建好了。 此时我们只需要映射路由和Controller的方法的关系了,但是还不能着急,因为路由的信息全部是配置在Controller那个类上面的,首先得先解元数据才能知道映射关系。

然后调用PathsExplorer类的方法,将路由path和处理的方法绑定。

此时还剩下一个问题,通俗的讲Controller只是拿到了offer,它还没有办理入职,还没有跟Express进行绑定,绑定了之后才算完成了入职,我们接下来看它是怎么进行绑定的。

回到routes-explorer.ts中,在scanForPaths下面的applyPathsToRouterProxy方法调用就是进行这个关联操作的,我们可以一直往它的调用链追踪,可以追踪到它最终绑定完成路由,如下:

至此,我们还没有考虑路由参数,及控制器方法参数装饰器等其它更加细节的技术,也没有详细探究我们编写的控制器的方法的返回内容是怎么映射回Express的(比如怎么样让Express知道我们是在渲染页面还是直接返回JSON,返回怎么样的http状态码相关细节等)我们现在只探究了NestJS路由处理流程,这些知识点在即将后续章节继续阐述。

可以看到,最后的流程被NestJS用一个Proxy包起来了,为什么要包起来呢,因为框架的设计者不能指望用户给出的内容一定能按预期运行,NestJS设计了一个全局的异常捕获器,这个可以实现统一的全局错误捕获。

总来的说,相比于启动流程的DI流程,路由处理还是很简单了,只需要将Controller的方法和Express的API绑定起来即可。

初探NestJS的运行原理之路由参数

在上一节中,我们探究了NestJS的路由处理过程,但是并没有详细的探究它是怎么映射数据到我们Controller的某个方法,本节开始探究这个过程。

在这之前,我们需要先留意一下NestJS提供的关于路由参数的装饰器,我们也挑几个比较关键的装饰器来看看。

  • Query
  • Body
  • Param
  • Req
  • Res

以上5个装饰器是我们常用的装饰器,因此我们来看看它们的实现。

Body,Query,Param:

ts 复制代码
// 参数合并
function assignMetadata<TParamtype = any, TArgs = any>(
  args: TArgs,
  paramtype: TParamtype,
  index: number,
  data?: ParamData,
  ...pipes: (Type<PipeTransform> | PipeTransform)[]
) {
  return {
    ...args,
    [`${paramtype}:${index}`]: {
      index,
      data,
      pipes,
    },
  };
}

const createPipesRouteParamDecorator =
  (paramtype: RouteParamtypes) =>
  (
    data?: any,
    ...pipes: (Type<PipeTransform> | PipeTransform)[]
  ): ParameterDecorator =>
  (target, key, index) => {
    const args =
      Reflect.getMetadata(ROUTE_ARGS_METADATA, target.constructor, key) || {};
    const hasParamData = isNil(data) || isString(data);
    const paramData = hasParamData ? data : undefined;
    const paramPipes = hasParamData ? pipes : [data, ...pipes];
    // 定义合并之后的路由数据解析
    Reflect.defineMetadata(
      ROUTE_ARGS_METADATA,
      assignMetadata(args, paramtype, index, paramData, ...paramPipes),
      target.constructor,
      key,
    );
  };

export function Query(
  property?: string | (Type<PipeTransform> | PipeTransform),
  ...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator {
  // 从querystrting上解析路由数据
  return createPipesRouteParamDecorator(RouteParamtypes.QUERY)(
    property,
    ...pipes,
  );
}

export function Body(
  property?: string | (Type<PipeTransform> | PipeTransform),
  ...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator {
  // 从body上解析路由数据  
  return createPipesRouteParamDecorator(RouteParamtypes.BODY)(
    property,
    ...pipes,
  );
}

export function Param(
  property?: string | (Type<PipeTransform> | PipeTransform),
  ...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator {
  // 从请求路径上解析路由数据  
  return createPipesRouteParamDecorator(RouteParamtypes.PARAM)(
    property,
    ...pipes,
  );
}

Request和Response相关:

ts 复制代码
function createRouteParamDecorator(paramtype: RouteParamtypes) {
  return (data?: ParamData): ParameterDecorator =>
    (target, key, index) => {
      const args =
        Reflect.getMetadata(ROUTE_ARGS_METADATA, target.constructor, key) || {};
      Reflect.defineMetadata(
        ROUTE_ARGS_METADATA,
        assignMetadata<RouteParamtypes, Record<number, RouteParamMetadata>>(
          args,
          paramtype,
          index,
          data,
        ),
        target.constructor,
        key,
      );
    };
}

export const Request: () => ParameterDecorator = createRouteParamDecorator(
  RouteParamtypes.REQUEST,
);

export const Response: (
  options?: ResponseDecoratorOptions,
) => ParameterDecorator =
  (options?: ResponseDecoratorOptions) => (target, key, index) => {
    if (options?.passthrough) {
      Reflect.defineMetadata(
        RESPONSE_PASSTHROUGH_METADATA,
        options?.passthrough,
        target.constructor,
        key,
      );
    }
    return createRouteParamDecorator(RouteParamtypes.RESPONSE)()(
      target,
      key,
      index,
    );
  };

这些装饰器注入的元数据是什么时候被解析的呢?我们得把目光回归到这个位置才行。 这个地方引入了一个RouterExecutionContext类,追踪进去看一下它做了什么,这个方法很长,我们只需要先关注getMetadata方法。 然后又进入到了一个很关键的exchangeKeysForValues方法,此刻就有点儿快接近字段映射真相了。 这个exchangeKeyForValue方法就有点儿像我们经常写的数组的map方法那样,只不过这个位置它处理的RequestResponse对象,Next方法。这个位置的paramsFactory是外界传递进来的,那就得回到RouterExecutionContext类初始化的位置,得回到RouterExplorer的constructor的位置了。 查看这个方法的定义,那也确实简单,即从某个对象上取相应的字段。

最后,我们再来看NestJS官方给我们的自定义装饰器的例子。

ts 复制代码
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user = request.user;

    return data ? user?.[data] : user;
  },
);

NestJS发现是自行实现的参数映射逻辑,就走我们定义的取值逻辑。 多记录了一个自定义转换函数 这儿有些同学可能看不太懂,给大家解释一下,getCustomFactory方法得到是我们传递进去的factory转换函数,contextFactory的执行结果是ExecutionContextHost,那不就是如NestJS官方文档写的那样了吗,第一个参数是装饰器配置的路由元信息,第二个参数最终得到的就是执行上下文,就可以转换成我们自定义的结果了。

初探NestJS的运行原理之Action的返回类型

之前我们就已经留下了疑问,NestJS是怎么知道我们返回的内容该怎么样映射成用户请求的HTTP资源呢? 我们得再把目光切回RouterExecutionContext这个类上,核心逻辑在这个fnHandlerResponse上: 首先是处理模板引擎的渲染逻辑 比如这种用法:

ts 复制代码
import { Get, Controller, Render } from '@nestjs/common';

@Controller()
export class AppController {
  @Get()
  @Render('index')
  root() {
    return { message: 'Hello world!' };
  }
}

Render装饰器的逻辑如下: NestJS读取Render装饰器定义的元数据 responseController的render方法调用了更加底层的render,来源于applicationRef,它的类型是HttpServer 追踪这个方法,可以看到是在http-server.interface.ts里面,那就基本上明确了,NestJS定义了一个规范,@nestjs/platform-express包的Adaptor实现了这个方法,在这个位置,result就是Action方法返回的供模板引擎渲染页面的数据,res是透传过来的ExpressResponse对象。

ts 复制代码
class RouterExecutionContext {
    
  public create(
    instance: Controller,
    callback: (...args: any[]) => unknown,
    methodName: string,
    moduleKey: string,
    requestMethod: RequestMethod,
    contextId = STATIC_CONTEXT,
    inquirerId?: string,
  ) {
    const {
      // 省略了很多无关代码
      fnHandleResponse,
    } = this.getMetadata(
      instance,
      callback,
      methodName,
      moduleKey,
      requestMethod,
      contextType,
    );
    const interceptors = this.interceptorsContextCreator.create(
      instance,
      callback,
      moduleKey,
      contextId,
      inquirerId,
    );
    // 省略了很多暂时还不需要过多关注的代码
    const handler =
      <TRequest, TResponse>(
        args: any[],
        req: TRequest,
        res: TResponse,
        next: Function,
      ) =>
      async () => {
        fnApplyPipes && (await fnApplyPipes(args, req, res, next));
        return callback.apply(instance, args);
      };

    return async <TRequest, TResponse>(
      req: TRequest,
      res: TResponse,
      next: Function,
    ) => {
      const args = this.contextUtils.createNullArray(argsLength);
      this.responseController.setStatus(res, httpStatusCode);
      hasCustomHeaders &&
        this.responseController.setHeaders(res, responseHeaders);
      // 执行拦截器得到控制器的方法返回的内容
      const result = await this.interceptorsConsumer.intercept(
        interceptors,
        [req, res, next],
        instance,
        callback,
        handler(args, req, res, next),
        contextType,
      );
      // 删除了类型映射,便于阅读
      await fnHandleResponse(result, res, req);
    };
  }
  
  public getMetadata<TContext extends ContextType = ContextType>(
    instance: Controller,
    callback: (...args: any[]) => any,
    methodName: string,
    moduleKey: string,
    requestMethod: RequestMethod,
    contextType: TContext,
  ): HandlerMetadata {
    // 省略了很多代码,只展示了fnHandleResponse函数的来源逻辑
    const fnHandleResponse = this.createHandleResponseFn(
      callback,
      isResponseHandled,
      httpRedirectResponse,
    );
    const handlerMetadata: HandlerMetadata = {
      // 省略了无关内容
      fnHandleResponse,
    };
    return handlerMetadata;
  }
  
  public createHandleResponseFn(
    callback: (...args: unknown[]) => unknown,
    isResponseHandled: boolean,
    redirectResponse?: RedirectResponse,
    httpStatusCode?: number,
  ): HandleResponseFn {
    // 处理模板引擎的逻辑
    const renderTemplate = this.reflectRenderTemplate(callback);
    if (renderTemplate) {
      return async <TResult, TResponse>(result: TResult, res: TResponse) => {
        return await this.responseController.render(
          result,
          res,
          renderTemplate,
        );
      };
    }
    // 已省略无关的代码
    // 处理一般返回
    return async <TResult, TResponse>(result: TResult, res: TResponse) => {
      result = await this.responseController.transformToResult(result);
      !isResponseHandled &&
        (await this.responseController.apply(result, res, httpStatusCode));
      return res;
    };
  }
}

搞明白了模板引擎是怎么渲染的了,再看一下返回JSON是怎么给前端的。

也是来源于@nestjs/platform-express注入的Express的方法。

其它的情况也就基本上类似了,最后再看一下我们通过@Header是怎么设置返回内容的HTTP的Response Header的基本上就差不多了。

先看一下Header装饰器的定义: 通过扫描装饰器的元数据得到返回的Header 最终设置到Response对象上即可。

总结

至此,我们可以给NestJS的路由处理流程来做个总结(暂时不考虑中间件,拦截器,守卫):

NestJS在程序启动的时候会注入一个更底层HTTP实现类,(比如默认是Express,正是因为这样的设计,也给了我们能够自定义底层Http框架的实现的能力)这个适配器必须遵循NestJS的适配器的规则;IoC容器在创建好了Controller的实例之后,NestJS开始去找什么样的路由该映射到哪个控制器的哪个方法上;并且,控制器的方法的参数的映射规则(怎么样从原始的request对象,response对象,next方法转换成控制器方法需要的数据),通过解析装饰器的元数据得到,通过解析装饰器的元数据,知道控制器方法的返回内容该怎么样对应上最终的Http资源,最后将控制器方法跟Express的实现进行绑定,实现将执行结果响应给浏览器。

在下一节内容,我们将开始分析NestJS的中间件,拦截器,守卫等内容的技术细节,敬请期待!

相关推荐
gnip1 小时前
链式调用和延迟执行
前端·javascript
SoaringHeart2 小时前
Flutter组件封装:页面点击事件拦截
前端·flutter
杨天天.2 小时前
小程序原生实现音频播放器,下一首上一首切换,拖动进度条等功能
前端·javascript·小程序·音视频
Dragon Wu2 小时前
React state在setInterval里未获取最新值的问题
前端·javascript·react.js·前端框架
Jinuss2 小时前
Vue3源码reactivity响应式篇之watch实现
前端·vue3
YU大宗师2 小时前
React面试题
前端·javascript·react.js
木兮xg2 小时前
react基础篇
前端·react.js·前端框架
ssshooter2 小时前
你知道怎么用 pnpm 临时给某个库打补丁吗?
前端·面试·npm
IT利刃出鞘3 小时前
HTML--最简的二级菜单页面
前端·html
yume_sibai3 小时前
HTML HTML基础(4)
前端·html