NestJS

Overview

  1. Nest深受Angular的影响,借鉴了NG的一些列前端设计思路,特别是其代码结构。让Angular易于开发大型前端的优势也发挥在Node服务器端。
  2. 模块化:高度可测试,可扩展,松散耦合且易于维护
  3. nestjs直接对接ts类型系统
  4. 解析数据库schema,产出swagger,对typeORM的整合的非常不错

A nestjs template that contains basic functions (typeorm, task, email, unit test): https://github.com/yogagii/nest_startup

$ npm i -g @nestjs/cli
$ nest new project-name

Structure

my-project
  │── src
     │── common
       │── decorators
       │── pipe
       └── utils
     │── custom
       │── dto
       │── entities
       │── custom.controller.ts
       │── custom.service.ts
       │── custom.module.ts
       │── custom.controller.spec.ts
       └── custom.service.spec.ts
     │── app.module.ts
     └── main.ts
  │── nest-cli.json
  │── package.json
  │── tslint.json
  │── test
     │── app.e2e-spec.ts
  └── tsconfig.json

Controllers 控制器

控制器的目的是接收应用的特定请求。路由机制控制哪个控制器接收哪些请求。通常,每个控制器有多个路由,不同的路由可以执行不同的操作。

$ nest g controller cats

Request:

  • @All() 用于处理所有 HTTP 请求方法的处理程序
  • @Get()
  • @Post()
  • @Put()
  • @Patch()
  • @Delete()
  • @Options()
  • @Header(‘Cache-Control’, ‘none’) 指定自定义响应头 Response Headers
  • @Redirect(‘https://nestjs.com’, 301) 重定向

Param decorators: * @Request(), @Req():@Req() 只是 @Request() 的别名 * @Param(key?: string) * @Body(key?: string) * @Query(key?: string) * @Headers():Request Headers

import { Controller, Get, Req, Param } from '@nestjs/common';
import moment from 'moment-timezone';

@Get(':id')
findOne(
  @Param() params  //@Param('id') id
  @Headers() headers // headers.timezone = 'Asia/Shanghai' (模拟时区Chrome -> More tools -> Sensors -> Location)
): string {
  const localeTime = moment(date).tz(timezone).format();
  return `This action returns a #${params.id} cat`;
}

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}

DTO(数据传输对象)模式。DTO是一个对象,它定义了如何通过网络发送数据。

export class CreateCatDto {
  readonly name: string;
  readonly age: number;
  readonly breed: string;
}

Response:

返回object or array,自动序列化为JSON

返回string,number,boolean,只发送值

import { Response } from 'express';

findAll(@Res() response){
  response.status(200).send()
})
  • @Response(),@Res()必须通过调用 response 对象(res.json(…) 或 res.send(…))发出某种响应,否则 HTTP 服务器将挂起。

  • @HttpCode() 响应的状态码总是默认为 200,除了 POST 请求(默认响应状态码为 201)

异步

async findAll(): Promise<any[]> {
  return [];
}

Providers 提供者

Provider 只是一个用 @Injectable() 装饰器注释的类。

$ nest g service cats

依赖注入

Nest 将 catsService 通过创建并返回一个实例来解析 CatsService,解析此依赖关系并将其传递给控制器的构造函数,实现数据共享

constructor(private readonly catsService: CatsService) {}

Provider 通常具有与应用程序生命周期同步的生命周期(“作用域”)。在启动应用程序时,必须解析每个依赖项,因此必须实例化每个提供程序。同样,当应用程序关闭时,每个 provider 都将被销毁。

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

@Injectable() // 类装饰器:基于构造函数的注入
export class HttpService<T> {
  @Inject('HTTP_OPTIONS') // 属性装饰器:基于属性的注入
  private readonly httpClient: T;
}
// app.module.ts
@Module({
  controllers: [CatsController], // 服务的使用者
  providers: [CatsService], // 服务的提供者
})

Modules 模块

模块是具有 @Module() 装饰器的类。 @Module() 装饰器提供了元数据,Nest 用它来组织应用程序结构。

