Nestjs 入门(一)

3,481 阅读8分钟

Nestjs 是 Node 渐进式框架,底层默认使用 express(可以通过 Adapter 转换到 fastify),可以使用 express 或者 fastify 所有中间件,完美支持 TypeScript。熟悉 Spring 和 Angular 的同学可以很快上手 Nestjs,它大量借鉴了 Spring 和 Angular 中的设计思想。

在开始写hello world之前,我们先来看看 Nestjs 中比较重要的设计思想和概念。

依赖注入

依赖注入(Dependency Injection,简称DI)是面向对象中控制反转(Inversion of Control,简称 IoC)最常见的实现方式,主要用来降低代码的耦合度。我们用一个例子来说明什么是控制反转。

假设你要造一辆车,你需要引擎和轮子:

import { Engine } from './engine'
import { Tire } from './tire'

class Car {
  private engine;
  private wheel;
  
  constructor() {
    this.engine = new Engine();
    this.tire = new Tire();
  }
}

这时候 Car 这个类依赖于EngineTire,构造器不仅需要把依赖赋值到当前类内部属性上还需要把依赖实例化。假设,有很多种类的Car都用了Engine,这时候需要把Engine替换为ElectricEngine,就会陷入牵一发而动全身的尴尬。

那么用 IoC 来改造一下:

import { Engine } from './engine'
import { Tire } from './tire'

class Container {
  private constructorPool;

  constructor() {
    this.constructorPool = new Map();
  }

  register(name, constructor) {
    this.constructorPool.set(name, constructor);
  }

  get(name) {
    const target = this.constructorPool.get(name);
    return new target();
  }
}

const container = new Container();
container.bind('engine', Engine);
container.bind('tire', Tire);

class Car {
  private engine;
  private tire;
  
  constructor() {
    this.engine = container.get('engine');
    this.tire = container.get('tire');
  }
}

此时,container相当于CarEngineTire之间的中转站,Car不需要自己去实例化一个Engine或者TireCarEngineTire之间也就没有了强耦合的关系。

从上面例子看出,在使用 IoC 之前,Car需要Engine或者Tire时需要自己主动去创建Engine或者Tire,此时对Engine或者Tire的创建和使用的控制权都在Car手上。

在使用 IoC 之后,CarEngine或者Tire之间的联系就切断了,当Car需要Engine或者Tire时,IoC Container会主动创建这个对象给Car使用,此时Car获取Engine或者Tire的行为由主动获取变成了被动获取,控制权就颠倒过来。当Engine或者Tire有任何变动,Car不会受到影响,它们之间就完成了解耦。

当我们需要测试Car时,我们不需要把Engine或者Tire全部new一遍来构造Car,只需要把 mock 的Engine或者Tire, 注入到 IoC 容器中就行。

IoC 有很多实现,比如 Java 的 Spring ,PHP 的 Laravel ,前端的 Angular2+ 以及 Node 的 Nestjs等。

在 Nestjs 中,通过@Injectable装饰器向 IoC 容器注册:

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

在构造函数中注入CatsService的实例:

import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

CatsService作为一个privider,需要在module中注册,这样在该module启动时,会解析module中所有的依赖,当module销毁时,provider也会一起销毁。

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class ApplicationModule {}

模块化

Nestjs 提供了一个模块化结构,用于将同一领域内的代码组织成单独的模块。模块化的作用就是可以清晰地组织你的应用,并使用外部库扩展应用。

Modulecontrollerservicepipe等打包成内聚的功能块,每个模块聚焦于一个特性区域、业务领域、工作流或通用工具。

在 Nestjs 中通过@Module装饰器声明一个模块,@Module接受一个描述模块属性的对象:

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { CoreModule } from './core/core.module';

@Module({
  imports: [CoreModule],
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService]
})
export class CatsModule {}

每个属于这个模块的controllerservice等都需要在这个模块中注册,如果需要引入其他模块或者第三方模块,需要将它注册到imports,通过exports可以将相应的servicemodule等共享出去。

面向切面编程

面向切面编程(Aspect Oriented Programming,简称AOP)主要是针对业务处理过程中的切面进行提取,在某个步骤和阶段进行一些操作,从而达到 DRY(Don't Repeat Yourself) 的目的。AOP 对 OOP 来说,是一种补充,比如可以在某一切面中对全局的 Log、错误进行处理,这种一刀切的方式,也就意味着,AOP 的处理方式相对比较粗粒度。

在 Nestjs 中,AOP 分为下面几个部分(按顺序排列):

  • Middlewares
  • Guards
  • Interceptors (在流被操纵之前)
  • Pipes
  • Interceptors (在流被操纵之后)
  • Exception filters (如果发现任何异常)

Middlewares

Middleware 和 express 的中间件一样,你可以直接使用 express 中的中间件:

import * as helmet from 'helmet'

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule, {
    cors: true,
    logger: false,
  })

  app.use(helmet())

  await app.listen(config.port, config.hostName, () => {
    Logger.log(
      `Flash API server has been started on http://${config.hostName}:${config.port}`,
    )
  })
}

