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

48次阅读

共计 6408 个字符,预计需要花费 17 分钟才能阅读完成。

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

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

原文在我的博客中:https://github.com/forthealll…
欢迎 star 和 fork
一、从依赖注入 (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 函数。具体的依赖注入的流程图如下所示:

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) 在请求到打真正的处理函数之间进行了层层的处理。

上图中的逻辑就是分层处理的过程,经过分层的处理请求才能到达服务端处理函数,下面我们来介绍 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

正文完
 0