深入了解Nest的Provider

3,088 阅读5分钟

Nest中,有个重要且基础的概念:Provider,就其字面意思翻译“提供者”,可我觉得还不够准确,一直在思考究竟用一个什么词来才能准确描述Nest中赋予它的含义,很遗憾,我能力有限水平一般,目前还没有找到。

从官方文档中得知,例如servicerepositoriesfactories等等的,都可以称之为provider。我觉得反过来理解简单:我们可以把控制器理解为使用功能,那么为控制器服务(被调用)的就都可以理解为Provider了。

不论从哪个方向去理解,总之就是将各种逻辑业务封装在其中,统一暴露给控制器来执行。这样做法的好处是:做业务的关心业务本身就可以了,其他例如:身份验证、参数检验,都不用去考虑,交给中间件就好。这里挖个坑,把基础的功能讲完了还有同学感兴趣的就继续写。

依赖注入

先前提到过,依赖注入是一种IoC的实现模式,而NEST则是深度地实现了这以模式,借助TypeScript的强大语言优势(让JS有了类型概念),将其IoC在JS领域推上一个新的高度。默认情况下(用命令行创建一个Controller)以构造函数参数的注入方式。我们经常可以在程序中看到类似以下的代码:

@Controller('/')
export class AppController {
  constructor(private readonly appService: AppService) {}
  // 不熟悉TS的同学请注意,上的一行代码等效下面的四行:
  // private readonly appService:AppService;
  // constructor(appService:AppService) {
  //   this.appService=appService;
  // }
}

利用TS的特性,相当于声明了一个(类)私有只读AppService类型的对象appService属性。如果需要用到多个,只需要定义多个入口参数即可。

Nest还支持基于属性方式注入:

@Controller('/')
export class AppController {
  @Inject('AppService')
  private readonly appService: AppService;
}

在AppService类中,则只需要用一个@Injectable()装饰器即可:

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

因为Provider的实例化过程都是有Nest来管理的,开发者一般情况下是无法介入的。所以,在实例化时需要处理的依赖注入需要使用装饰器来声明。上面用了两个例子来说明如何利用装饰器来定义构造函数注入和属性注入,大家请注意,被注入的对象是Controller,也就是功能的使用者。那么Provider本身是否可以支持被注入呢?答案是可以的。后面还会降到动态模块时会再提到这个概念。

作用域

一般情况下,Provider都有着同应用程序一样的生命周期(根据依赖关系逐个生成,请求完成后再一并销毁)。但是也可以通过一些其他方法来延申其作用域。Nest提供三种周期:

  • 默认(DEFAULT):同应用程序周期;
  • 请求(REQUEST):请求处理完成后便被销毁;
  • 瞬态(TRANSIENT):每个使用Provider的程序都会得到一个独立的实例;

用例

用一个例子说明以上不同作用域的区别:先创建一个项目,然后为这个项目添加一个控制器:

nest new provider
nest g co user

先修改app.service文件,我们就以这个为例,来实践一下作用域:

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }

  private scopeA = '';
  set A(value: string) {
    this.scopeA = value;
  }
  get A(): string {
    return this.scopeA;
  }
}

修改app.controller,定义两个方法,分别读取和设置Service的A属性:

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('set')
  setString(@Query('str') str: string): string {
    return this.appService.setA(str);
  }

  @Get('get')
  getString(): string {
    return this.appService.getA();
  }
}

将新创建的user.controller,执行与上述app.controller一样的功能:

@Controller('user')
export class UserController {
  constructor(private readonly appService: AppService) {}

  @Get('set')
  setString(@Query('str') str: string): string {
    return this.appService.setA(str);
  }

  @Get('get')
  getString(): string {
    return this.appService.getA();
  }
}

测试:

npm run start:dev
curl --location --request GET 'localhost:3000/set?str=Hi~' # done
curl --location --request GET 'localhost:3000/get' # Hi~
curl --location --request GET 'localhost:3000/user/get' # Hi~

此时可以看到,Nest以单例模式运行后,一次设定,再多个请求中都可以被使用到。下面,我们稍作调整,将作用域调整为每个被注入类用一个新实例,修改app.service

@Injectable({
  scope: Scope.TRANSIENT,
})
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }

  private scopeA = '';
  set A(value: string) {
    this.scopeA = value;
  }
  get A(): string {
    return this.scopeA;
  }
}

再次测试:

npm run start:dev
curl --location --request GET 'localhost:3000/set?str=Hi~' # done
curl --location --request GET 'localhost:3000/get' # Hi~
curl --location --request GET 'localhost:3000/user/get' # 
curl --location --request GET 'localhost:3000/user/set?str=Hi User~' # done
curl --location --request GET 'localhost:3000/user/get' # Hi User~
curl --location --request GET 'localhost:3000/get' # Hi~

可以看出,每次请求虽然都成功的写入了信息,但是当再次请求时,什么都没有留下。再稍作调整,将作用域调整为每次请求都用一个新实例,修改app.service

@Injectable({
  scope: Scope.REQUEST,
})
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }

  private scopeA = '';
  set A(value: string) {
    this.scopeA = value;
  }
  get A(): string {
    return this.scopeA;
  }
}

再次测试:

npm run start:dev
curl --location --request GET 'localhost:3000/set?str=Hi~' # done
curl --location --request GET 'localhost:3000/get' # 
curl --location --request GET 'localhost:3000/user/get' # 
curl --location --request GET 'localhost:3000/user/set?str=Hi User~' # done
curl --location --request GET 'localhost:3000/user/get' # 

作用域链

关于作用域这一概念,不局限于Provider应用,控制器在这方面也是同理。由于整个架构都是应用了依赖注入,被依赖方注入到依赖方,所以被依赖方的作用域将会等于依赖方的作用域。还记得上面那个通过构造函数注入吗?一个新的实例才能才能应对新的构造函数。例如:一个Service是REQUEST作用域的,那么使用它的Controller也是REQUEST作用域。

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  private B = '';

  @Get('set')
  setString(@Query('str') str: string): string {
    this.appService.A = str;
    this.B = str;
    return 'done';
  }

  @Get('get')
  getString(): string {
    console.log(this.B);
    return this.appService.A;
  }
}

在控制器中增加了一个B字段,请求后的控制台输出一个空行。删除掉app.service的作用域参数(默认为App作用域),可以看到控制台输出了一个结果。

注册Provider

即便是一个复杂的依赖关系,最后都得交由Nest的IoC策略来处理。把所引用到的文件一一放在Module文件中,用@Module装饰器进行注册动作。