1.0.2 • Published 2 years ago

@tapnow/nestjs-common v1.0.2

Weekly downloads
-
License
UNLICENSED
Repository
-
Last release
2 years ago

TapNow-NestJS-Common

TapNow-NestJS-Common 的主要目标是为了各个服务聚焦于业务需求,避免各种模块初始化(如Logger,DataBase等)、公用服务等代码重复出现在各个微服务中。TapNow-NestJS-Common 提供一个CommonModule来初始化服务需要的模块,提供一些公用的装饰器和Service类来简化业务代码,以提高开发效率,减少维护成本。 CommonModule 是一个全局模块,默认会初始化ConfigModule, LoggerModule(nestjs-pino),HttpModule, SequelizeModule, 可根据微服务自身需要选择是否初始化RedisModule, RabbitMQModule

安装

yarn add tapnow-nestjs-common

配置

在初始化Module时,需要定义每个服务自己的配置项,比如数据库等。配置定义如下:

export interface ConfigOptions {
  namespaces: any[]
  schema: any
}

namespaces

ConfigModule允许定义和加载多个自定义配置文件,使用registerAs()函数返回namespaces配置对象,详细介绍可查看官方文档。配置主要分为TapNow-NestJS-Common各模块所需要的配置项和微服务自身业务所需要的配置项。 TapNow-NestJS-Common 详细配置如下:

export default registerAs('common', () => ({
  withCore: {
    pino: {
      // LoggerModule配置
    },
  },
  withSequelize: {
    // sequelize配置
  },
  withRedis: {
    // redis 配置
  },
  withRabbitMQ: {
    // rabbit MQ 配置
  },
  jwt: {
    secret: {
      client: 'XXXX',   // for web client user, x-auth-schema === CLIENT will use this secret
      merchant: 'XXXX', // for merchant user, x-auth-schema === MERCHANT will use this secret
      admin: 'XXXX',    // for admin user, x-auth-schema === ADMIN will use this secret
      api: 'XXXX',      // for partner backend, x-auth-schema === API will use this secret
      internal: 'XXXX', // for internal service, x-auth-schema === INTERNAL will use this secret
    },
  },
}))

配置文件示例可查看 test/config/common.config.ts

schema

如果未提供所需的环境变量或它们不符合某些验证规则,则在应用程序启动期间抛出异常是标准做法。详细信息可查看官方文档 schema示例可查看test/config/schema.ts

初始化

定义好配置文件后,便可以开始初始化工作了。

@Module({
  imports: [CommonModule.forRoot(config, { withRedis: true, withRabbitMQ: true })],
  controllers: [],
  providers: [],
})
export class AppModule {}

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { cors: true })
  await app.listen(3000)
}
bootstrap()

Module

LoggerModule

首先设置logger:

import { Logger } from 'nestjs-pino';

const app = await NestFactory.create(AppModule, { bufferLogs: true });
app.useLogger(app.get(Logger));

推荐Logger使用方式

import { PinoLogger, InjectPinoLogger } from 'nestjs-pino';

export class MyService {
  constructor(
    private readonly logger: PinoLogger
  ) {
    // Optionally you can set context for logger in constructor or ...
    this.logger.setContext(MyService.name);
  }

  constructor(
    // ... set context via special decorator
    @InjectPinoLogger(MyService.name)
    private readonly logger: PinoLogger
  ) {}

  foo() {
    // PinoLogger has same methods as pino instance
    this.logger.trace({ foo: 'bar' }, 'baz %s', 'qux');
    this.logger.debug('foo %s %o', 'bar', { baz: 'qux' });
    this.logger.info('foo');
  }
}

SequelizeModule

Sequelize的Model已定好了createdAt, updatedAt, deletedAt,不需要再额外定义了,直接继承Model就好

createdAt?: Date | any;
updatedAt?: Date | any;
deletedAt?: Date | any;

TapNow-NestJS-Common 提供了paginate函数来处理分页查询。

RedisModule

RedisModule提供RedisService, RedlockService, SessionService三个service 类。RedisService 提供一个redis实例来存储数据。RedlockService 提供分布式锁,获取单一资源锁用lock()lockWrapper会在函数执行完成后,自动释放锁资源。

