Skip to content

服务提供者

NestJs的设计是划分不同的模块,每一块业务可以放到一个模块里面

每一个模块有它的独立作用域,模块之间是封闭的,这个模块里面的控制器和服务都只是属于这个模块的,但是模块的某些东西要提供出去使用,这时模块就要往外提供接口(总之,模块默认是独立的,但是也要暴露出一些接口给其他模块来使用(@Injectable()装饰器将类变成提供者))

在模块定义文件app.module.ts中,这一块就是服务提供者,模块提供装饰器注册后,会有一个容器(providers)来管理服务提供者,我们要把服务提供者(AppService)放到我们的容器中:

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

注册服务的完整语法应该如下所示:

ts
providers: [
    {
        provide: AppService,    // 注册服务的名字,可以进行修改,如改为'newName'
        useClass: AppService,
    },
],

服务我们可以将其想象成一个对象,控制器在使用这个服务的时候,就是拿keyAppService去容器中找对应的方法,provide就是对象中的key,我们可以对其名字进行修改,但是在控制器中构造函数调用的时候依赖注入部分就要做相应的修改,使用装饰器进行依赖注入:

ts
constructor(
 @Inject('newName')
 private readonly appService: AppService
) {}

appService: AppService这两个名字不能一样,前面的是对象属性,后面的是类型修饰

但是这里AppService没有了key值匹配的作用,但是还有提供类型支持的作用

但是我们一般都不会写完整的注册语法,会进行简写

在控制器文件中进行依赖注入后,回去模块的容器中查找,如果没有这个服务提供者,就会报错,提示找不到这个服务,所以我们一定要将服务放到容器中

新服务的创建

创建一个新服务,使用命令行进行创建:nest g s new --no-spec -d

  • g s:创建服务
  • --no-spec:不创建测试文件
  • -d表示只是返回执行的结果,不会实现真正的服务创建,想要真正的创建服务,要将-d去掉

执行结果:在src/new/new.service.ts中创建一个新的服务,并更新到根模块的容器中,如果我们不想要子目录,我们可以加上--flat,具体修改为nest g s new --no-spec --flat -d

这样的执行结果为:在src/new.service.ts中创建一个新的服务,并更新到根模块的容器中:

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

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService, NewService],       // 将服务放到容器中
})
export class AppModule {}

之后我们就可以在新的服务中编写我们的业务方法:

ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class NewService {
    newFunction(){
      return 'Hello!';
    }
}

我们通常在app.service.ts(该文件可以理解为汇聚服务业务的主文件)文件中自定义服务依赖的注入

ts
import { Injectable } from '@nestjs/common';
import { NewService } from './new.service';

@Injectable()
export class AppService {
  constructor(private readonly newF: NewService) {}
  getHello(){
    return this.newF.newFunction();
  }
}

@Injectable()装饰器将类变成服务提供者,会自动的分析类中的构造函数constructor,将依赖实现出来

控制器文件不做修改,还是引用AppService服务:

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

使用域名访问:http://localhost:3000进行访问,网页中会显示Hello!


基本类型提供者注册

普通值类型也可以进行提供者的注册,也就是说,在模块容器中,我们可以自定义提供者,在模块文件中自定义提供者:

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

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService, 
              {   // 自定义基本类型的服务提供者
                  provide: 'info',
                  useValue: {
                      name: 'jlc',
                      age: '24',
                  },
              },
             ],
})
export class AppModule {}

可以将这个服务写在外面,使逻辑更加清晰:

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

const infoService = {
  provide: 'info',
  useValue: {
      name: 'jlc',
      age: '24',
  },
}

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService, infoService],
})
export class AppModule {}
  • useClass 期望的是一个类,而不是一个对象字面量。 即用于提供类类型的依赖项
  • useValue用于提供具体值(如对象字面量、字符串、数字等)
  • useFactory用于提供通过工厂函数创建的依赖项
  • useExisting用于提供现有提供者的别名

如果我们想要进行使用,在服务文件/控制器文件(不推荐,业务逻辑一般放服务文件中)中进行依赖注入即可:

ts
import { Inject, Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  constructor(@Inject('info') private info: any) {}
  getHello(): string {
    return 'Hello World!' + this.info.name;
  }
}

@Inject('info')表示去容器中找到这个key值的服务提供者

业务逻辑编写完成后,提供的业务在网站中显示为:Hello World!jlc


动态注册服务提供者

服务是可编程的,我们可以根据不同的场景来注册不同的服务(如:开发的时候使用一个服务,上线的时候使用另一个服务)

在模块文件中动态注册服务提供者,我们需要根据环境变量进行动态的设置,在根目录下新建.env文件,定义目前是一个开发环境:

txt
NODE_ENV=development

在模块文件中尝试进行读取环境变量,并实现动态注册服务提供者

读取环境变量时,我们需要安装一个插件npm install dotenv,后续环境变量一般使用第三方的包会比较简单

ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { DevService } from './dev.service';
import { config } from 'dotenv';
import { join } from 'path';

config({ path: join(__dirname, ',,/.env') })

const configService = {
  provide: 'configService',
  // 动态注册:根据环境变量选择哪个服务
  useClass: process.env.NODE_ENV == 'development' ? DevService : AppService,
}

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

import { config } from 'dotenv'只是从dotenv包中导出了config一个内容,如果我们想要包中的所有内容进行导出,统一使用一个变量来接收,我们需要修改为import dotenv from 'dotenv',但是实际上ts是不支持的,我们要去tsconfig.json中进行配置,添加一个配置项:"esModuleInterop": true

配置完后,就可以使导出的内容进行一块接收,使用的时候dotenv.即可

这样读取环境变量的代码就可以修改为:

ts
import dotenv from 'dotenv';
import path from 'path';

dotenv.config({ path: path.join(__dirname, ',,/.env') })

后续发展:import * as dotenv from 'dotenv'; 的方式引入库也可以直接全部导入,不用额外配置ts

在开发环境下使用DevService,在上线环境下使用AppService

具体的两个服务为:

ts
// app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
get() {
 return 'AppService get';
}
}
ts
// dev.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class DevService {
get() {
 return 'DevService get';
}
}

在控制器文件中进行注册:

ts
import { Controller, Get, Inject } from '@nestjs/common';

@Controller()
export class AppController {
  constructor(@Inject('configService') private readonly configService) {}

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

在网页中,会显示DevService get,如果.env配置文件NODE_ENV不为development,那么网页中显示AppService get

根据环境动态的选择配置项

对于开发环境和生产环境,可能有不同的配置项,我们可以将这些配置项全部放在src/config文件夹中,创建用于开发环境的配置项development.config.ts

ts
export const developmentConfig = {
    url: 'localhost',
    port: 5000
}

线上环境的配置项production.config.ts

ts
export const productionConfig = {
    url: '6.6.6.6',
    port: 3000
}

在根目录下新建.env文件,定义目前是一个开发环境:

txt
NODE_ENV=development

在模块文件中进行环境变量的加载和配置项的加载:配置项加载的时候是使用它的值而不是类,要用到useValue

ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { developmentConfig } from './config/development.config';
import { productionConfig } from './config/production.config';
import { AppService } from './app.service';
import dotenv from 'dotenv';
import path from 'path';

dotenv.config({ path: path.join(__dirname, ',,/.env') })

const ConfigService = {
  provide: 'ConfigService',
  useValue: process.env.NODE_ENV == 'development' ? developmentConfig : productionConfig,
}

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

其中app.service.ts的内容为:在服务中动态读取配置

ts
import { Inject, Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  constructor(
    @Inject('ConfigService') private readonly ConfigService: Record<string, any>
  ) {}
  getHello(): string {
    return 'Hello World!' + `[${this.ConfigService.url}]`;
  }
}

Record<string, any>表示使用类型工具进行定义,表示定义的是一个对象,其值是任意的类型

在控制器中,还是使用AppService进行服务的调用:

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

网站中显示的结果为:Hello World![localhost]

如果环境变量发生了改变,不是development了,网站中的内容就变成了:Hello World![6.6.6.6]

这样,就实现了根据环境,动态的选择我们的配置项


工厂函数注册提供者

如果一个服务需要依赖其他的服务,如数据库服务调用配置项服务,这时我们可以把它定义成工厂函数

创建一个数据库服务:nest g s db --no-spec --flat

db.service.ts服务用于连接我们的数据库,具体代码如下:

ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class DbService {
    constructor(private readonly options: Record<string, any>) {}
    public connect(){
      return `<h1 style="background:red">连接数据库 - 主机:${this.options.url}</h1>`;
    }
}

模块文件的具体代码:

ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { developmentConfig } from './config/development.config';
import { productionConfig } from './config/production.config';
import { AppService } from './app.service';
import { DbService } from './db.service';
import dotenv from 'dotenv';
import path from 'path';

dotenv.config({ path: path.join(__dirname, ',,/.env') })

const ConfigService = {
  provide: 'ConfigService',
  useValue: process.env.NODE_ENV == 'development' ? developmentConfig : productionConfig,
}

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService, ConfigService, {
      provide: 'DbService',
      inject: ['ConfigService'],
      useFactory(configService) {
        return new DbService(configService)    // 返回一个DbService的实例
      },
  }],
})
export class AppModule {}

