Nest.js:给你看个不一样的 Node.js

Posted Qunar技术沙龙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Nest.js:给你看个不一样的 Node.js相关的知识,希望对你有一定的参考价值。


姚建龙



个人介绍:姚建龙,2014年加入去哪儿网技术团队。目前主要负责大住宿RN相关业务,参与开发了酒店eb系统、酒店touch站、easy住地图选房等项目。对跨平台实现方案、混合开发等很感兴趣。


Nest.js:给你看个不一样的 Node.js

简介

NestJs 是一款用于构建高效且可伸缩 Web 应用程序的渐进式 Node.js 框架。看下官方给的简介,NestJs 模块化的体系结构,允许开发者使用任何其他的库,从而提供灵活性;为 Nodejs 提供一个适应性强大的生态系统;利用最新的js特性,为 nodejs 提供更加方便的设计模式和成熟的解决方案。

Nest.js:给你看个不一样的 Node.js

在利用 NestJs 框架中,开发者可以体验到 NestJs 清晰的模块组织方式,隔离应用的各个工作区间;在各种工作区间都涉及不一样的设计模式,比如面相对象的SOLID原则在整个应用构建中的体现,AOP、DI、IoC 等在各个核心部件中的应用,OOP、FP、FRP 在各模块编码中的完美结合,都给开发中带来不一样的体验。

设计哲学

NestJs 的设计是为了解决 Node.js 体系结构的问题,NestJs 旨在为 Node.js 提供一个开箱即用的应用程序体系结构,允许轻松创建高可测试性、可伸缩、松散耦合和易于维护的应用程序。

Nest.js:给你看个不一样的 Node.js

NestJs 底层使用 express 进行构建,推荐使用 Typescript 进行代码编写的同时也支持原生 javascript 的降级编写方式,在代码的组织结构上借鉴了 Angular 的组织方式,NestJs 基础库中设计了大量的装饰器,同时支持开发中自定义业务需要的装饰器。

Nest.js:给你看个不一样的 Node.js

核心组件

NestJs 主要有 8 个组件(Controller 控制器、Component 组件、Module 模块、Middlewares 中间件、Exception Filters 异常过滤器、Pipes 管道、Guards 守卫、Interceptors 拦截器),主要通过 Controller、Component、Module 三个最核心的组件构成。

Nest.js:给你看个不一样的 Node.js

其中 Controller 是传统意义的控制器,工程启动时,控制台可以清晰的看到应用的路由配置信息,Provider 一般做 Service,比如数据的 CRUD 都可以封装在 Service 中,每一个 Service 就是一个 Provider,也是主要的业务代码逻辑部分,Module 表示应用或者模块,一个 Module 可以拥有多个 Controller。

 
   
   
 
  1. [Nest] 7209   - 2018-8-31 20:38:52   [NestFactory] Starting Nest application...

  2. [Nest] 7209   - 2018-8-31 20:38:52   [InstanceLoader] ApplicationModule dependencies initialized +9ms

  3. [Nest] 7209   - 2018-8-31 20:38:52   [InstanceLoader] CatsModule dependencies initialized +1ms

  4. [Nest] 7209   - 2018-8-31 20:38:52   [RoutesResolver] CatsController {/cats}: +21ms

  5. [Nest] 7209   - 2018-8-31 20:38:52   [RouterExplorer] Mapped {/, POST} route +3ms

  6. [Nest] 7209   - 2018-8-31 20:38:52   [RouterExplorer] Mapped {/, GET} route +0ms

  7. [Nest] 7209   - 2018-8-31 20:38:52   [RouterExplorer] Mapped {/:id, GET} route +1ms

  8. [Nest] 7209   - 2018-8-31 20:38:52   [NestApplication] Nest application successfully started +1ms

Controller

控制器层负责处理传入的请求,并返回对客户端的响应,这部分和传统的控制器没有差异。

Nest.js:给你看个不一样的 Node.js

NestJs 中通过 @Controller() 装饰器来创建控制器,可以通过 @Get()、@Post 等对类方法进行修饰,说明请求方式,以此实现 NestJs 路由的去中心化。

 
   
   
 
  1. import {

  2.  Body,

  3.  Controller,

  4.  Get,

  5.  Param,

  6.  Post,

  7. } from '@nestjs/common';

  8. import { CatsService } from './cats.service';

  9. import { CreateCatDto } from './dto/create-cat.dto';

  10. import { Cat } from './interfaces/cat.interface';

  11. @Controller('cats')

  12. export class CatsController {

  13.  constructor(private readonly catsService: CatsService) {}

  14.  @Post()

  15.  async create(@Body() createCatDto: CreateCatDto) {

  16.    this.catsService.create(createCatDto);

  17.  }

  18.  @Get()

  19.  async findAll(): Promise<Cat[]> {

  20.    return this.catsService.findAll();

  21.  }

  22.  @Get(':id')

  23.  findOne(

  24.    @Param('id', new ParseIntPipe())

  25.    id,

  26.  ) {

  27.    // logic

  28.  }

  29. }