Guards

Guards 和前端路由中的路由守卫一样,主要确定请求是否应该由路由处理程序处理。通过守卫可以知道将要执行的上下文信息,所以和 middleware 相比,守卫可以确切知道将要执行什么。

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

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);			// validateRequest 函数实现 Request 的验证
  }
}

Interceptors

Interceptors 可以给每一个需要执行的函数绑定,拦截器将在该函数执行前或者执行后运行。可以转换函数执行后返回的结果,扩展基本函数行为等。

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { getFormatResponse } from '../../shared/utils/response'

export interface Response<T> {
  data: T
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<Response<T>> {
    return next.handle().pipe(map(getFormatResponse))
  }
}

Pipes

Pipe 是具有 @Injectable() 装饰器的类,并实现了 PipeTransform 接口。通常 pipe 用来将输入数据转换为所需的输出或者处理验证

下面就是一个ValidationPipe,配合class-validatorclass-transformer ,可以更方便地对参数进行校验。

import {
  PipeTransform,
  ArgumentMetadata,
  BadRequestException,
  Injectable,
} from '@nestjs/common'
import { validate } from 'class-validator'
import { plainToClass } from 'class-transformer'

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value, metadata: ArgumentMetadata) {
    const { metatype } = metadata
    if (!metatype || !this.toValidate(metatype)) {
      return value
    }
    const object = plainToClass(metatype, value)
    const errors = await validate(object)
    if (errors.length > 0) {
      throw new BadRequestException('Validation failed')
    }
    return value
  }

  private toValidate(metatype): boolean {
    const types = [String, Boolean, Number, Array, Object]
    return !types.find(type => metatype === type)
  }
}

Exception filters

内置的 Exception filters 负责处理整个应用程序中的所有抛出的异常,也是 Nestjs 中在 response 前,最后能捕获异常的机会。

import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';

@Catch()
export class AnyExceptionFilter implements ExceptionFilter {
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    response
      .status(status)
      .json({
        statusCode: exception.getStatus(),
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}

DTO

数据访问对象简称DTO(Data Transfer Object), 是一组需要跨进程或网络边界传输的聚合数据的简单容器。它不应该包含业务逻辑,并将其行为限制为诸如内部一致性检查和基本验证之类的活动。

在 Nestjs 中,可以使用 TypeScript 接口或简单的类来完成。配合 class-validatorclass-transformer 可以很方便地验证前端传过来的参数:

import { IsString, IsInt, MinLength, MaxLength } from "class-validator";
import { ApiModelProperty } from '@nestjs/swagger'

export class CreateCatDto {
  @ApiModelProperty()
  @IsString()
  @MinLength(10, {
    message: "Name is too short"
  })
  @MaxLength(50, {
    message: "Name is too long"
  })
  readonly name: string;
  
  @ApiModelProperty()
  @IsInt()
  readonly age: number;
  
  @ApiModelProperty()
  @IsString()
  readonly breed: string;
}

import { Controller, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat';
  }
}

如果 Body 中的参数不符合要求,会直接报 Validation failed 错误。

ORM

ORM 是"对象-关系映射"(Object/Relational Mapping) 的缩写,通过实例对象的语法,完成关系型数据库的操作。通过 ORM 就可以用面向对象编程的方式去操作关系型数据库。

在 Java 中,通常会有 DAO(Data Access Object, 数据访问对象)层,DAO 中包含了各种数据库的操作方法。通过它的方法,对数据库进行相关的操作。DAO 主要作用是分离业务层与数据层,避免业务层与数据层耦合。

在 Nestjs 中,可以用 TypeORM 作为你的 DAO 层,它支持 MySQL / MariaDB / Postgres / CockroachDB / SQLite / Microsoft SQL Server / Oracle / MongoDB / NoSQL。

在 typeORM 中数据库的表对应的就是一个类,通过定义一个类来创建实体。实体(Entity)是一个映射到数据库表(或使用 MongoDB 时的集合)的类,通过@Entity()来标记。

import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";

@Entity()
export class User {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    firstName: string;

    @Column()
    lastName: string;

    @Column()
    age: number;

}

上面代码将创建以下数据库表:

+-------------+--------------+----------------------------+
|                          user                           |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| firstName   | varchar(255) |                            |
| lastName    | varchar(255) |                            |
| isActive    | boolean      |                            |
+-------------+--------------+----------------------------+

使用 @InjectRepository() 修饰器注入 对应的Repository,就可以在这个Repository对象上进行数据库的一些操作。

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) {}

  async findAll(): Promise<User[]> {
    return await this.userRepository.find();
  }
}

参考

Nestjs

浅析控制反转

依赖注入和控制反转的理解

从Express到Nestjs,谈谈Nestjs的设计思想和使用方法