NestJS 微服务示例

项目生成

$ nest new nest-app -p yarn

该项目将作为主项目使用。

此时的目录结构为:

.
├── README.md
├── nest-cli.json
├── package.json
├── src
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── tsconfig.build.json
├── tsconfig.json

再生成另一个项目作为微服务项目:

$ cd nest-app
$ nest g app nest-service

此时的目录结构更新成了:

.
├── README.md
├── apps
│   ├── nest-app
│   │   ├── src
│   │   │   ├── app.controller.spec.ts
│   │   │   ├── app.controller.ts
│   │   │   ├── app.module.ts
│   │   │   ├── app.service.ts
│   │   │   └── main.ts
│   │   ├── test
│   │   │   ├── app.e2e-spec.ts
│   │   │   └── jest-e2e.json
│   │   └── tsconfig.app.json
│   └── nest-service
│       ├── src
│       │   ├── app.controller.spec.ts
│       │   ├── app.controller.ts
│       │   ├── app.module.ts
│       │   ├── app.service.ts
│       │   └── main.ts
│       ├── test
│       │   ├── app.e2e-spec.ts
│       │   └── jest-e2e.json
│       └── tsconfig.app.json
├── nest-cli.json
├── package.json
├── tsconfig.build.json
├── tsconfig.json

nest-cli.json

此时主程序 nest-app 和微服务 nest-service 同在一个仓库中,称为 monorepo,注意到根目录有个 nest-cli.josn 文件,可以配置 monorepo 的参数。

比如其中 root 指定了哪个项目是主项目,也可通过 tsConfigPath 为每个项目指定自己的 tsconfig.json 文件路径等。关于 nest-cli.json 的详细配置参见文档

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "apps/nest-app/src",
  "monorepo": true,
  "root": "apps/nest-app",
  "compilerOptions": {
    "webpack": true,
    "tsConfigPath": "apps/nest-app/tsconfig.app.json"
  },
  "projects": {
    "nest-app": {
      "type": "application",
      "root": "apps/nest-app",
      "entryFile": "main",
      "sourceRoot": "apps/nest-app/src",
      "compilerOptions": {
        "tsConfigPath": "apps/nest-app/tsconfig.app.json"
      }
    },
    "nest-service": {
      "type": "application",
      "root": "apps/nest-service",
      "entryFile": "main",
      "sourceRoot": "apps/nest-service/src",
      "compilerOptions": {
        "tsConfigPath": "apps/nest-service/tsconfig.app.json"
      }
    }
  }
}

微服务开发

下面开始改造 nest-service 项目使其提供微服务被调用的能力。

添加微服务依赖

$ yarn add @nestjs/microservices

创建微服务

修改 nest-service/main.ts 中 NestFactory.createNestFactory.createMicroservice,后者用于创建一个微服务实例。它接收两个参数,第一个和正常创建 nest app 一样,另一个则用于控制要创建的微服务的具体属性,比如端口,地址等。

import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    AppModule,
    {
      transport: Transport.TCP,
      options: {
        port: 4000,
      },
    },
  );

  await app.listen(() => {
    console.log(`nest service is listning`);
  });
}
bootstrap();

添加消息处理器

apps/nest-service/src/app.controller.ts

import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { AppService } from './app.service';

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

  @MessagePattern({ cmd: 'getHello' })
  getHello(name: string): string {
    return this.appService.getHello(name);
  }
}

这里 MessagePattern 定义了个消息处理器,它将监听并处理调用方发来的指令为 getHello 的消息。

其依赖的服务:

apps/nest-service/src/app.service.ts

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

@Injectable()
export class AppService {
  getHello(name: string): string {
    return `Hello ${name}!`;
  }
}

修改启动命令

启动单个项目

  • nest start nest-app
  • nest start nest-service

开发过程中因为需要从主项目调用微服务提供的服务,通过 concurrently 来同时启动两个项目,

$ yarn add -D concurrently
"start:dev": "concurrently --kill-others \"nest start nest-app --watch\" \"nest start nest-service --watch\"",
$ yarn start:dev

调用

要调用微服务,需要先初始化一个客户端对象。因为 nest 支持多种类型的微服务,所以提供 ClientProxy 对象作为统一的客户端,完成初始化之后使用者无需关心不同类型微服务的差异,该代理对象对外提供了统一的调用接口。

有多种方式可初始这样的客户端。推荐下方第一种方式,在根模块中注册后,整个模块中通过依赖注入方式使用,高效便捷。而后面两种方式不易共享也难测试。

依赖注入

apps/nest-app/src/app.module.ts

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

@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'NEST_SERVICE',
        transport: Transport.TCP,
        options: {
          port: 4000,
        },
      },
    ]),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

使用时通过 @Inject('NEST_SERVICE') 进行注入。

apps/nest-app/src/app.service.ts

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

@Injectable()
export class AppService {
  constructor(@Inject('NEST_SERVICE') private readonly client: ClientProxy) {}
  getHello(name: string): Promise<string> {
    return this.client.send<string>({ cmd: 'getHello' }, name).toPromise();
  }
}

工厂方法

apps/nest-app/src/app.service.ts

import { Injectable } from '@nestjs/common';
import {
  ClientProxy,
  ClientProxyFactory,
  Transport,
} from '@nestjs/microservices';

@Injectable()
export class AppService {
  private client: ClientProxy;
  constructor() {
    this.client = ClientProxyFactory.create({
      transport: Transport.TCP,
      options: {
        port: 4000,
      },
    });
  }
  getHello(name: string): Promise<string> {
    return this.client.send<string>({ cmd: 'getHello' }, name).toPromise();
  }
}

装饰器

apps/nest-app/src/app.service.ts

import { Injectable } from '@nestjs/common';
import { Client, ClientProxy, Transport } from '@nestjs/microservices';

@Injectable()
export class AppService {
  @Client({ transport: Transport.TCP, options: { port: 4000 } })
  private client: ClientProxy;

  getHello(name: string): Promise<string> {
    return this.client.send<string>({ cmd: 'getHello' }, name).toPromise();
  }
}

测试

$ curl "localhost:3000?name=niuwayong"
Hello niuwayong!⏎

参考