NestJs 支持 asnyc/await,极大的方便开发者,同时,NestJs 为开发者提供了丰富的修饰器方法用于解析及处理 http 请求,使得开发者更快更便捷的构建自己的控制器。

Provider

几乎所有的东西都可以被认为是 Provider(service, repository, factory, helper 等等)。他们都可以注入依赖关系 constructor,也就是说,他们可以创建各种关系。但事实上,提供者不过是一个用 @Injectable() 装饰器注解的简单类。

注:Nest4.x 为 Component,Nest 5.x 更新为 Provider

Nest.js:给你看个不一样的 Node.js

值得注意的是 NestJs 推荐使用面对象的方式来设计和组织依赖性,因此强烈建议遵循 SOLID 原则。

HINT:Since Nest enables the possibility to design and organize the dependencies in a more OO-way, we strongly recommend following the SOLID principles.

Nest.js:给你看个不一样的 Node.js

在 NestJs 中,经常使用依赖注入,当我们设计好一个 Provider 之后,就可以通过 Controller 类构造函数注入。

 
   
   
 
  1. import { Injectable } from '@nestjs/common';

  2. import { Cat } from './interfaces/cat.interface';

  3. @Injectable()

  4. export class CatsService {

  5.  private readonly cats: Cat[] = [];

  6.  create(cat: Cat) {

  7.    this.cats.push(cat);

  8.  }

  9.  findAll(): Cat[] {

  10.    return this.cats;

  11.  }

  12. }

Modules

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

Nest.js:给你看个不一样的 Node.js

当我们定义编写完某块业务逻辑的 Controller 和 Provider 之后,我们就可以通过 @Module() 将对应模块的逻辑组织起来,NestJs 同时提供模块导出、动态模块、共享模块的支持,可以很方便的解决业务场景中常见的单例等问题。

 
   
   
 
  1. import { Module } from '@nestjs/common';

  2. import { CatsController } from './cats.controller';

  3. import { CatsService } from './cats.service';

  4. @Module({

  5.  controllers: [CatsController],

  6.  providers: [CatsService],

  7. })

  8. export class CatsModule {}

至此,如上述代码,我们就通过 Controller、Provider 以及 Module 就构建出了一个简单的应用。 然鹅,我们的实际的应用需求并不仅限于此,在真实的世界里,我们还得处理数据的校验、异常的捕获及处理、权限管理、日志记录等等一系列实际的问题。别担心,NestJs 早已为我们想好了一切。

Middleware

NestJs 的中间件对 express 的中间件进行了一层封装。中间件在路由处理器之前被调用。 中间件可以访问请求和响应对象,以及应用程序请求响应周期中的下一个中间件功能。下一个中间件函数通常由名为 next 的变量表示。

Nest.js:给你看个不一样的 Node.js