每个 Nest 应用程序至少有一个模块,即根模块。根模块是 Nest 开始安排应用程序树的地方,根模块可能是应用程序中唯一的模块.

// app.module.ts
@Module({
  controllers: [AppController, CatsController],
  providers: [AppService, CatsService],
})
export class AppModule {}

$ nest g module cats

// cats.module.ts
// @Global() // 全局模块,不需要在 imports 数组中导入
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  // exports: [CatsService], // 每个导入CatsModule的模块都可以访问CatsService,共享相同的CatsService实例
})
export class CatsModule {}

// app.module.ts
@Module({
  imports: [CatsModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

nestjs

Middleware 中间件

Nest 中间件等价于 express 中间件

  • 对请求和响应对象进行更改。
  • 调用堆栈中的下一个中间件函数。
  • 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起。
// app.module.ts
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware)
    .exclude(
      { path: 'cats', method: RequestMethod.GET },
      'cats/(.*)',
    )
    .forRoutes(CatsController);
  }
}

forRoutes() 可接受一个字符串、多个字符串、对象、一个控制器类甚至多个控制器类(逗号分隔)。

// main.ts
app.use(logger); // 绑定到每个注册路由

Exception filters 异常过滤器

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

throw new ForbiddenException(); // 内置异常
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN); // 基础异常类
throw new HttpException({
  status: HttpStatus.FORBIDDEN,  // 403
  error: 'This is a custom message',
}, HttpStatus.FORBIDDEN);

try {
    return await this.usersRepository.save(userToUpdate);
} catch (error) {
  throw new HttpException(error.sqlMessage, HttpStatus.BAD_REQUEST);
}

绑定过滤器

@UseFilters(new HttpExceptionFilter())  // 实例
@UseFilters(HttpExceptionFilter) // 类

尽可能使用类而不是实例。由于 Nest 可以轻松地在整个模块中重复使用同一类的实例,因此可以减少内存使用。

// main.ts
app.useGlobalFilters(new HttpExceptionFilter()); // 全局范围的过滤器

若已经设置了useGlobalFilters,在单个接口上加UseFilters会覆盖掉全局的useGlobalFilters

Pipes 管道

管道有两个类型:

  • 转换:管道将输入数据转换为所需的数据输出
  • 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常;

Nest 自带八个开箱即用的管道:

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • DefaultValuePipe
  • ParseEnumPipe
  • ParseFloatPipe

验证字符串是否是 UUID

@Get('/users/:id')
getUserDetail(
  @Param('id', new ParseUUIDPipe()) id: string,
): Promise<UserDto> {
  return this.userService.findOne(id);
}

Pagination 分页 法一:

import { ParseIntPipe, DefaultValuePipe } from '@nestjs/common';
@Get('/users')
  getUserList(
    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page?: number,
    @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit?: number,
  ): Promise<UserDto[]> {
    return this.userService.findAll({ page, limit });
  }

Pagination 分页 法二:

npm i —save class-validator class-transformer

// pagination.dto.ts
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsPositive, IsInt, Max } from 'class-validator';
import { Type, Expose } from 'class-transformer';

export class PaginationDto {
  @IsOptional()
  @IsInt()
  @IsPositive()
  @Type(() => Number)
  @Expose()
  @ApiPropertyOptional({
    default: 1,
  })
  readonly page: number = 1;

  @IsOptional()
  @IsInt()
  @IsPositive()
  @Type(() => Number)
  @Expose()
  @Max(100)
  @ApiPropertyOptional({
    default: 50,
  })
  readonly limit: number = 50; // 加:number才会显示在swagger中

  get skip(): number {
    return (this.page - 1) * this.limit;
  }
  get take(): number {
    return this.limit;
  }
}
// controller
import { UsePipes, ValidationPipe } from '@nestjs/common';
import { PaginationDto } from 'src/core/pagination.dto';
@Get('/users')
@UsePipes(
    new ValidationPipe({
      transform: true,
      transformOptions: {
        excludeExtraneousValues: true, // 排除dto中没有定义的query,当有其他filter时不能为true
        exposeDefaultValues: true, // 没有query时使用默认值
      },
    }),
  )
  getUserList(
    @Query() dto: PaginationDto,
  ): Promise<UserDto[]> {
    return this.userService.findAll(dto);
  }

