NestJS快速入门 for KoaJSer/EggJSer/Expresser(施工中)

NestJS 与EggJS基本上属于一种类型的NodeJS框架,它们提供了一整套的解决方案,但侧重点可能不一样。

KoaJS、ExpressJS则是提供了更为基础的HTTP框架,并且提供了极大地扩展性,所以EggJS和NestJS也分别基于Koa、Express之上来实现(当然了,NestJS也提供了切换底层为FastifyJS这种HTTP框架)

NestJS和EggJS的侧重点差异在于前者关注服务实现也就是代码书写的方式,提供了更为多样的扩展方式。EggJS则是关注具体应用开发中的问题,用相对更加规范化的模板来提供解决方案。不过问题就在于现在整体Cloud Native日趋流行的今天,再采用EggJS全部的解决方案反而有点累赘(比如多进程架构、进程内通信)

核心概念

NestJS核心的流程

作为一个IOC Web框架,基本运行模式是固定下来的,就是看各类组件是怎样组织整合到一起。核心源码在package/core目录下面,具体源码解析看另外一篇文章就好了

这里只是简单描绘下NestJS从启动开始到运行到接受请求做的事情

Controller

从MVC时代流传下来的名词,当然了这么经典的名词肯定要保持它本来的作用

用于接受处理HTTP请求(如果使用Microservices模式可以接受事件请求),这块跟经典的框架的设计理念并没有什么不同,看官方参考文档就可以了

Provider

我个人觉得这是整个NestJS框架最核心的概念,顾名思义,Provider相当于一个容器,被包装的东西可以被依赖注入给框架。来自于Controller的很多业务Provider委托处理。

Provider有生命周期的,默认是全局共享单例,另外还有两种不同的生命周期,分别是每个请求-响应对时初始化和每个Provider访问时初始化,依次内存占用会升高

NestJS本身算是一个IoC控制反转的框架,有的时候,框架并不清楚自己要加载哪些或者初始化哪些,通过@Injectable和Module来注册Provider这样就告诉了框架该初始化哪些以及何时初始化

让我们来看下@Injectable的实现

export function Injectable(options?: InjectableOptions): ClassDecorator {
  return (target: object) => {
    Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target);
  };
}

这是一个装饰器工厂,主要做的事情就是借助Reflect库来定义SCOPE_OPTIONS_METADATA数据,乍一看好像并没有做什么事。这是为了后续框架根据Scope不同初始化时传入不同参数来判断

// module.ts

  public addInjectable<T extends Injectable>(
    injectable: Type<T>,
    host?: Type<T>,
  ) {
    if (this.isCustomProvider(injectable)) {
      return this.addCustomProvider(injectable, this._injectables);
    }
    const instanceWrapper = new InstanceWrapper({
      name: injectable.name,
      metatype: injectable,
      instance: null,
      isResolved: false,
      scope: this.getClassScope(injectable), // 通过这里来获取
      host: this,
    });
    this._injectables.set(injectable.name, instanceWrapper);

    if (host) {
      const hostWrapper = this._controllers.get(host && host.name);
      hostWrapper && hostWrapper.addEnhancerMetadata(instanceWrapper); 
    }
  }

让我们再来看下@Inject的实现

export function Inject<T = any>(token?: T) {
  return (target: Object, key: string | symbol, index?: number) => {
    token = token || Reflect.getMetadata('design:type', target, key);
    const type =
      token && isFunction(token) ? ((token as any) as Function).name : token;

    if (!isUndefined(index)) {
      let dependencies =
        Reflect.getMetadata(SELF_DECLARED_DEPS_METADATA, target) || [];

      dependencies = [...dependencies, { index, param: type }];
      Reflect.defineMetadata(SELF_DECLARED_DEPS_METADATA, dependencies, target);
      return;
    }
    let properties =
      Reflect.getMetadata(PROPERTY_DEPS_METADATA, target.constructor) || [];

    properties = [...properties, { key, type }];
    Reflect.defineMetadata(
      PROPERTY_DEPS_METADATA,
      properties,
      target.constructor,
    );
  };
}

同样的,这块的实现依然是装饰器工厂,主要做的事情就是利用Reflect建立属性property和传入token之间的联系,这样做的目的是在类实例初始化/函数执行的时候传入正确的provider/class实例

Module

Module是需要使用@Module装饰器的,这个@Module装饰器的作用主要是用来告诉Nest各个module的metadata信息,然后用以构建程序架构,每一个程序都至少有一个Module,称之为root module。Root Module的一个重要作用是用来构建整个应用程序的全貌(毕竟这是一个IOC框架,需要知道自己该加载什么该初始化什么以及弄清楚各个模块之间的依赖关系)

@Module装饰器可以接受包含以下参数的对象。

  • providers: 这些providers会由Nest的injector初始化,并根据注册的对应scope范围内有效
  • controllers: 由这个模块定义的controller并且需要初始化的
  • imports: 列出那些需要导入/依赖的provider所归属的module
  • exports: 这个是providers的一个子集,由本module内提供并允许被其他module使用的

综合所述,provider是由module来封装的,所以这就意味着不可能在引用对应module下使用该module未导出的provider,也不可能引用不存在的provider。因此可以考虑通过封装module的public接口来暴露provider对应的功能

默认情况下,module是全局单例的,并且module都是只局限自己内部的,比如说A引入B,C引入了A的话,C是没办法直接使用A里面的Provider的,但是如果A在其exports里面除了暴露它的provider之外如果再暴露Bmodule的话,那么C就可以了

为了避免出现模块中A引入B,B引入A这种情况,提供了两种方式,一是使用forwardRef(()=>XX)的方式,另外一种是使用moduleRef的方式

为了使某些模块中的provider成为全局可用的,那么需要使用@Global装饰器

可以自定义module,用以不同配置

其他次级概念

middleware

middleware实现很简单,而且可以使用@Injectable装饰器,这样就方便引入以及初始化了,不过middleware的引入不像provider和controller一样还要在module中声明,而是在module的类声明中使用configure函数配置

如果middleware非常简单且没有任何依赖的话,可以使用函数式的中间件,这种方式是最简单的,都不需要依赖注入了,自己初始化就完事了

全局的中间件可以使用由NestApplication实例提供的use函数

exception filter

用来处理异常的,并且有三个不同的生效范围

  • method
  • controller
  • global

使用全局filter并不会影响到混合应用(微服务、WS)

倾向于使用依赖注入的方式来创建filter那样比较节省内存

pipe

pipe通常有两种用途:一种转换输入到指定输出形式,另一种是验证数据。校验输入数据是否有效如果数据不正常就抛出。无论哪种使用方式,都是作用于controller指定路径的参数,具体执行的时间也是在触发对应路由的handler之前,在pipe收到参数后。

如果pipe发生异常后,会被当前context或者全局的exception filter捕获处理

pipe比filter多一个scope,就是可以param-scoped

guards

用途只有一个,就是决定是否由route handler处理,具体是由Permission、ACL 、role来决定。

guards执行再middleware之后,pipe和intercept之前

guard必须实现canActivate()函数,且必须返回boolean值,可以以异步(Promise或者Observable)或者同步的形式来返回

interceptos

这个用途主要受AOP编程思想影响,用以扩展原有逻辑。基本上啥都能改

interceptor需要实现intercept()接口,这个接口由俩参数,一个是ExecutionContext,另一个是handler,如果不在intercept中调用handler的handle函数的话,那么handler永远不会触发。 intercept函数返回的是一个Observable类型的

所以给我们留下了很大的发挥空间