与 express 不同的是,NestJs 的中间件在使用时,需要通过实现 NestModule 的 configure 方法来引入,中间件可以这对单独的路由或者控制器进行处理。

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

  2. import { LoggerMiddleware } from './common/middlewares/logger.middleware';

  3. import { CatsModule } from './cats/cats.module';

  4. @Module({

  5.  imports: [CatsModule],

  6. })

  7. export class ApplicationModule implements NestModule {

  8.  configure(consumer: MiddlewareConsumer) {

  9.    consumer

  10.      .apply(LoggerMiddleware)

  11.      .forRoutes('/cats’); // ‘/cats’ 变更为 CatsController 则是对控制器进行中间件配置

  12.  }

  13. }

与此同时,NestJs 同样也支持函数式的中间件、异步中间件。

 
   
   
 
  1. // 异步中间件

  2. import { Injectable, NestMiddleware, MiddlewareFunction } from '@nestjs/common';

  3. @Injectable()

  4. export class LoggerMiddleware implements NestMiddleware {

  5.  async resolve(name: string): Promise<MiddlewareFunction> {

  6.    await someAsyncJob();

  7.    return async (req, res, next) => {

  8.      await someAsyncJob();

  9.      console.log(`[${name}] Request...`); // [ApplicationModule] Request...

  10.      next();

  11.    };

  12. }

  13. }

  14. // 函数式中间件

  15. export function logger(req, res, next) {

  16.  console.log(`Request...`);

  17.  next();

  18. };

全局中间件配置,需要在 NestFactory 创建的实例上进行配置,这里配置方法和 express 类似

 
   
   
 
  1. const app = await NestFactory.create(ApplicationModule);

  2. app.use(logger);

  3. await app.listen(3000);

Exception Filters

健壮的应用应该有完善的异常处理逻辑,并且有针对的将异常划分为不同的层次,未捕获到异常时,需要给用户良好的响应,NestJs 提供 HttpException 来处理 http 请求的异常。

Nest.js:给你看个不一样的 Node.js

NestJs 通过 @ UseFilters() 设置需要使用的过滤器,通过 @Catch() 来捕获异常抛给对应的异常过滤器处理。

 
   
   
 
  1. // 设置异常过滤器

  2. @Post()

  3. @UseFilters(new HttpExceptionFilter())

  4. async create(@Body() createCatDto: CreateCatDto) {

  5.  throw new ForbiddenException();

  6. }

  7. // 捕获异常,并给异常过滤器处理

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

  9. import { HttpException } from '@nestjs/common';

  10. @Catch(HttpException)

  11. export class HttpExceptionFilter implements ExceptionFilter {

  12.  catch(exception: HttpException, host: ArgumentsHost) {

  13.    const ctx = host.switchToHttp();

  14.    const response = ctx.getResponse();

  15.    const request = ctx.getRequest();

  16.    response

  17.      .status(status)

  18.      .json({

  19.        statusCode: exception.getStatus(),

  20.        timestamp: new Date().toISOString(),

  21.        path: request.url,

  22.      });

  23.  }

  24. }

好的异常系统设计应该是分层次的,同样的,NestJs 支持开发者自定义异常、设置异常的处理顺序、设置全局异常处理等。

 
   
   
 
  1. import { HttpExceptionFilter } from './exceptions/http-exception.filter';

  2. import { AnyExceptionFilter } from './exceptions/any-exception.filter’;

  3. const app = await NestFactory.create(ApplicationModule);

  4. // 异常捕获顺序根据 useGlobalFilters 顺序决定

  5. app.useGlobalFilters(new HttpExceptionFilter());

  6. app.useGlobalFilters(new AnyExceptionFilter());

  7. await app.listen(3000);

Pipe

管道是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口。

Nest.js:给你看个不一样的 Node.js

管道将输入数据转换为所需的输出,比如请求数据格式化、请求数据校验抛异常处理等等。

 
   
   
 
  1. // create-cat.dto.ts

  2. import { IsString, IsInt } from 'class-validator';

  3. export class CreateCatDto {

  4.  @IsString()

  5.  readonly name: string;

  6.  @IsInt()

  7.  readonly age: number;

  8.  @IsString()

  9.  readonly breed: string;

  10. }

  11. // validation.pipe.ts

  12. import { PipeTransform, Pipe, ArgumentMetadata, BadRequestException } from '@nestjs/common';

  13. import { validate } from 'class-validator';

  14. import { plainToClass } from 'class-transformer';

  15. @Injectable()

  16. export class ValidationPipe implements PipeTransform<any> {

  17.    async transform(value, metadata: ArgumentMetadata) {

  18.      const { metatype } = metadata;

  19.      if (!metatype || !this.toValidate(metatype)) {

  20.          return value;

  21.      }

  22.      const object = plainToClass(metatype, value);

  23.      const errors = await validate(object);

  24.      if (errors.length > 0) {

  25.          throw new BadRequestException('Validation failed');

  26.      }

  27.      return value;

  28.    }

  29.    private toValidate(metatype): boolean {

  30.      const types = [String, Boolean, Number, Array, Object];

  31.      return !types.find((type) => metatype === type);

  32.    }

  33. }

  34. // cats.controler.ts

  35. @Post()

  36. @UsePipes(new ValidationPipe())

  37. async create(@Body() createCatDto: CreateCatDto) {

  38.  this.catsService.create(createCatDto);

  39. }

同样地,管道也可以设置对全局路由进行处理(@useGlobalPipes)。

Guard

守卫有一个单独的责任。它确定请求是否应该由路由处理程序处理。到目前为止, 访问限制逻辑大多在中间件内。这样很好, 因为诸如 token 验证或将 req 对象附加属性与特定路由没有强关联。但中间件是非常笨拙的。它不知道调用 next() 函数后应该执行哪个处理程序。另一方面, 守卫可以访问 ExecutionContext 对象, 所以我们确切知道将要评估什么。

Nest.js:给你看个不一样的 Node.js

注:守卫是在每个中间件之后执行的, 但在管道之前。

守卫是一个使用 @Injectable() 装饰器的类。 守卫需要实现 canActivate 方法。

 
   
   
 
  1. // 角色装饰器 roles.decorator.ts

  2. import { ReflectMetadata } from '@nestjs/common';

  3. export const Roles = (...roles: string[]) => ReflectMetadata('roles', roles);

  4. //  用角色装饰器修饰路由 cats.controller.ts

  5. @Post()

  6. @Roles('admin')

  7. async create(@Body() createCatDto: CreateCatDto) {

  8.  this.catsService.create(createCatDto);

  9. }

  10. // 基于权限判断的守卫: roles.guard.ts

  11. import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';

  12. import { Observable } from 'rxjs';

  13. import { Reflector } from '@nestjs/core';

  14. @Injectable()

  15. export class RolesGuard implements CanActivate {

  16.  constructor(private readonly reflector: Reflector) {}

  17.  canActivate(context: ExecutionContext): boolean {

  18.    const roles = this.reflector.get<string[]>('roles', context.getHandler());

  19.    if (!roles) {

  20.      return true;

  21.    }

  22.    const request = context.switchToHttp().getRequest();

  23.    const user = request.user;

  24.    const hasRole = () => user.roles.some((role) => roles.includes(role));

  25.    return user && user.roles && hasRole();

  26.  }

  27. }

从这里可以看出,首先拿到执行上下文的关键就是这里的反射器,通过反射拿到待执行处理的路由访问权限信息,判断该请求是否具备访问改路由的权限,从而很好的将权限校验的逻辑从业务逻辑中分离出来。

Interceptor

拦截器是 @Injectable() 装饰器注解的类。拦截器应该实现 NestInterceptor 接口。 拦截器具有一系列有用的功能,这些功能受面向切面编程(AOP)技术的启发。它们可以:

在函数执行之前/之后绑定额外的逻辑 转换从函数返回的结果 转换从函数抛出的异常 根据所选条件完全重写函数 (例如, 缓存目的)

Nest.js:给你看个不一样的 Node.js

NestJs 的拦截器借助于 rxjs 强大功能来实现。

 
   
   
 
  1. // 请求/响应日志记录 logging.interceptor.ts

  2. import { Injectable, NestInterceptor, ExecutionContext } from '@nestjs/common';

  3. import { Observable } from 'rxjs';

  4. import { tap } from 'rxjs/operators';

  5. @Injectable()

  6. export class LoggingInterceptor implements NestInterceptor {

  7.  intercept(

  8.    context: ExecutionContext,

  9.    call$: Observable<any>,

  10.  ): Observable<any> {

  11.    console.log('Before...');

  12.    const now = Date.now();

  13.    return call$.pipe(

  14.      tap(() => console.log(`After... ${Date.now() - now}ms`)),

  15.    );

  16.  }

  17. }

  18. // UseInterceptors来用装饰控制器

  19. @UseInterceptors(LoggingInterceptor)

  20. export class CatsController {}

响应顺序

综上所述,NestJs 各部件调用顺序可以整理如下:

Nest.js:给你看个不一样的 Node.js

总结:

根据上述的各部件功能拆分,NestJs 工程组织结构大致可以形成如下的组织结构:

 
   
   
 
  1. .

  2. ├── jest.json

  3. ├── node_modules

  4. ├── package-lock.json

  5. ├── package.json

  6. ├── src

  7.   ├── app.module.ts // 模块聚合

  8.   ├── cats // 业务模块

  9.     ├── cats.controller.spec.ts

  10.     ├── cats.controller.ts

  11.     ├── cats.module.ts

  12.     ├── cats.service.ts

  13.     ├── dto

  14.     └── interfaces

  15.   ├── common

  16.     ├── decorators // 自定义装饰器等等

  17.     ├── filters // 过滤器:异常处理等等

  18.     ├── guards // 守卫:权限校验等等

  19.     ├── interceptors // 拦截器:AOP处理

  20.     ├── middlewares // 中间件处理

  21.     └── pipes // 管道处理,DTO数据处理等等

  22.   └── main.ts // 主入口

  23. ├── tsconfig.json

  24. ├── tslint.json

  25. └── yarn.lock

NestJs 不单单针对 Node.js 进行框架封装,同时 NestJs 官方针对 GrapQL、MicroService、Websocket 等服务提供了不同的解决方案。写到这里,是否很多东西都有 Java spring 的影子,是的,后端的框架发展以及很成熟,相反,前端发展就庞杂了,回头发现不管框架语言如何的变化,不变的是编程的思想,前端要走的路还有很远。

以上是关于Nest.js:给你看个不一样的 Node.js的主要内容,如果未能解决你的问题,请参考以下文章

Nest+Vue实战:工作计划管理系统

nest.js + typeORM:基本使用

前端工程师梭哈初体验(基于Nest.js写服务端代码)

Nest.js 是如何实现 AOP 架构的?

核弹级漏洞,把 log4j 扒给你看!

nestjs为啥不火