// service
findAll(ListUserDto): Promise<UserDto[]> {
  return this.usersRepository.find({
    skip: ListUserDto.skip,
    take: ListUserDto.take,
  });
}

query 转string为float (url /solutions/:id?boMin=xxx 中的query直接拿到都是string)

import { Transform } from 'class-transformer';

export class DetailMdSolutionDto {
  @ApiPropertyOptional()
  @Transform(({ value }) => parseFloat(value))
  boMin?: number;
}
  @Get('/solutions/:id')
  @UsePipes(
    new ValidationPipe({
      transform: true,
    }),
  )
  getSolution(
    @Query() detailSolutionDto: DetailMdSolutionDto,
  ) {...}

Guards 守卫

守卫的责任:授权 — 根据运行时出现的某些条件(例如权限,角色,访问控制列表等)来确定给定的请求是否由路由处理程序处理。

在 Express 中通常由中间件处理授权、验证身份。中间件不知道调用 next() 函数后会执行哪个处理程序。然而守卫可以访问 ExecutionContext 实例,因此确切地知道接下来要执行什么。

守卫在每个中间件之后执行,但在任何拦截器或管道之前执行。

每个守卫必须实现一个canActivate()函数。此函数应该返回一个布尔值,指示是否允许当前请求。

@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {
  @Post()
  @Roles('admin')
  create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }
}

Nest提供了通过 @SetMetadata() 装饰器将定制元数据附加到路由处理程序的能力。

// roles.decorator.ts
@SetMetadata('roles', ['admin'])

创建一个RolesGuard类来比较当前用户拥有的角色和当前路径需要的角色

// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(
      ROLES_KEY,
      [context.getHandler(), context.getClass()],
    );
    if (!requiredRoles) { // 没有@Roles()的接口直接返回true
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const { user_role } = request.session;
    return !requiredRoles.some((role) => role > user_role); // 判断用户权限是否大于接口所需权限
  }
}

全局注册RolesGuard

// app.module.ts
providers: [
  {
    provide: APP_GUARD,
    useClass: process.env.NODE_ENV ? SSOAuthGuard : LocalAuthGuard,
  },
  {
    provide: APP_GUARD,
    useClass: process.env.NODE_ENV ? RolesGuard : LocalAuthGuard,
  },
],

SSOAuthGuard 会在 RolesGuard 之前执行,SSOAuthGuard 返回 true 才会执行到 RolesGuard。所以可以在 SSOAuthGuard 中在session里存下用户权限 user_role。

Interceptor 拦截器

拦截器是使用 @Injectable() 装饰器注解的类。拦截器应该实现 NestInterceptor 接口。

功能:在函数执行之前/之后绑定额外的逻辑;转换从函数返回的结果;转换从函数抛出的异常

  • 在接口相应前后记录时间
  • 包装返回的数据
  • 缓存拦截器
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    console.log('Before...')
    return next.handle().pipe(map(data => ({ data, code: 200, message: '请求成功', }))); // After
  }
}

全局拦截器用于整个应用程序、每个控制器和每个路由处理程序。在依赖注入方面, 从任何模块外部注册的全局拦截器 无法插入依赖项, 因为它们不属于任何模块。

刷新 Token

@Injectable()
export class RefreshTokenInterceptor implements NestInterceptor {
  constructor(private readonly authService: AuthService) {}
  async intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Promise<Observable<any>> {
    const request = context.switchToHttp().getRequest();
    const token = request.session.token;
    await this.authService // 若没有await,先执行next.handle(),then中更新session不会生效
      .refreshToken(token.refresh_token)
      .then((result) => {
        request.session.token = {
          ...result,
          expires_at: new Date().getTime() + result.expires_in * 1000,
        };
      });
    return next.handle();
  }
}
// main.ts
app.useGlobalInterceptors(
  new RefreshTokenInterceptor(
    new AuthService(new HttpService(), new UserManagementService()), 
  ), // 只能传入实例,不能传入类,当传入的实例又调用其他类时会嵌套多层
);

