Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
forthealllight opened this issue Dec 19, 2018 · 0 comments

Comments

@forthealllight
Copy link
Owner

@forthealllight forthealllight commented Dec 19, 2018

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


  最近已经使用过一段时间的nestjs,让人写着有一种java spring的感觉,nestjs可以使用express的所有中间件,此外完美的支持typescript,与数据库关系映射typeorm配合使用可以快速的编写一个接口网关。本文会介绍一下作为一款企业级的node框架的特点和优点。

  • 从依赖注入(DI)谈起
  • 装饰器和注解
  • nestjs的“洋葱模型”
  • nestjs的特点总结

一、从依赖注入(DI)谈起

(1)、angular中的依赖注入

  从angular1.x开始,实现了依赖注入或者说控制反转的模式,angular1.x中就有controller(控制器)、service(服务),模块(module)。笔者在早年间写过一段时间的angular1.3,下面举例来说明:

var myapp=angular.module('myapp',['ui.router']);
myapp.controller('test1',function($scope,$timeout){}
myapp.controller('test2',function($scope,$state){}

  上面这个就是angular1.3中的一个依赖注入的例子,首先定义了模块名为“myapp”的module, 接着在myapp这个模块中定义controller控制器。将myapp模块的控制权交给了myapp.controller函数。具体的依赖注入的流程图如下所示:

default

myapp这个模块如何定义,由于它的两个控制器决定,此外在控制器中又依赖于$scope、$timeout等服务。这样就实现了依赖注入,或者说控制反转。

(2)、什么是依赖注入

  用一个例子来通俗的讲讲什么是依赖注入。

class Cat{

}
class Tiger{

}
class Zoo{
  constructor(){
     this.tiger = new Tiger();
     this.cat = new Cat();
  }
}

  上述的例子中,我们定义Zoo,在其constructor的方法中进行对于Cat和Tiger的实例化,此时如果我们要为Zoo增加一个实例变量,比如去修改Zoo类本身,比如我们现在想为Zoo类增加一个Fish类的实例变量:

class Fish{}

class Zoo{
  constructor(){
     this.tiger = new Tiger();
     this.cat = new Cat();
     this.fish = new Fish();
  }
}

  此外如果我们要修改在Zoo中实例化时,传入Tiger和Cat类的变量,也必须在Zoo类上修改。这种反反复复的修改会使得Zoo类并没有通用性,使得Zoo类的功能需要反复测试。

我们设想将实例化的过程以参数的形式传递给Zoo类:

class Zoo{
  constructor(options){
     this.options = options;
  }
}
var zoo = new Zoo({
  tiger: new Tiger(),
  cat: new Cat(),
  fish: new Fish()
})

  我们将实力化的过程放入参数中,传入给Zoo的构造函数,这样我们就不用在Zoo类中反复的去修改代码。这是一个简单的介绍依赖注入的例子,更为完全使用依赖注入的可以为Zoo类增加静态方法和静态属性:

class Zoo{
  static animals = [];
  constructor(options){
     this.options = options;
     this.init();
  }
  init(){
    let _this = this;
    animals.forEach(function(item){
      item.call(_this,options);
    })
  }
  static use(module){
     animals.push([...module])
  }
}
Zoo.use[Cat,Tiger,Fish];
var zoo = new Zoo(options);

  上述我们用Zoo的静态方法use往Zoo类中注入Cat、Tiger、Fish模块,将Zoo的具体实现移交给了Cat和Tiger和Fish模块,以及构造函数中传入的options参数。

(3)、nestjs中的依赖注入

  在nestjs中也参考了angular中的依赖注入的思想,也是用module、controller和service。

@Module({
  imports:[otherModule],
  providers:[SaveService],
  controllers:[SaveController,SaveExtroController]
})
export class SaveModule {}

  上面就是nestjs中如何定一个module,在imports属性中可以注入其他模块,在prividers注入相应的在控制器中需要用到的service,在控制器中注入需要的controller。

二、装饰器和注解

  在nestjs中,完美的拥抱了typescript,特别是大量的使用装饰器和注解,对于装饰器和注解的理解可以参考我的这篇文章:Typescript中的装饰器和注解。我们来看使用了装饰器和注解后,在nestjs中编写业务代码有多么的简洁:

import { Controller, Get, Req, Res } from '@nestjs/common';

@Controller('cats')

export class CatsController {
  @Get()
  findAll(@Req() req,@Res() res) {
    return 'This action returns all cats';
  }
}

  上述定义两个一个处理url为“/cats”的控制器,对于这个路由的get方法,定义了findAll函数。当以get方法,请求/cats的时候,就会主动的触发findAll函数。

  此外在findAll函数中,通过req和res参数,在主题内也可以直接使用请求request以及对于请求的响应response。比如我们通过req上来获取请求的参数,以及通过res.send来返回请求结果。

三、nestjs的“洋葱模型”

  这里简单讲讲在nestjs中是如何分层的,也就是说请求到达服务端后如何层层处理,直到响应请求并将结果返回客户端。

在nestjs中在service的基础上,按处理的层次补充了中间件(middleware)、异常处理(Exception filters)、管道(Pipes),守卫(Guards),以及拦截器(interceptors)在请求到打真正的处理函数之间进行了层层的处理。

default

上图中的逻辑就是分层处理的过程,经过分层的处理请求才能到达服务端处理函数,下面我们来介绍nestjs中的层层模型的具体作用。

(1)、middleware中间件

  在nestjs中的middle完全跟express的中间件一摸一样。不仅如此,我们还可以直接使用express中的中间件,比如在我的应用中需要处理core跨域:

import * as cors from 'cors';
async function bootstrap() {
  onst app = await NestFactory.create(/* 创建app的业务逻辑*/)
  app.use(cors({
    origin:'http://localhost:8080',
    credentials:true
  }));
  await app.listen(3000)
}
bootstrap();

在上述的代码中我们可以直接通过app.use来使用core这个express中的中间件。从而使得server端支持core跨域等。

初此之外,跟nestjs的中间件也完全保留了express中的中间件的特点:

  • 在中间件中接受response和request作为参数,并且可以修改请求对象request和结果返回对象response
  • 可以结束对于请求的处理,直接将请求的结果返回,也就是说可以在中间件中直接res.send等。
  • 在该中间件处理完毕后,如果没有将请求结果返回,那么可以通过next方法,将中间件传递给下一个中间件处理。

在nestjs中,中间件跟express中完全一样,除了可以复用express中间件外,在nestjs中针对某一个特定的路由来使用中间件也十分的方便:

class ApplicationModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats');
  }
}

