用Nest.js微服务构建的图像服务

1,964 阅读2分钟

前言

前面两章节讲的是图像的缩放、旋转、位移的处理,本章将介绍进行后续的图像生成服务处理。案例及背景都已做脱敏处理。

背景

将照片生成为不同证件尺寸模版下排版并返回给客户端资源链接。ps: 此服务不仅仅是用来处理证件照片的,还可以处理各类海报业务及头像处理服务。 示例如下:

uTools_1683984623870.png

Nest.js创建一个微服务模块

首先全局安装Nestcli工具:

npm i -g @nestjs/cli

然后用Nestcli快速的创建一个微服务模块,这里是假设已有一个主服务的情况下。

nest new micro-node-canvas

npm i --save @nestjs/microservices

修改main.ts为,因为微服务通过 模式 识别消息。模式是一个普通值,例如对象、字符串。模式将自动序列化,并与消息的数据部分一起通过网络发送。这里选用TCP模式,具体可以查看文档微服务 (nestjs.cn)

import { MicroserviceOptions, Transport } from '@nestjs/microservices';

import { AppModule } from './app.module';
import { NestFactory } from '@nestjs/core';

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

app.module.ts模块导入全局配置模块,这些模块导出当前模块所需提供者。

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { getConfiguration } from './config/configuration';
import { qiniuProvider } from './qiniu.provider';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [getConfiguration],
      envFilePath: [`.env.${process.env.NODE_ENV}`, '.env'],
    }),
  ],
  controllers: [AppController],
  providers: [AppService, qiniuProvider()],
})
export class AppModule {}

qiniu.provider.ts七牛云模块提供者,这里不再贴出具体配置项代码。


import { ConfigService } from '@nestjs/config';
import { ConfigurationKeyPaths } from './config/configuration';
import { FactoryProvider } from '@nestjs/common';
import { IQiniuConfig } from './config/config.interface';
import { QINIU_CONFIG } from './constants/config.constants';

/**
 * 提供使用 @Inject(QINIU_CONFIG) 直接获取七牛配置
 */
export function qiniuProvider(): FactoryProvider {
  return {
    provide: QINIU_CONFIG,
    useFactory: (
      configService: ConfigService<ConfigurationKeyPaths>,
    ): IQiniuConfig => ({
      accessKey: configService.get('qiniu.accessKey'),
      secretKey: configService.get('qiniu.secretKey'),
      domain: configService.get('qiniu.domain'),
      bucket: configService.get('qiniu.bucket'),
      zone: configService.get('qiniu.zone'),
      access: configService.get('qiniu.access'),
    }),
    inject: [ConfigService],
  };
}

贴一下./config/configuration类型体操

import * as qiniu from 'qiniu';

const parseZone = (zone: string) => {
  switch (zone) {
    case 'Zone_as0':
      return qiniu.zone.Zone_as0;
    case 'Zone_na0':
      return qiniu.zone.Zone_na0;
    case 'Zone_z0':
      return qiniu.zone.Zone_z0;
    case 'Zone_z1':
      return qiniu.zone.Zone_z1;
    case 'Zone_z2':
      return qiniu.zone.Zone_z2;
  }
};
/**
 * NestedKeyOf
 * Get all the possible paths of an object
 * @example
 * type Keys = NestedKeyOf<{ a: { b: { c: string } }>
 * // 'a' | 'a.b' | 'a.b.c'
 */
type NestedKeyOf<ObjectType extends object> = {
  [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
    ? `${Key}` | `${Key}.${NestedKeyOf<ObjectType[Key]>}`
    : `${Key}`;
}[keyof ObjectType & (string | number)];

export const getConfiguration = () =>
  ({
    // qiniu config
    qiniu: {
      accessKey: process.env.QINIU_ACCESSKEY,
      secretKey: process.env.QINIU_SECRETKEY,
      domain: process.env.QINIU_DOMAIN,
      bucket: process.env.QINIU_BUCKET,
      zone: parseZone(process.env.QINIU_ZONE || 'Zone_z2'),
      access: (process.env.QINIU_ACCESS_TYPE as any) || 'public',
    },
  } as const);
export type ConfigurationType = ReturnType<typeof getConfiguration>;

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export type ConfigurationKeyPaths = Record<NestedKeyOf<ConfigurationType>, any>;

app.service.ts的主要工作就是处理图像服务,将处理好的图像上传至七牛云,返回给客户端预览。此处仅贴图展示。

uTools_1684036605168.png 在主服务中需要注入的ControllerModules中注册这个微服务,如下:

import { ClientsModule, Transport } from '@nestjs/microservices';
@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'CANVAS_SERVICE',
        transport: Transport.TCP,
        options: {
          port: 8888,
        },
      },
    ])
  ],
  //此处省略下面代码
})

ClientsModule和 ClientProxy类需要从 @nestjs/microservices 包导入。

使用 ClientsModule 暴露的静态register() 方法。此方法将数组作为参数,其中每个元素都具有 name属性,以及一个可选的transport属性(默认是Transport.TCP),以及特定于微服务的options属性。 name属性充当一个 injection token,可以在需要时将其用于注入 ClientProxy 实例。name 属性的值作为注入标记。在外部模块导入后,可以使用@Inject() 装饰器将'CANVAS_SERVICE'注入ClientProxy的一个实例。

uTools_1684043010459.png 在主服务中引入完整服务后,我们可以来测试生成的效果。

uTools_1684043831073.png 测试完一组数据后效果还算可以,但是测试用例有很多,模拟用户数据一个一个测试费时间还容易将数据弄混,所幸成熟的软件产品都会集成自动化测试这部分。 回到micro-node-canvas微服务模块中,创建app.controller.spec.ts文件:

import { Test, TestingModule } from '@nestjs/testing';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { getConfiguration } from './config/configuration';
import { jestMockData } from './mock/data';
import { qiniuProvider } from './qiniu.provider';

describe('AppController', () => {
  let appController: AppController;

  beforeEach(async () => {
    const app: TestingModule = await Test.createTestingModule({
      imports: [
        ConfigModule.forRoot({
          isGlobal: true,
          load: [getConfiguration],
          envFilePath: [`.env.${process.env.NODE_ENV}`, '.env'],
        }),
      ],
      controllers: [AppController],
      providers: [AppService, qiniuProvider()],
    }).compile();

    appController = app.get<AppController>(AppController);
  });
  describe.each(jestMockData)('生成照片集合', (dto) => {
    const { modeInfo } = dto;
    test(`returns ${modeInfo.media_name}`, async () => {
      const res = await appController.genImage(dto);
      expect(res).toBe(
        `/public/images/test/${modeInfo.media_name}${modeInfo.media_size}${modeInfo.media_set_type}.jpg`,
      );
    });
  });
});

jestMockData中就是所有客户端所传的集合,然后执行npm jest命令跑起测试用例。

uTools_1684045176176.png 嗯,一个完整的流程完毕,最后测试下七牛云上传是否正常。

uTools_1684045366924.png 测试完毕,整个流程结束。

结束语

本次服务中用到的图像库为Automattic/node-canvas: Node canvas is a Cairo backed Canvas implementation for NodeJS. (github.com),API使用上与Canvas Web Canvas API一致,用着完全无心智负担,放心食用。