从模块(app.module)中设置拦截器可以传入类

// app.module
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: RefreshTokenInterceptor,
    },
  ],
})

Task Scheduling 定时任务

npm install —save @nestjs/schedule

  • 计时工作(cron job)
@Cron('45 * * * * *') // 该方法每分钟执行一次,在第 45 秒执行。
handleCron() {
  this.logger.debug('Called when the current second is 45');
}
* * * * * *
| | | | | |
| | | | | day of week
| | | | month
| | | day of month
| | hour
| minute
second (optional)

cron patterns: http://crontab.org/

stop()-停止一个按调度运行的任务

start()-重启一个停止的任务

setTime(time:CronTime)-停止一个任务,为它设置一个新的时间,然后再启动它

lastDate()-返回一个表示工作最后执行日期的字符串

const job = this.schedulerRegistry.getCronJob('notifications');

job.stop();
console.log(job.lastDate());
  • 间隔(Interval)
@Interval(10000)
handleInterval() {
  this.logger.debug('Called every 10 seconds');
}
  • 延时任务(Timeout)
@Timeout(5000)
handleTimeout() {
  this.logger.debug('Called once after 5 seconds');
}

跨域

// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    cors: process.env.ENABLE_CORS === 'true',
  });
  app.enableCors({
    origin: ['http://localhost:3008'],
  });
  await app.listen(3007);
}
bootstrap();

Test 测试

Nest 提供Jest和SuperTest开箱即用的集成。

https://github.com/yogagii/bst_data

Unit test:

  • xxx.controller.spec.ts: 模拟service的方法,测试controller是否调用service方法
  • xxx.service.spec.ts: 模拟Repository方法,不连接数据库,mock假数据,验证业务逻辑

e2e测试:

  • app.e2e-spec.ts: 模拟app module,连接测试数据库,验证各路由

踩坑:需在beforeAll中createTestingModule,beforeEach中import AppModule会导致数据库重复连接(AlreadyHasActiveConnectionError)

Event 监听事件

使用 try catch 无法处理异步代码块内出现的异常

// 异常捕获成功
try {
  throw new Error('error');
} catch(e) {
  console.log('异常捕获');
}

// 异常捕获失败
try {
  setTimeout(() => {
    throw new Error('error');
  })
} catch(e) {
  console.log('异常捕获');
}

使用event方式来处理异常

npm i --save @nestjs/event-emitter

事件监听

import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';

@Injectable()
export class UpdateStatusListener {
  constructor(private readonly mdmService: MdmService) {}

  @OnEvent('updateStatus.sendFormFailed')
  handleOrderCreatedEvent(event) {
    console.log('sendFormFailed: ', event);
  }
}
@Module({
  providers: [UpdateStatusListener],
})

事件触发

import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable()
export class EmailService {
  constructor(
    private eventEmitter: EventEmitter2,
  ) {}

  async sendEmailCode() {
    try {
      await this.mailerService.sendMail(sendMailOptions);
    } catch (error) {
      this.eventEmitter.emit('updateStatus.sendFormFailed', {
        event_id: courseData.event_id,
      });
    }
  }
}

Authentication 身份验证

