nestjs微服务-系列2

系列文章1: juejin.cn/post/752267...

使用consul进行服务注册发现

consul初始化

在根目录创建 consul 相关的文件夹,在 /consul/config 文件夹中增加配置文件 consul.hcl

ini 复制代码
datacenter = "dc1"
data_dir = "/consul/data"
log_level = "INFO"
server = true
bootstrap_expect = 1
ui = true

# Enable the Consul UI
connect {
  enabled = true
}

acl {
  enabled = true
  default_policy = "deny"
  down_policy = "extend-cache"
  tokens {
    master = "123456"  # 注意这里,这里是登录consul配置的密token
  }
}

然后在 consul 目录下增加 docker-compose.yml文件

yml 复制代码
services:
  consul:
    image: hashicorp/consul
    container_name: consul
    ports:
      - 8500:8500
      - 8600:8600/udp
    volumes:
      - ./data:/consul/data
      - ./config:/consul/config
    command:
      [
        'agent',
        '-server',
        '-ui',
        '-client=0.0.0.0',
        '-bootstrap-expect=1',
        '-data-dir=/consul/data',
        '-config-dir=/consul/config',
      ]
    environment:
      - CONSUL_BIND_INTERFACE=eth0

在 consul 文件夹下,执行 docker 命令启动 consul 服务

复制代码
docker-compose up -d

启动完成之后,服务器访问 127.0.0.1:8500出现如下界面则启动成功

选择登录,输入上面配置的 token,这里配置的是 123456

github.com/0x0bit/nest...

因为 consul这个 npm 包已经不在更新了,而且我没有找到更好的 npm 包,而 consul 官方提供了对应的 API,所以我们自己将 consul 封装成 lib 模块。

consul 模块封装

vbnet 复制代码
nest g lib consul

libs/
├── consul/
│   ├── interfaces
│   │   ├── consul-module.interface.ts
│   │   ├── consul-service.interface.ts
│   ├── consul.module.ts
│   ├── consul.module.ts
│   ├── consul.service.ts
│   ├── consul.constants.ts
│   └── index.ts

定义 interface

consul-service.interface.ts

typescript 复制代码
export interface ServiceRegisterOptions {
  ID: string; // 服务唯一标识
  Name: string; // 服务名称
  Address: string; // host/ip 服务地址
  Port: number; // 服务端口
  TAgs?: string[];
  /**
   * 健康检查配置,可自行扩展更多字段
   * 这里只演示 HTTP Check
   */
  Check?: {
    path: string; // '/health'
    interval: string; // '10s'
  };
}

export interface NodeService {
  Node: {
    ID: string;
    Node: string;
    Address: string;
    Datacenter: string;
    TaggedAddresses: string | null;
    Meta: Record<string, any>;
  };
  Service: {
    ID: string;
    Service: string;
    Tags: string[];
    Address: string;
    TaggedAddresses: Record<string, any>;
    Port: number;
  };
}

export interface ResolveAddress {
  address: string;
  port: number;
}

consul-module.interface.ts

typescript 复制代码
import { ServiceRegisterOptions } from './consul-service.interface';

export interface ConsulModuleOptions {
  /** Consul 服务器地址(含协议和端口)例如 'http://127.0.0.1:8500' */
  consulHost: string;
  /** consul登录token */
  token?: string;
  /** 当前服务注册信息 */
  service?: ServiceRegisterOptions;
}

consul.constants.ts

typescript 复制代码
export const CONSUL_OPTIONS = 'CONSUL_OPTIONS';

consul.module.ts

consul.module.ts文件中,实现同步注册方法和异步注册方法

typescript 复制代码
import { DynamicModule, Global, Module, Provider } from '@nestjs/common';
import { CONSUL_OPTIONS } from './consul.constants';
import { ConsulService } from './consul.service';
import { ConsulModuleOptions } from './interfaces/consul-module.interface';

@Global()
@Module({})
export class ConsulModule {
  /** 同步注册 */
  static forRoot(options: ConsulModuleOptions): DynamicModule {
    return {
      module: ConsulModule,
      providers: [
        { provide: CONSUL_OPTIONS, useValue: options },
        ConsulService,
      ],
      exports: [ConsulService],
    };
  }

  /** 异步注册(从 ConfigService、ENV 等拿配置) */
  static forRootAsync(
    optionsFactory: () => Promise<ConsulModuleOptions> | ConsulModuleOptions,
  ): DynamicModule {
    const asyncOptionsProvider: Provider = {
      provide: CONSUL_OPTIONS,
      useFactory: optionsFactory,
    };

    return {
      module: ConsulModule,
      providers: [asyncOptionsProvider, ConsulService],
      exports: [ConsulService],
    };
  }
}

