Nest.js 上手指南

2023-10-03

是什么

Nest.js 是用于构建高效、可靠和可扩展的服务端应用的 Node.js 框架,可以用来开发 HTTP 服务、Websocket 服务和微服务,特点是可扩展和渐进式。可扩展是指它的模块化架构允许你灵活的引入任何第三方的库;渐进式是指框架本身没有包含所有的功能,而是可以按需引入功能模块或 Node.js 生态中的任意内容。

对比

Node.js 框架常用的有 Express、Koa、Fastify、Egg.js、Midway 等,简单做个对比:

基本概念

装饰器和依赖注入

Nest.js 与 Angular 类似,大量使用了装饰器语法和依赖注入机制:装饰器是一种特殊类型的声明,用于给类、方法、属性或参数添加元数据,在 Nest.js 里面一般用于类、方法和参数上;依赖注入是一种设计模式,可以灵活地在需要的地方来注入依赖的对象或方法等,来实现解耦合和可测试性。

举个例子:

// user.controller.ts
@Controller()
export class UserController {
  constructor(private userService: UserService) {}
 
  @Get('/user')
  async getUser() {
    return this.userService.getUserById();
  }
}
 
// user.service.ts
@Injectable()
export class UserService {
  async getUserById() {
    // ...
  }
}

上面的 @Controller()@Get()@Injectable() 都是装饰器,UserController 的 constructor 方法中是通过依赖注入机制来导入 UserService 实例,并且该实例可以在其他地方复用,不会重复进行实例化。

模块

模块是 Nest.js 中组织代码的方式,本质上和 ESM 或 CommonJS 类似,都是为了将一部分内容打包到一块,并于其他模块做隔离。不同的是 Nest.js 的模块机制构建在 ESM 之上,聚合的也都是与框架相关的内容,并且这些内容可以分散在多个文件中。 Nest.js 中定义一个模块需要使用 @Module() 装饰器,并且接收 importscontrollersprovidersexports 参数来指定模块的内容:

@Module({
  imports: [DatabaseModule],
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}

如上 UserModule 依赖了 DatabaseModule,可以在 UserService 中调用该模块的内容;UserController 中需要用到 UserService 中的一些方法,所以声明在了 providers 中;同时希望其他模块可以调用 UserService 中的方法,所以在 exports 中声明了 UserService。

每个完整的 Nest.js 应用至少要包含一个根模块。

控制器

控制器就是 MVC 架构中的 controller,它用来接收请求并返回响应,这个请求在 HTTP 服务中是 HTTP 请求,在微服务中是 RPC 请求。本质上也是一种路由机制,将特定的路径对应到特定的控制器上。

控制器的每个方法对应一个方法和路径,可以通过 @Get()@Post() 等装饰器来定义,这些方法可以传入参数作为请求路径,如 @Get('/user')。如果一个控制器的所有方法有共同的路径,可以在 @Controller() 中来定义。

@Controller('cats')
export class CatsController {
  @Get() // GET /cats
  async getAll() {
    // ...
  }
 
  @Post() // POST /cats
  async create() {
    // ...
  }
 
  @Get(':id') // GET /user/xx
  async detail() {
    // ...
  }
}

Middleware、Pipes、Guards

这三者本质上都是类似于 Express 的中间件,都是在一个请求到达 Controller 之前或 Controller 响应之后做一些操作,Nest.js 根据其功能做了细分。

Middleware:默认情况下,Nest.js 的中间件就是 Express 的中间件,与 Express 生态完全兼容,也可以是 @Injectable() 装饰器注释的类,并且实现了 NestMiddleware 类型。中间价可以执行任何代码,可以用来更改请求或响应对象,也可以提前结束当前请求等;

Pipes:管道是用 @Injectable() 装饰器注释的类,并且实现了 PipeTransform 类型。一般用来转换数据格式和参数验证,比如对请求参数做校验,对于非法的请求参数直接抛出错误并终止此次请求,也可以对请求的参数做一些转换,比如将 URL 中的一些请求参数从字符串转为数字等;

Guards:守卫是用 @Injectable() 装饰器注释的类,并且实现了 CanActivate 类型。一般用来做登录和权限的验证,当 canActive 方法返回 false 时,请求会终止;

这三种东西都可以全局配置,也可以按需配置。其执行顺序为:

客户端 -> Middleware -> Guards -> Pipes -> Controller

错误处理

Nest.js 内置了异常处理层,所有未捕获的错误都会被该层捕获,然后发送对用户友好的响应。你可以在代码中抛出 HttpException,它会自动生成对应的错误信息返回给客户端,出现无法识别的错误时,默认会返回 500 错误。

也可以自行实现 Exception filters 来按照自己的需求处理错误,比如需要添加日志或修改返回给客户端的错误信息的格式等。一个 Exception filter 需要实现 ExceptionFilter 类型,并且使用 @Catch() 装饰器来注释,例如:

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();
 
    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

实践

安装

Nest.js 提供了 CLI 工具,可以快速创建一个项目:

# 全局安装 CLI 工具
npm i -g @nestjs/cli
 
# 创建项目
nest new project-name

创建的空项目默认会包含一个根模块 AppModule,以及 AppService 和 AppController,可以算作是一个完整的应用了。

配置

在一个项目中,配置是必不可少的内容,通常这些内容会根据环境的不同而不同,比如数据库配置、Secret 等。Nest.js 官方提供了 @nestjs/config 模块用来加载配置,他默认从 env 文件和环境变量中读取配置。

使用比较简单,安装后在项目的根模块引入即可:

// app.module.ts
@Module({
  imports: [ConfigModule.forRoot({})],
  providers: [AppService],
})
export class AppModule {}
 
export class AppService {
  constructor(private configService: ConfigService) {}
 
  async func() {
    const DB_URL = this.configService.get('DB_URL');
  }
}

这样就可以在 AppModule 中调用 ConfigService 来读取配置了,需要注意的是,如果需要在其他模块中读取配置,也要在相应的模块中导入 ConfigModule。

如果想要 ConfigModule 在全局生效,不用繁琐的在每个模块都导入一边,那在根模块导入时的 forRoot 方法中添加 { isGlobal: true } 的参数即可,这样 ConfigModule 就是一个全局模块了。

数据库

和 Nest.js 搭配比较好的 ORM 是 TypeORM,官方也提供了模块封装 @nestjs/typeorm 和详细的文档,按照官方文档安装并导入即可使用。

为了更好的说明 Nest.js 是如何工作的,这里就用 Drizzle ORM 来做个示例,这个 ORM 并没有提供 Nest.js 模块的封装,需要自己封装。这里补充一个知识点,前面提到的 Provider 都是 class,比如 Service 等,但 Provider 除了是 class,也可以是对象、工厂函数等。

使用 class 类型的 Provider 时的写法如下:

@Module({
  providers: [AppService],
})
export class AppModule {}

他本质上是一个语法糖,完整的写法为:

@Module({
  providers: [
    {
      provide: AppService,
      useClass: AppService,
    },
  ],
})
export class AppModule {}

除了 useClass,还可以是 useFactory、useValue 等,这里就用 useFactory 封装一个 Drizzle ORM 模块:

export const DRIZZLE = Symbol('DRIZZLE_INSTANCE');
 
@Global()
@Module({
  providers: [
    {
      provide: DRIZZLE,
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => {
        const client = postgres(configService.get<string>('DATABASE_URL'));
        return drizzle(client);
      },
    },
  ],
  exports: [DRIZZLE],
})
export class DrizzleModule {}

这样就可以在其他模块中导入 Drizzle 实例了,并且多个模块中共用同一个实例,不会重复生成多个。

@Injectable()
export class UserService {
  constructor(@Inject(DRIZZLE) private readonly drizzle: Drizzle) {}
 
  async getUserById() {
    // this.drizzle.select().from().where();
  }
}

至此已经包含了 Nest.js 中大多数的常用内容了,更详细的内容可以参考官方文档。