Passport是 node.js 身份验证库,Passport 执行步骤:

  1. 验证用户的credentials (username/password, JSON Web Token (JWT), or identity token
  2. 给经过身份验证的状态签发token(JWT),或创建一个 Express 会话
  3. 将经过身份验证的用户的信息附加到请求对象以便进一步使用
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { CipherService } from '../common/cipher';
import { jwtConstants } from './constants';

@Injectable()
export class AuthService {
  constructor(
    private readonly jwtService: JwtService,
    private cipherService: CipherService,
  ) {}

  async validateUser(username: string, pass: string): Promise<any> {
    if (
      username === process.env.ADMIN_USERNAME &&
      pass === process.env.ADMIN_PASSWORD
    ) {
      return { username, pass };
    }
    return null;
  }

  async login(user: any) {
    console.log('login: ', user);
    const payload = {
      sub: this.cipherService.encryptAES128ECB(
        'xxx',
        jwtConstants.cipherKey,
      ),
    };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

Passport JWT

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

Node.js提供了一个内置的crypto模块可用于加密和解密字符串

import { Injectable } from '@nestjs/common';
import { createCipheriv, createDecipheriv } from 'crypto';

@Injectable()
export class CipherService {
  encryptAES128ECB(data: string, key: string) {
    const cipher = createCipheriv('aes-128-ecb', key, null);
    cipher.setAutoPadding(true);
    const encrypted = cipher.update(data, 'utf8', 'base64');
    return encrypted + cipher.final('base64');
  }

  decryptAES128ECB(data: string, key: string) {
    const cipher = createDecipheriv('aes-128-ecb', key, null);
    cipher.setAutoPadding(true);
    const receivedPlaintext = cipher.update(data, 'base64', 'utf8');
    return receivedPlaintext + cipher.final('utf8');
  }
}

自定义CustomStrategy

import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport';

@Injectable()
export class CustomStrategy extends PassportStrategy(Strategy, 'custom') { // 自定义Strategy名字

}

aadAuthGuard

https://www.npmjs.com/package/passport-azure-ad

import { OIDCStrategy } from 'passport-azure-ad';

@Injectable()
export class AADStrategy extends PassportStrategy(OIDCStrategy, 'aad') {
  constructor() {
    super(
      {
        identityMetadata: 'https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration',
        clientID: 'xxx',
        clientSecret: 'xxx',
        responseType: 'id_token',
        responseMode: 'form_post',
        redirectUrl: 'xxx/callback',
      },
      (iss, sub, profile, accessToken, refreshToken, done) => {},
    );
  }
}

跳转到aad登录界面

Swagger 接口文档

配置

// main.ts
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ApplicationModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(ApplicationModule);

  const options = new DocumentBuilder()
    .setTitle('Project API')
    .setDescription('The Project API description')
    .setVersion('1.0')
    .addTag('project')
    .build();
  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}
bootstrap();

在DTO中添加request schema

import { ApiPropertyOptional, ApiProperty, IntersectionType } from '@nestjs/swagger';

export enum UserRole {
  'READER',
  'DESIGNER',
  'APPROVER',
  'ADMIN',
}

export class UserDto {
  @ApiProperty()  // 必填项
  email: string;

  @ApiPropertyOptional() // 非必填
  last_name?: string;
}

export class ListUserDto extends IntersectionType(PaginationDto, SortDto) {
  @ApiPropertyOptional({
    description: 'filter: role',
    enum: UserRole, // 枚举
    isArray: true, // ?role=0,1
  })
  role?: string;
}

踩坑:body为数组类型时显示为 [“string”], 需要用 @ApiBody({ type: [CreateCatDto] }) 显示定义schema

Mapped types:

  • IntersectionType: 可以将两个类型中所有属性组合在一起生成一个新类型,但是继承不到pagination的getter
  • PartialType: 把所有字段变为optional
  • PickType: 选择部分字段
  • OmitType: 排除部分字段

https://docs.nestjs.com/openapi/mapped-types#composition

在controller中添加response schema

import { ApiCreatedResponse } from '@nestjs/swagger';
import { UserDto, ListUserDto } from './dto/user';

@Controller()
export class UserController {
  @Get('/users')
  @ApiCreatedResponse({
    type: UserDto,
    isArray: true,
  })
  getUserList(@Query() listUserDto: ListUserDto): Promise<Pagination<UserDto>> {
    ...
  }

Session 会话

$ npm i express-session

$ npm i -D @types/express-session

// main.ts
import * as session from 'express-session';

app.use(
  session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
  }),
);

从路径处理程序中读取session

@Get('callback')
async ssoCallback(@Session() session: Record<string, any>): Promise<any> {
  session.token = {
    ...result,
    expires_at: new Date().getTime() + result.expires_in * 1000,
  };
}

踩坑:Warning: connect.session() MemoryStore is not designed for a production environment, as it will leak memory, and will not scale past a single process.

解决:

npm install memorystore

express-session full featured MemoryStore module without leaks! A session store implementation for Express using lru-cache. Because the default MemoryStore for express-session will lead to a memory leak due to it haven’t a suitable way to make them expire. The sessions are still stored in memory, so they’re not shared with other processes or services.

// main.ts
import session from 'express-session';
import createMemoryStore from 'memorystore';

const MemoryStore = createMemoryStore(session);

app.use(
  session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    store: new MemoryStore({
      checkPeriod: 86400000, // prune expired entries every 24h
    }),
  }),
);