consul.service.ts

因为调用 consul 的 api,因此,需要安装 got

复制代码
npm install got

然后修改 tsconfig.ts的配置项 "module": "NodeNext",否则 got 会报错。

consul.service.ts 中,增加服务注册和发现的方法.

typescript 复制代码
import {
  Inject,
  Injectable,
  Logger,
  OnModuleDestroy,
  OnModuleInit,
} from '@nestjs/common';
import type { Got } from 'got';
import got from 'got';
import { CONSUL_OPTIONS } from './consul.constants';
import { ConsulModuleOptions } from './interfaces/consul-module.interface';
import {
  NodeService,
  ResolveAddress,
  ServiceRegisterOptions,
} from './interfaces/consul-service.interface';

@Injectable()
export class ConsulService implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger('ConsulService');
  private readonly request: Got;
  private readonly defServiceRegisterOptions: ServiceRegisterOptions;

  constructor(@Inject(CONSUL_OPTIONS) opts: ConsulModuleOptions) {
    this.request = got.extend({
      prefixUrl: `${opts.consulHost}/v1`,
      responseType: 'json',
      resolveBodyOnly: true,
      headers: {
        'X-Consul-Token': opts.token,
      },
    });

    // ✅ 只有传了 service 配置才保存
    if (opts.service) {
      this.defServiceRegisterOptions = opts.service;
    }
  }

  /** 🟢 模块启动时自动注册 */
  async onModuleInit() {
    // 因为在gateway中,不需要注册服务,只需要初始化consul就行
    if (this.defServiceRegisterOptions) {
      await this.registerService();
    }
  }

  /** 🔴 应用关闭时自动注销 */
  async onModuleDestroy() {
    if (this.defServiceRegisterOptions) {
      await this.deregisterService();
    }
  }

  /**
   * 注册服务
   */
  async registerService(opts?: ServiceRegisterOptions) {
    const registerPayload = { ...this.defServiceRegisterOptions, ...opts };
    const serviceName = registerPayload.Name;
    try {
      await this.request.put('agent/service/register', {
        json: registerPayload,
      });
      this.logger.log(`[Consul] Service [${serviceName}] registered successfully`);
    } catch (error) {
      this.logger.error(`[Consul] Error registering service ${serviceName}: ${error.message}`,);
    }
    return true;
  }

  /**
   * 服务注销:注销注册到 Consul 的服务
   * @param serviceId 服务唯一标识
   */
  async deregisterService(serviceId?: string) {
    serviceId = serviceId ? serviceId : this.defServiceRegisterOptions.ID;
    try {
      await this.request.put(`agent/service/deregister/${serviceId}`);
      this.logger.log( `[Consul] Service [${serviceId}] deregistered successfully`,);
    } catch (error) {
      this.logger.error(`[Consul] Error deregistering service ${serviceId}: ${error.message}`,);
    }
  }

  /**
   * 服务发现:通过 Consul 查找健康服务
   * @param serviceName 服务名称
   */
  async resolveAddress(serviceName: string): Promise<ResolveAddress> {
    const res = await this.request
      .get(`health/service/${serviceName}?passing=true`)
      .json<NodeService[]>();

    if (!res.length) {
      throw new Error(`No healthy instance found for ${serviceName}`);
    }
    const node = res[0].Service;
    this.logger.log(`[Consul] Service [${serviceName}] get resolveAddress successfully. [${node.Address}:${node.Port}]`);
    return { address: node.Address, port: node.Port };
  }
}

github.com/0x0bit/nest...

服务注册

现在我们的 consul 模块就已经封装好了,只需要对应的微服务中进行调用就可以了。

分别修改 user-serviceorder-serviceapp.module.ts文件,让服务在启动的时候就注册到 consul 中。

typescript 复制代码
import { ConsulModule } from '@libs/consul';
import { Module } from '@nestjs/common';
import { UserModule } from './user/user.module';

