[NestJS 带你飞!] DAY06 - Provider (上)

前一篇有提到 Provider 与 Module 之间有很核心的机制,该机制使用了 依赖注入 的概念。这边会先针对依赖注入及 Nest 如何将其融入进行解释,再针对 Provider 的使用方式做说明,如此一来会对 Provider 有更深度的理解,在学习上比较不会满头问号,那就废话不多说赶快开始吧!

依赖注入 (Dependency Injection)

依赖注入是一种设计方法,透过此方式可以大幅降低耦合度,来个简单的例子吧,假设有两个 class 分别叫 ComputerCPU

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);

Nest 的依赖注入机制

不过依赖注入跟 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 {}

可以看到变成了一个物件,该物件的 providetokenuseClass 则是指定使用的 class 为何,进而建立实例。

Provider

Provider 透过控制反转容器做实例的管理,可以很方便且有效地使用这些 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

如果觉得标准 Provider 无法满足需求,如:

  • 想自行建立一个实例,而不是透过 Nest 建立。
  • 想要在其他依赖项目中重用实例。
  • 使用模拟版本的 class 进行覆写,以便做测试。

没关系,Nest 提供了多种方式来自订 Provider,都是透过展开式进行定义:

Value Provider

这类型的 Provider 主要是用来:

  • 提供常数 (Constant)。
  • 将外部函式库注入到控制反转容器。
  • 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 来查看 tokenAppService 的内容为何:

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' }

非类别型 token

事实上,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 的名称。

Class Provider

这类型的 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' }

Factory Provider

这类型的 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!' }

Alias Provider

这个 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 是非常重要的机制,要用一篇的幅度来介绍它实在不太够,剩下的部分会在下篇做说明,这里就先给大家今天的懒人包:

  1. Provider 与 Module 之间有依赖注入机制的关系。
  2. 透过控制反转容器管理 Provider 实例。
  3. Provider 分为 标准 Provider 与 自订 Provider。
  4. 自订 Provider 使用展开式。
  5. 有四种方式提供自订 Provider:useValueuseClassuseFactoryuseExist
  6. Provider 的 token 可以是:stringsymbolenum

<<:  Day09 - 网站开发从Django开始

>>:  类比数位转换模组

Golang 转生到web世界 - Cookie与session

Cookie与session是web开发常需要使用的玩意 先来个cookie的范例程序 packag...

【Day 30】- 结语 : 从 0 开始的网路爬虫

结语   完成了连续一个月的铁人赛了!当初觉得每天发一篇应该不会太难,甚至还在开赛前屯了四篇,结果事...

[PoEAA] Domain Logic Pattern - Service Layer

本篇同步发布於个人Blog: [PoEAA] Domain Logic Pattern - Serv...

Day 3 : Git 回推版本

reset 回推 使用Git的一大好处就是,当我的程序在改动的过程中发生了难以修复的错误,我们可以透...

Day04 - 在 GCE 建立第一个 Web Service

在 Day03 我们使用 GCE 建立一台 VM,今天要学习如何连线到虚拟机,并在服务器上使用 No...