上面就是对于特定的路由url为/cats的时候,使用LoggerMiddleware中间件。

(2)、Exception filters异常过滤器

  Exception filters异常过滤器可以捕获在后端接受处理任何阶段所跑出的异常,捕获到异常后,然后返回处理过的异常结果给客户端(比如返回错误码,错误提示信息等等)。

  我们可以自定义一个异常过滤器,并且在这个异常过滤器中可以指定需要捕获哪些异常,并且对于这些异常应该返回什么结果等,举例一个自定义过滤器用于捕获HttpException异常的例子。

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

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

  我们可以看到host是实现了ArgumentsHost接口的,在host中可以获取运行环境中的信息,如果在http请求中那么可以获取request和response,如果在socket中也可以获取client和data信息。

  同样的,对于异常过滤器,我们可以指定在某一个模块中使用,或者指定其在全局使用等。

(3)Pipes管道

  Pipes一般用户验证请求中参数是否符合要求,起到一个校验参数的功能。

  比如我们对于一个请求中的某些参数,需要校验或者转化参数的类型:

@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): number {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException('Validation failed');
    }
    return val;
  }
}

  上述的ParseIntPipe就可以把参数转化成十进制的整型数字。我们可以这样使用:

@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
  return await this.catsService.findOne(id);
}

  对于get请求中的参数id,调用new ParseIntPipe方法来将id参数转化成十进制的整数。

(4)Guards守卫

  Guards守卫,其作用就是决定一个请求是否应该被处理函数接受并处理,当然我们也可以在middleware中间件中来做请求的接受与否的处理,与middleware相比,Guards可以获得更加详细的关于请求的执行上下文信息。

通常Guards守卫层,位于middleware之后,请求正式被处理函数处理之前。

下面是一个Guards的例子:

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

这里的context实现了一个ExecutionContext接口,该接口中具有丰富的执行上下文信息。

export interface ArgumentsHost {
  getArgs<T extends Array<any> = any[]>(): T;
  getArgByIndex<T = any>(index: number): T;
  switchToRpc(): RpcArgumentsHost;
  switchToHttp(): HttpArgumentsHost;
  switchToWs(): WsArgumentsHost;
}

export interface ExecutionContext extends ArgumentsHost {
  getClass<T = any>(): Type<T>;
  getHandler(): Function;
}

除了ArgumentsHost中的信息外,ExecutionContext还包含了getClass用户获取对于某一个路由处理的,控制器。而getClass用于获取返回对于指定路由后台处理时的处理函数。

对于Guards处理函数,如果返回true,那么请求会被正常的处理,如果返回false那么请求会抛出异常。

(5)、interceptors拦截器

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

概括来说:

interceptors拦截器在函数执行前或者执行后可以运行,如果在执行后运行,可以拦截函数执行的返回结果,修改参数等。

再来举一个超时处理的例子:

@Injectable()
export class TimeoutInterceptor implements NestInterceptor{
  intercept(
    context:ExecutionContext,
    call$:Observable<any>
  ):Observable<any>{
    return call$.pipe(timeout(5000));
  }
}

该拦截器可以定义在控制器上,可以处理超时请求。

四、nestjs的特点总结

  最后总结一下nestjs的优缺。

nestjs的优点:

  • 完美的支持typescript,因此可以使用日益繁荣的ts生态工具
  • 兼容express中间件,因为express是最早出现的轻量级的node server端框架,nestjs能够利用所有express的中间件,使其生态完善
  • 层层处理,一定程度上可以约束代码,比如何时使用中间件、何时需要使用guards守卫等。
  • 依赖注入以及模块化的思想,提供了完整的mvc的链路,使得代码结构清晰,便于维护,这里的m是数据层可以通过modules的形式注入,比如通过typeorm的entity就可以在模块中注入modules。
  • 完美支持rxjs
@forthealllight forthealllight changed the title 从Express到Nestjs,带你深入理解N e s t j s 从Express到Nestjs,带你深入理解Nestjs的设计思想和使用方法 Dec 19, 2018
@forthealllight forthealllight changed the title 从Express到Nestjs,带你深入理解Nestjs的设计思想和使用方法 从Express到Nestjs,深入理解Nestjs的设计思想和使用方法 Dec 19, 2018
@forthealllight forthealllight changed the title 从Express到Nestjs,深入理解Nestjs的设计思想和使用方法 从Express到Nestjs,谈谈Nestjs的设计思想和使用方法 Dec 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
1 participant
You can’t perform that action at this time.