前一篇有提到 Provider 与 Module 之间有很核心的机制,该机制使用了 依赖注入 的概念。这边会先针对依赖注入及 Nest 如何将其融入进行解释,再针对 Provider 的使用方式做说明,如此一来会对 Provider 有更深度的理解,在学习上比较不会满头问号,那就废话不多说赶快开始吧!
依赖注入是一种设计方法,透过此方式可以大幅降低耦合度,来个简单的例子吧,假设有两个 class
分别叫 Computer
与 CPU
:
class CPU {
name: string;
constructor(name: string) {
this.name = name;
}
}
class Computer {
cpu: CPU;
constructor(cpu: CPU) {
this.cpu = cpu;
}
}
可以看到 Computer
在建构的时候需要带入类别为 CPU
的参数,这样的好处是把 CPU
的功能都归在 CPU
里、Computer
不需要实作 CPU
的功能,甚至抽换成不同 CPU
都十分方便:
const i7 = new CPU('i7-11375H');
const i9 = new CPU('i9-10885H');
const PC1 = new Computer(i7);
const PC2 = new Computer(i9);
不过依赖注入跟 Provider 还有 Module 有什麽关系呢?仔细回想一下,当我们在 Controller 的 constructor
注入了 Service 後,没有使用到任何 new
却可以直接使用。这里以 app.controller.ts
为例:
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
没有经过实例化那这个实例从哪里来的?前面有说过当 Module 建立起来的同时,会把 providers
里面的项目实例化,而我们注入的 Service 就是透过这样的方式建立实例的,也就是说有个机制在维护这些实例,这个机制叫 控制反转容器 (IoC Container)。
控制反转容器是透过 token
来找出对应项目的,有点类似 key/value
的概念,这时候可能会想说:我没有指定 token
是什麽 Nest 怎麽知道对应的实例是哪一个?事实上,我们写 providers
的时候就已经指定了。这里以 app.module.ts
为例:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [
AppService
],
})
export class AppModule {}
奇怪,只是写了一个 AppService
就指定了 token
?没错,因为那是缩写,把它展开来的话会像这样:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [
{ provide: AppService, useClass: AppService }
],
})
export class AppModule {}
可以看到变成了一个物件,该物件的 provide
即 token
,useClass
则是指定使用的 class
为何,进而建立实例。
Provider 透过控制反转容器做实例的管理,可以很方便且有效地使用这些 Provider,而 Provider 大致上可以分成两种:
这是最简单的作法,也是大多数 Service 的作法,在 class
上添加 @Injectable
让 Nest 知道这个 class
是可以由控制反转容器管理的。通常 Service 会使用下方指令来产生:
$ nest generate service <SERVICE_NAME>
注意:
<SERVICE_NAME>
可以含有路径,如:features/todo
,这样就会在src
资料夹下建立该路径并含有 Service。
这里以 app.service.ts
为例:
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
在 Module 中,只需要於 providers
中声明该 Service 即可。这里以 app.module.ts
为例:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
如果喜欢写展开式也是可以:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [
{
provide: AppService,
useClass: AppService
}
],
})
export class AppModule {}
如果觉得标准 Provider 无法满足需求,如:
class
进行覆写,以便做测试。没关系,Nest 提供了多种方式来自订 Provider,都是透过展开式进行定义:
这类型的 Provider 主要是用来:
class
抽换成特定的模拟版本。那要如何使用呢?在展开式中使用 useValue
来配置。这里以 app.module.ts
为例:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [
{
provide: AppService,
useValue: {
name: 'HAO'
}
}
],
})
export class AppModule {}
修改 app.controller.ts
来查看 token
为 AppService
的内容为何:
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {
console.log(this.appService);
}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
会发现注入的 AppService
变成我们指定的物件,会在终端机看到结果:
{ name: 'HAO' }
事实上,Provider 的 token
不一定要使用 class
,Nest 允许使用以下项目:
string
symbol
enum
这边同样以 app.module.ts
为例,我们指定 token
为字串 HANDSOME_MAN
,并使用 HAO
作为值:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [
AppService,
{
provide: 'HANDSOME_MAN',
useValue: 'HAO'
}
],
})
export class AppModule {}
在注入的部分需要特别留意,要使用 @Inject(token?: string)
装饰器来取得。这里以 app.controller.ts
为例:
import { Controller, Get, Inject } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
@Inject('HANDSOME_MAN') private readonly handsome_man: string
) {
console.log(this.handsome_man);
}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
会发现注入的 HANDSOME_MAN
即为指定的值,在终端机会看到:
HAO
提醒:通常会把这类型的
token
名称放在独立的档案里,好处是当有其他地方需要使用的时候,可以直接取用该档案里的内容,而不需要再重写一次token
的名称。
这类型的 Provider 最典型的用法就是让 token
指定为抽象类别,并使用 useClass
来根据不同环境提供不同的实作类别。这里以 app.module.ts
为例:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoModule } from './features/todo/todo.module';
import { TodoService } from './features/todo/todo.service';
class HandSomeMan {
name = 'HAO';
}
class TestHandSomeMan {
name = 'HAO';
}
@Module({
imports: [TodoModule],
controllers: [AppController],
providers: [
AppService,
{
provide: TodoService,
useClass: process.env.NODE_ENV === 'production' ? HandSomeMan : TestHandSomeMan
}
],
})
export class AppModule {}
提醒:如果没有建立
TodoService
的话,先建立TodoModule
并将其汇出;如果已经建立的话,也需要留意有没有汇出呦。
稍微改写一下 app.controller.ts
:
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { TodoService } from './features/todo/todo.service';
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
private readonly todoService: TodoService
) {
console.log(this.todoService);
}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
如果环境变数 NODE_ENV
不等於 production
的话,会在终端机看到下方结果:
TestHandSomeMan { name: 'HAO' }
这类型的 Provider 使用工厂模式让 Provider 更加灵活,透过 注入其他依赖 来变化出不同的实例,是很重要的功能。使用 useFactory
来指定工厂模式的函数,并透过 inject
来注入其他依赖。以 app.module.ts
为例:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
class MessageBox {
message: string;
constructor(message: string) {
this.message = message;
}
}
@Module({
imports: [],
controllers: [AppController],
providers: [
AppService,
{
provide: 'MESSAGE_BOX',
useFactory: (appService: AppService) => {
const message = appService.getHello();
return new MessageBox(message);
},
inject: [AppService]
}
],
})
export class AppModule {}
稍微修改一下 app.controller.ts
:
import { Controller, Get, Inject } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
@Inject('MESSAGE_BOX') private readonly messageBox
) {
console.log(this.messageBox);
}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
会在终端机看到下方结果:
MessageBox { message: 'Hello World!' }
这个 Provider 主要就是替已经存在的 Provider 取别名,使用 useExist
来指定要使用哪个 Provider。以 app.module.ts
为例:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [
AppService,
{
provide: 'ALIAS_APP_SERVICE',
useExisting: AppService
}
],
})
export class AppModule {}
这样就会把 ALIAS_APP_SERVICE
指向到 AppService
的实例。这里修改一下 app.controller.ts
做验证:
import { Controller, Get, Inject } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
@Inject('ALIAS_APP_SERVICE') private readonly alias: AppService
) {
console.log(this.alias === this.appService); // 进行比对
}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
会发现两个参数是相等的,在终端机看到的结果为:
true
Provider 是非常重要的机制,要用一篇的幅度来介绍它实在不太够,剩下的部分会在下篇做说明,这里就先给大家今天的懒人包:
useValue
、useClass
、useFactory
、useExist
。token
可以是:string
、symbol
、enum
。
Cookie与session是web开发常需要使用的玩意 先来个cookie的范例程序 packag...
结语 完成了连续一个月的铁人赛了!当初觉得每天发一篇应该不会太难,甚至还在开赛前屯了四篇,结果事...
本篇同步发布於个人Blog: [PoEAA] Domain Logic Pattern - Serv...
reset 回推 使用Git的一大好处就是,当我的程序在改动的过程中发生了难以修复的错误,我们可以透...
在 Day03 我们使用 GCE 建立一台 VM,今天要学习如何连线到虚拟机,并在服务器上使用 No...