nestjs 开发阶段总结

119 阅读5分钟

前言

这是一个新手全栈的阶段总结。

nestjs 根据请求流抽象代码

由于 nestjs 提供了守卫,拦截器,管道,过滤器,以便我们在请求响应前后对请求和相应进行特殊处理。这也对我们开发时如何抽象代码提出了问题。解决这一问题的关键有两点,一是明确请求流动顺序,二是明确各个功能对应的场景。

执行顺序

  1. 中间件(Middleware)
  2. 守卫(Guard)
  3. 拦截器(Intercaptor)
  4. 管道(Pipe)
  5. 控制器(Controller)

应用场景

通过这个执行顺序,在抽象代码时就可以思考某些逻辑是否可以不写在 service 中,而是提前或延后处理。比如,响应程序需要根据用户传入的用户 id 获取对应的用户实体,那么可以使用管道对用的 id 进行处理,在 controller 中直接获取到用户实体,方便处理。比如登陆逻辑,权限校验逻辑,可以放到守卫中执行。

nestjs 代码结构

nestjs 是从根模块开始,向下查找所有依赖,所以在划分代码结构的时候可以以模块维度划分。由于模块的概念相对较大,所以不建议划分的过细。一个模块中包含对应的 controller,一个 controller 中包含多个路由,在 controller 中可以通过依赖注入的方式使用 service。通常不建议在路由中写具体的逻辑代码,通过抽象出 service 层来处理具体逻辑。然后在 service 中获取数据实体并处理。

module -> controller -> service -> entity

对于模块和 controller 来说,只需要通过业务功能进行合适的划分即可,但是对于 service 和 entity 则会涉及到复用。所以不建议将 servie 放在 controller 目录下,而应放在一个单独目录,方便公用。

当然,对于复杂项目,还可以通过微服务进行拆分。

数据库

mysql

接入

在 nestjs 中,一般会通过 orm 来操作数据库,常用的有 TypeORM、Sequelize 等。

nestjs 提供了 TypeOrmModule 以便快速接入 typeorm。

对于自动运行迁移,建议直接关闭,以保证各个环境一致,为了方便也可以在开发环境打开。

import { TypeOrmModule } from '@nestjs/typeorm';

TypeOrmModule.forRootAsync({
      inject: [ConfigurationService],
      useFactory: (configurationService: ConfigurationService) => {
        return {
          type: 'mysql', // 连接数据库的类型
          ...configurationService.mysqlConfig,
          timezone: '+08:00', // 设置时区
          autoLoadEntities: true, // 自动加载所有的实体,一个实体对应一张表
          synchronize: configurationService.synchronize, // 是否自动同步数据库和实体的表结构, 开发时为true,生产环境为false
          logging: false, // 打印真正的sql语句
          // 配置迁移
          migrations: [__dirname + '/migrations/*{.ts,.js}'], // 迁移文件路径
          migrationsRun: false, // 是否自动运行迁移(生产环境建议为false)

          // 配置 CLI 路径(用于生成迁移)
          cli: {
            migrationsDir: 'src/migrations',
          },
        };
      },
    }),
    TypeOrmModule.forFeature([
      AdminUser,
      Category,
      Product,
      Sku,
      Inventory,
      CUser,
      Order,
      OrderItem,
      Withdrawal,
      RenewalOrder,
      Coupon,
      UserCoupon,
      Activity,
    ]),

使用

接入数据库后,只需要通过 @InjectRepository 装饰器将实体注入类的构造函数即可。

@Injectable()
export class OrderService extends MysqlBaseService<Order> {
  constructor(
    @InjectRepository(Order) protected repository: Repository<Order>, // 注入实体
  ) {
    super(repository);
  }
 }

我们还可以定义一个几类,用来服用常用的数据库相关函数,比如 save、create 等。

export abstract class MysqlBaseService<T> {
  constructor(protected repository: Repository<T>) {}
  async findAll() {
    return this.repository.find();
  }
  async find(options?: FindManyOptions<T>) {
    return await this.repository.find(options);
  }
  async findOne(findOptions: FindOneOptions<T>) {
    return await this.repository.findOne(findOptions);
  }

  async findBy(findOptions: FindOptionsWhere<T> | FindOptionsWhere<T>[]) {
    return await this.repository.findBy(findOptions);
  }

  async create(createDto?: DeepPartial<T>) {
    const entity = this.repository.create(createDto);

    return await this.repository.save(entity);
  }

