从Express到Nestjs,谈谈Nestjs的设计思想和使用方法
最近已经使用过一段时间的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