在上一篇文章中分析了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首先用一个适配器来抹平Express
API本身和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方法那样,只不过这个位置它处理的Request
、Response
对象,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是透传过来的Express
的Response
对象。
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的中间件,拦截器,守卫等内容的技术细节,敬请期待!