最近已经使用过一段时间的 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