RabbitModule

RabbitMQTaskService提供服务自己发布消息,自己处理消息的模式。 比如发送邮件,我们先发布一个发送邮件的任务

const EMAIL_TASK = 'email_task'

class EmailService {
  constructor(private readonly rabbitMQTaskService: RabbitMQTaskService) {}

  async sendEmail(payload) {
    return rabbitMQTaskService.publishTask({
      msgName: EMAIL_TASK,
      payload,
    })
  }
}

然后监听EMAIL_TASK event,发送邮件

@Injectable()
class EmailWork {
  @OnEvent(EMAIL_TASK)
  async charge(payload: PublishPayload) {
    // sending email here
  }
}

在业务里需要发送邮件时,调用EmailServicesendEmail即可。 RabbitTopicMQService提供对其他服务发布消息或者订阅其他服务发布的消息功能。若消费服务失败,消息将被加入到死信队列。 例如: 支付服务需要将支付成功告知Partner,此时支付服务需要发布一条支付成功的消息:

const PAYMENT_MESSAGE = 'payment_message'
const PAYMENT_SUCCESS = 'payment_success'

class NotificationService {
  constructor(private readonly rabbitMQTopicService: RabbitTopicMQService) {}

  async sendMessage(payload) {
    return rabbitMQTopicService.publishMessage(PAYMENT_MESSAGE, {
      msgName: PAYMENT_SUCCESS,
      payload,
    })
  }
}

Partner订阅支付服务的支付消息:

const QUEUE_NAME = 'payment_queue'
const DEAD_QUEUE_NAME = 'payment_dead_queue'

@Injectable()
class PaymentMessage implements OnApplicationBootstrap {
  constructor(private readonly rabbitMQTopicService: RabbitTopicMQService) {}

  // subscribe payment message
  async onApplicationBootstrap() {
    await rabbitMQTopicService.subscribe(
        QUEUE_NAME,
        DEAD_QUEUE_NAME,
        `#.${PAYMENT_MESSAGE}.#`,
        {
          durable: true,
          autoDelete: false,
        },
      )
  }

  @OnEvent(PAYMENT_SUCCESS)
  async onPaymentSuccess(payload: PublishPayload) {
    // do something when received the payment successful message
  }
}

Request

TapNow-NestJS-Common提供了一些公用的装饰器来简化API实现。

import { ClientAuth, CurrentUser, ApiPropertyOptional, ApiProperty, ApiBaseResult } from 'tapnow-nestjs-common'

class PartnerRequest {
  @ApiPropertyOptional({
    description: 'Partner名字',
    example: 'Kitty',
  })
  @IsString()
  name?: string
}

class PartnerResponse {
  ApiProperty({
    description: 'Partner的ID',
    example: 'p_123e',
  })
  id: string,

  ApiProperty({
    description: 'Partner名字',
    example: 'Kitty',
  })
  name: string,
}

@Controller()
class TestingController {
  @ApiBaseResult(PartnerResponse, 200, '获取Partner信息')
  @Get('fake/:id')
  @AdminAuth()
  getPartner(@CurrentUser() user, @Query() query: PartnerRequest)
    : Promise<PartnerResponse> {
    // some code here, and return partner finally, and TapNow-NestJS-Common will transform it to { code, message, data: partner }
  }
}

如上代码实现了一个获取Partner信息的API,这个API是提供给管理后台的,所以需要进行AdminAuth权限校验。TapNow-NestJS-Common已设置全局的ValidationPipe,因此会对PartnerRequest请求参数进行校验。 PartnerRequest + ApiBaseResult定义了Swagger文档中该API的完整描述。

Response

TapNow-NestJS-Common 会捕捉请求范围内的异常并返回错误信息,对于正常的请求结果会转换成{ code, message, data }的数据结构,其中data是api返回的数据。

Auth

TapNow-NestJS-Common提供了PUBLIC, CLIENT, ADMIN, MERCHANT, API, INTERNAL, DEBUG这几种授权方式,并提供了相应的装饰器。由于授权用到了session,所以必须加载RedisModule。