@Module({
  imports: [
    ConsulModule.forRoot({
      consulHost: 'http://127.0.0.1:8500',
      token: '123456',
      service: {
        ID: 'user-service-id',
        Name: 'user-service',
        Address: '127.0.0.1',
        Port: 3001,
      },
    }),
    UserModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

order-service 只需要替换对应的 service 信息就行。

服务发现

在 gateway 中,我们需要发现我们需要调用的 service,因此,我们需要在 gateway 的 src 目录下,创建 micro-clients.module.ts

typescript 复制代码
import { ConsulModule, ConsulService } from '@libs/consul';
import { ResolveAddress } from '@libs/consul/interfaces/consul-service.interface';
import { DynamicModule, Module } from '@nestjs/common';
import { ClientsModule, GrpcOptions, Transport } from '@nestjs/microservices';
import { ClientsProviderAsyncOptions } from '@nestjs/microservices/module/interfaces/clients-module.interface';
import { join } from 'path';
import { USER_PACKAGE_NAME } from 'proto/generated/user.interface';
import { ORDER_PACKAGE_NAME } from '../../../proto/generated/order.interface';

/**
 * 获取grpc服务连接
 * @param packageName 服务包名
 * @param serviceName 服务名称
 * @param protoFile   服务所调用的proto文件名称
 */
function createGrpcClient(packageName: string, serviceName: string, protoFile: string): ClientsProviderAsyncOptions {
  return {
    name: packageName,
    inject: [ConsulService],
    useFactory: async (consul: ConsulService): Promise<GrpcOptions> => {
      // 通过 consul 的服务发现方法获取对应服务的address和port
      const svc: ResolveAddress = await consul.resolveAddress(serviceName);
      if (!svc) throw new Error(`${serviceName} 不可用`);
      return {
        transport: Transport.GRPC,
        options: {
          url: `${svc.address}:${svc.port}`,
          package: packageName,
          protoPath: join(process.cwd(), 'proto', protoFile),
        },
      };
    },
  };
}

@Module({})
export class MicroClientsModule {
  static register(): DynamicModule {
    return {
      module: MicroClientsModule,
      global: true, // 标记为全局包
      imports: [
        ConsulModule,
        // 通过 ClientsModule 对需要调用的服务进行注册
        ClientsModule.registerAsync([
          createGrpcClient(USER_PACKAGE_NAME, 'user-service', 'user.proto'),
          createGrpcClient(ORDER_PACKAGE_NAME, 'order-service', 'order.proto'),
        ]),
      ],
      exports: [ClientsModule],
    };
  }
}

然后,我们在 app.module.ts中,进行服务注册,让服务启动的时候,就对我们所有依赖的服务进行发现。

typescript 复制代码
import { ConsulModule } from '@libs/consul';
import { Module } from '@nestjs/common';
import { MicroClientsModule } from './micro-clients.module';
import { UserModule } from './user/user.module';

@Module({
  imports: [
    // 初始化consul
    ConsulModule.forRoot({
      consulHost: 'http://127.0.0.1:8500',
      token: '123456',
    }),
    // 从consul中获取注册的微服务
    MicroClientsModule.register(),
    UserModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

然后,我们需要删掉之前在 user.module.tsClientsModule.register相关的内容。变成:

typescript 复制代码
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';

@Module({
  imports: [],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

然后先启动 user-service 和 order-service,查看 consul 的服务,这里发现已经注册成功。

最后启动 gateway,调用接口就 OK 了。

github.com/0x0bit/nest...

github.com/0x0bit/nest...

相关推荐
pe7er42 分钟前
使用 Vue 官方脚手架创建项目时遇到 Node 18 报错问题的排查与解决
前端·javascript·vue.js
绝无仅有42 分钟前
对接三方SDK开发过程中的问题排查与解决
后端·面试·架构
pe7er1 小时前
使用 types / typings 实现全局 TypeScript 类型定义,无需 import/export
前端·javascript·vue.js
islandzzzz1 小时前
(第二篇)HMTL+CSS+JS-新手小白循序渐进案例入门
前端·javascript·css·html
喝拿铁写前端1 小时前
前端实战优化:在中后台系统中用语义化映射替代 if-else,告别魔法数字的心智负担
前端·javascript·架构
超人不会飛2 小时前
就着HTTP聊聊SSE的前世今生
前端·javascript·http
蓝胖子的多啦A梦2 小时前
Vue+element 日期时间组件选择器精确到分钟,禁止选秒的配置
前端·javascript·vue.js·elementui·时间选选择器·样式修改
夏天想2 小时前
vue2+elementui使用compressorjs压缩上传的图片
前端·javascript·elementui
The_cute_cat2 小时前
JavaScript的初步学习
开发语言·javascript·学习
海天胜景2 小时前
vue3 el-table 列增加 自定义排序逻辑
javascript·vue.js·elementui