Nest-Provider 建立实例的原理与Provider的种类你都了解吗?

654 阅读6分钟

前言

文章看着长主要是代码占地多...

最主要的是虽然代码贴的多,但是大部分代码没怎么改,几乎所有都是只改了Provider .....

所以大家不要害怕,这个比前面两章都简单~~

这是先修,要先有以下这3篇的概念才能看懂哦~~

Nest 模组化

学好 Controller

next.js-- 学好 Module

依赖注入 (Dependency Injection)

依赖注入是一种设计方法,透过此方式可以大幅降低耦合度。

依赖注入跟 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

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService //写了一个 `AppService` 就指定了`token`
 { provide: AppService, useClass: AppService }可以看到变成了一个物件,
 //该物件的 provide 即 tokenuseClass 则是指定使用的 class 为何,进而建立实例。
  ],
})
export class AppModule { }

provide原理{ provide: AppService, useClass: AppService }可以看到变成了一个物件, 该物件的 provide 即 tokenuseClass 则是指定使用的 class 为何,进而建立实例。

Provider

Provider 透过控制反转容器做实例的管理,可以很方便且有效地使用这些 Provider,而 Provider 大致上可以分成两种:

1. 标准Provider

在 class 上添加*@Injectable*让 Nest 知道这个 是可以由控制反转容器管理的,透过 Nest 建立

nest generate service <SERVICE_NAME>

image.png

image.png

2.自定义 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 来查看 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' }

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

非类别型 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 的名称。

导出自定义provider

我们把 Custom Provider 的 展开式用变数储存起来,再将该展开式放到 providers 与 exports 中:

nest-mvc-provider-export.gif

import { Module } from '@nestjs/common';

const HANDSOME_HAO = {
    provide: 'HANDSOME_MAN',
    useValue: {
        name: 'HAO'
    }
};

@Module({
    providers: [
        HANDSOME_HAO
    ],
    exports: [
        HANDSOME_HAO
    ]
})
export class HandsomeModule { }

在 AppModule 进行汇入,下方为 app.module.ts 的范例:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { HandsomeModule } from './handsome/handsome.module';

@Module({
    imports: [HandsomeModule],
    controllers: [AppController],
    providers: [AppService],
})
export class AppModule { }

修改 app.controller.ts 查看结果,会在终端机看到:

{ name: 'HAO' }

非同步 Provider

有时候可能需要等待某些非同步的操作来建立 Provider,比如:需要与资料库连线,Nest App 会等待该 Provider 建立完成才正式启动。这边以上方的范例来做修改,调整一下 handsome.module.ts 的内容:

import { Module } from '@nestjs/common';

const HANDSOME_HAO = {
    provide: 'HANDSOME_MAN',
    useFactory: async () => {
        const getHAO = new Promise(resolve => {
            setTimeout(() => resolve({ name: 'HAO' }), 2000);
        });
        const HAO = await getHAO;
        return HAO;
    }
};

@Module({
    providers: [
        HANDSOME_HAO
    ],
    exports: [
        HANDSOME_HAO
    ]
})
export class HandsomeModule { }

在等待两秒后,终端机会出现下方结果:

{ name: 'HAO' }

自选式 Provider

有时候可能会有 Provider 没有被提供但却注入的情况,这样在启动时会报错,因为 Nest 找不到对应的 Provider,那遇到这种情况该如何处理呢?首先,遇到这类型情况通常会给个预设值代替没被注入的 Provider,然后要在注入的地方添加 @Optional 装饰器。这里我们同样沿用上方范例,去修改 app.module.ts 的内容,将 HandsomeModule 移除汇入:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
    imports: [],
    controllers: [AppController],
    providers: [AppService],
})
export class AppModule { }

接着去修改 app.controller.ts 的内容,替 HANDSOME_MAN 给定预设值:


import { Controller, Get, Inject, Optional } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
    constructor(
        private readonly appService: AppService,
        @Optional() @Inject('HANDSOME_MAN') private readonly handsomeMan = { name: '' }
    ) {
        console.log(this.handsomeMan);
    }

    @Get()
    getHello(): string {
        return this.appService.getHello();
    }
}

此时的终端机会显示结果:

{ name: '' }

3.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 { }

稍微改写一下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' }

4.Factory Provider

这类型的 Provider 使用工厂模式让 Provider 更加灵活,透过 注入其他依赖 来变化出不同的实例,是很重要的功能。使用** useFactory 来指定工厂模式的函数**,并透过** inject 来注入其他依赖**。以 app.module.ts 为例:

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

小结

  1. Provider 与 Module 之间有依赖注入机制的关系。透过控制反转容器管理 Provider 实例。
  2. 有四种方式提供自定义 Provider:useValueuseClassuseFactoryuseExist
  3. Provider 的 token 可以是:stringsymbolenum

Provider 小技巧:

  1. 自定义 Provider 可以透过把展开式抽离至变数来进行汇出
  2. Provider支援非同步 建立在尚未建立完 Provider 以前,Nest App 不会正式启动
  3. 如果 Provider 并不是必须项目,必须在注入的地方添加 @Optional,这边也建议替该参数设置预设值

下期讲Exception & Exception filters,下期见啦~~