我们要在数据库中使用配置项的信息,因此DbService服务要依赖ConfigService服务,我们要使用工厂函数来实现,数据库连接的参数在ConfigService服务中,使用inject: ['ConfigService']会将ConfigService服务作为工厂函数的参数传递过来

inject 属性可以注入多个依赖项。在这种情况下,工厂函数的参数将是一个包含这些依赖项的数组,依赖项的顺序与inject数组中的顺序相同。 多个依赖项的示例 假设我们有一个 AppModule,其中 DbService 依赖于 ConfigService 和另一个服务 OtherService。可以如下设置 inject 属性和工厂函数:

ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { ConfigService } from './config.service';
import { DbService } from './db.service';
import { OtherService } from './other.service';

@Module({
imports: [],
controllers: [AppController],
providers: [ConfigService, OtherService,
 {
   provide: 'DbService',
   inject: [ConfigService, OtherService], // 注入 ConfigService 和 OtherService
   useFactory: (configService: ConfigService, otherService: OtherService) => {
     // 使用 ConfigService 和 OtherService 创建 DbService 实例
     return new DbService(configService, otherService);
   },
 },
],
})
export class AppModule {}

但是在实际的开发中,我们会将其变成模块来开发

在控制器中,引入放在容器中的服务:

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

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    @Inject('DbService')
    private readonly dbService: DbService
  ) {}

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

网页中显示的内容:连接数据库 - 主机:localhost,如果环境变量的值不是development,网页中显示:连接数据库 - 主机:6.6.6.6


异步服务提供者

服务在容器注册的时候,可以支持异步操作,app.module.ts的代码如下所示:

ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { developmentConfig } from './config/development.config';
import { productionConfig } from './config/production.config';
import { AppService } from './app.service';
import { DbService } from './db.service';
import dotenv from 'dotenv';
import path from 'path';

dotenv.config({ path: path.join(__dirname, ',,/.env') })

const ConfigService = {
  provide: 'ConfigService',
  useValue: process.env.NODE_ENV == 'development' ? developmentConfig : productionConfig,
}

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
      AppService, 
      ConfigService, 
      // 服务进行异步注册
      {
          provide: 'DbService',
          inject: ['ConfigService'],
          useFactory: async (configService) => {
            return new new Promise((r) => {
                setTimeout(() => {
                    r('abc')
                }, 3000)
            })
          },
      }
  ],
})
export class AppModule {}

控制器文件在调用时进行修改,具体文件app.controller.ts修改为:

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

@Controller()
export class AppController {
  constructor(private readonly appService: AppService,
              @Inject('DbService')
              private readonly dbService: string) {}

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

项目在启动的时候,服务在进行注册的时候是有3秒的异步等待的,注册完后,在网页中还是正常显示abc

服务实例在进行返回的时候,不是进行同步返回的,如查询数据库操作,就是一个典型的异步操作

Released under the MIT License.