  async update(id: number, updateDto: QueryDeepPartialEntity<T>) {
    return await this.repository.update(id, updateDto);
  }

  async delete(id: number) {
    return await this.repository.delete(id);
  }
}

redis

连接 reids 可以使用 ioredis 库。通过 ioredis 库提供的 Redis 类创建 redis 客户端。

我们可以创建一个 redisService 来使用。

@Injectable()
export class RedisService implements OnModuleDestroy {
  private redisClient: Redis;
  constructor(private configurationService: ConfigurationService) {
    this.redisClient = new Redis({
      host: this.configurationService.redisHost,
      port: this.configurationService.redisPort,
      password: this.configurationService.redisPassword,
    });
  }
  onModuleDestroy() {
    //当模块销毁的时候退出当前的客户端
    this.redisClient.quit();
  }
  getClient() {
    return this.redisClient;
  }
  async set(key: string, value: string, ttl?: number) {
    if (ttl) {
      await this.redisClient.set(key, value, 'EX', ttl);
    } else {
      await this.redisClient.set(key, value);
    }
  }
  async get(key: string) {
    return this.redisClient.get(key);
  }
  async del(key: string) {
    await this.redisClient.del(key);
  }
}

配置信息

开发中,我们会有一些配置信息,需要根据环境切换对应的配置。

在 nestjs 项目中,可以使用官方提供的 ConfigModule 和 ConfigService。

import { ConfigService, ConfigModule } from '@nestjs/config';

首先需要引入 ConfigModule 并进行对应配置,通常对环境配置切换的配置,以及设置全局。默认配置会加载 .env 文件,可以根据环境自己配置加载什么配置文件。也可以配合 docker 注入,就不用在这里配置。

ConfigModule.forRoot({ 
     isGlobal: true, // 使配置项在整个应用中都可用 
     envFilePath: '.env', // 加载 .env 文件 
}),

使用就很简单,我们可以自己创建一个 ConfigurationService ,并注入 ConfigService,在 ConfigurationService 进行 get 获取对应的配置,以便使用和处理。

打印日志信息

nestjs 官方提供了日志功能,但是不支持持久化,对生产环境不够用。所以可以使用 winston 库来打印需要持久化的日志信息。

首先,需要引入 WinstonModule 并进行配置。

WinstonModule.forRoot({
      // options
      transports: [
        new winston.transports.Console({
          format: winston.format.combine(
            winston.format.timestamp(),
            winston.format.ms(),
            nestWinstonModuleUtilities.format.nestLike('MyApp', {
              colors: true,
              prettyPrint: true,
              processId: true,
              appName: true,
            }),
          ),
        }),
        // other transports...
      ],
    }),

然后,我们可以创建一个 WinstonLoggerService 来初始化,并且使用。我们还需要准备一个函数,判断持久化日志存储文件夹是否存在,不存在则创建。

@Injectable()
export class WinstonLoggerService implements LoggerService {
  private readonly logger: winston.Logger;

  constructor() {
    // 创建 Daily Rotate File transport
    const dailyRotateTransport = new winston.transports.DailyRotateFile({
      filename: './logs/%DATE%-app.log', // 日志文件名
      datePattern: 'YYYY-MM-DD', // 日志日期格式
      zippedArchive: true, // 启用日志文件压缩
      maxSize: '20m', // 每个日志文件最大大小
      maxFiles: '7d', // 保留日志文件7天
      level: 'info', // 设置最低日志级别
    });

    // 创建 Winston Logger
    this.logger = winston.createLogger({
      level: 'info', // 默认日志级别
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.timestamp(),
        winston.format.printf(
          ({ timestamp, level, message }) =>
            ` ${timestamp} [${level}]: ${message}`,
        ),
      ),
      transports: [
        dailyRotateTransport, // 添加 Daily Rotate File 传输
        new winston.transports.Console(), // 控制台输出
      ],
    });
  }

  log(message: string) {
    this.logger.info(message);
  }

  error(message: string, trace?: string) {
    this.logger.error(message, trace);
  }

  warn(message: string) {
    this.logger.warn(message);
  }

  debug(message: string) {
    this.logger.debug(message);
  }

  verbose(message: string) {
    this.logger.verbose(message);
  }
}

结束

以上是对 nestjs 常用功能的总结,希望大神们如果有更好的方案可以多多指点。