// tsconfig.json
"esModuleInterop": true

https://www.npmjs.com/package/memorystore

checkPeriod: This option specifies the time interval (in milliseconds) between two consecutive checks for expired sessions. In this case, expired entries are pruned every 24 hours (86400000 milliseconds).

Caching 缓存

  1. 整个接口缓存

只有使用 GET 方式声明的节点会被缓存,只要api参数相同,则从缓存返回

npm install @nestjs/cache-manager

全局注册CacheModule

import { CacheModule, CacheInterceptor } from '@nestjs/cache-manager';

@Module({
  imports: [
    CacheModule.register({
      ttl: 1000 * 60 * 60 * 2, // 2h
      isGlobal: true, // 声明为全局模块
    }),
  ],
  // 若全局绑定到所有controller就无需手动给需要的接口加Interceptors
  // providers: [
  //   {
  //     provide: APP_INTERCEPTOR,
  //     useClass: CacheInterceptor,
  //   },
  // ],
})
export class AppModule {}

手动给需要的接口加上Interceptors

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { CacheInterceptor } from '@nestjs/cache-manager';

@Controller('user')
@UseInterceptors(CacheInterceptor) // 作用于整个controller
export class UserController {
  @UseInterceptors(CacheInterceptor) // 或者只作用于单个接口
  @Get('dashboard')
  findAll() { return ... }
}

1.接口涉及的表若是会随用户操作更新,则不能开启缓存

2.若接口中使用到 @Session 中的 user_id也不能开启缓存,不同用户会返回相同数据

  1. 自定义缓存
npm install cache-manager
npm install -D @types/cache-manager

common/cache/cache.module.ts

import { Module, CacheModule } from '@nestjs/common';
import { CacheService } from './cache.service';

@Module({
  imports: [
    CacheModule.register({
      ttl: 0, // 缓存永不过期
    }),
  ],
  controllers: [],
  providers: [CacheService],
  exports: [CacheService],
})
export class CustomCacheModule {}

common/cache/cache.service.ts

import { Injectable, Inject, CACHE_MANAGER } from '@nestjs/common';
import { Cache } from 'cache-manager';

@Injectable()
export class CacheService {
  constructor(
    @Inject(CACHE_MANAGER)
    private cacheManager: Cache,
  ) {}

  cacheSet(key: string, value: string) {
    this.cacheManager.set(key, JSON.stringify(value));
  }

  async cacheGet(key: any): Promise<any> {
    const result: string = await this.cacheManager.get(key);
    return result ? JSON.parse(result) : null;
  }
}

ENV

nest start 时 .dto.ts 文件内拿不到 process.env.xxx

nest build 时 .dto.ts 文件内可用 process.env.xxx

Article
Tagcloud
DVA Java Express Architecture Azure CI/CD database ML AWS ETL nest sql AntV Next Deep Learning Flutter TypeScript Angular DevTools Microsoft egg Tableau SAP Token Regexp Unit test Nginx nodeJS sails wechat Jmeter HTML2Canvas Swift Jenkins JS event GTM Algorithm Echarts React-Admin Rest React hook Flux Redux ES6 Route Component Ref AJAX Form JSX Virtual Dom Javascript CSS design pattern