为什么说 Nest.js 提供了 Express 没有的架构能力?
Nest.js 是当前最流行的 Node.js 框架,现在已经 56k star 了
有的同学说,不是还有 express 么?
其实严格来说 express 并不是一个框架,它只是提供了基于中间件的请求响应处理流程。
但它并没有规定代码应该怎么组织,怎么复用,怎么集成各种方案,所以代码能写成各种样子,这对于大项目开发来说是很难维护的。
所以出现了更上层的 node 框架,比如 egg、midway、nest 这些,它们额外提供了架构能力,这类框架也叫企业级开发框架。
nest 是最优秀的 node 企业级开发框架。
其实 nest 的底层也是 express,但它封装出了 IOC、AOP 等架构特性。
我们分别来看一下它提供的这些架构特性:
nest 最主要的特性就是模块机制了,它类似 es module 但又有很多不一样。
es module 是这样的:
import { a, b } from 'a';
const c = 1;
export { a, c };
而 nest 的 module 机制是这样写:
import { Module } from '@nestjs/common';
import { aaa } from './aa.module';
import { CatsService } from './cats.service';
@Module({
imports: [aaa]
providers: [CatsService],
exports: [CatsService]
})
export class CatsModule {}
可以 import 别的模块,可以 export 内部的值。
这些内部的值是通过 provider 提供的,其实 import、export 导入导出的都是这些 provider。
这和 es module 也差不多呀?为啥要自己搞一套模块机制?
因为这套模块机制是可以实现自动注入依赖的,也就是 DI(Denpendency Injection),我们说的 IOC (Inverse Of Control)容器就是有这种依赖注入机制的容器。
这俩概念可能你不理解,看个例子就懂了。
我们写个 AppService 类,它有个 getHello 方法,通过 @Injectable 装饰器声明它是可以被注入的。
把它放到 module 的 provider 里:
这里还有个 controller,它是用来处理 http 请求的,也可以被注入:
它在构造器里声明了对 appService 的依赖。
然后我们把这个应用跑起来:
打个断点:
可以看到这时候 AppService 是有值的。
明明我们只是在 controller 里声明了对这个 service 的依赖,并没有 new 这个 AppService 的对象,也没有 new contoller 的时候传入 AppService 对象。
那它是怎么创建的对象,又是怎么注入的依赖呢?
这些就是 Nest 的模块机制提供的特性了,也就是 DI 或者说 IOC 的能力。
自动化创建对象,并且根据依赖关系自动注入。
这样就算你的对象之间的关系再错综复杂也不用自己去 new 和组装,就很方便。
我们再来测试个 import 的例子:
我 import 了一个操作数据库的 module。
然后在这个 module 的一个 Service 里就可以直接用数据库 module 内的做 crud 的 repository 对象了:
也就是说 module 内部的 provider 之间可以相互注入,provider 也可以注入 controller 中用。
如果 import 了别的 module,那也可以注入它 export 的 provider 了。
这就是 nest 自己实现的模块机制和 es module 最大的不同:实现了依赖自动注入。
用了 nest,只要按照它的写法来声明 @Module 和 @Injectable,就可以使用这种 IOC 能力。
而 express 呢?
你需要手动 new 一个个对象,手动组装。
这是 Nest 提供的第一个架构能力:IOC。
接下来是 Nest 提供的第二个架构能力:AOP。
AOP 是 Aspect Oriented Programming,面向切面编程。
切面也就是处理流程中的某个点,在这个点来扩展一些逻辑,比如在 controller 之前加入日志、权限、异常处理等逻辑:
nest 里主要有 4 种切面:
第一个是 Guard:
它是用来在 Controller 之前判断权限,返回 true、false 来表示是否继续:
要声明在 Contoller 上:
第二个是 Interceptor:
它用来在请求前后加入一些逻辑:
也是声明在 Controller 上:
第三个是 Pipe:
它用来在参数传入 Controller 之前做一些转换:
声明在 Contoller 的某个路由上:
第四个是 ExceptionFilter:
处理过程中可能会抛不同的异常,而 ExceptionFilter 就是处理这些异常,返回友好的信息给客户端的:
也是声明在某个路由上:
这就是 nest 提供的 4 种切面,可以声明某种功能的切面,然后加入到任意的处理流程中。因为是有统一的规范的,所以复用起来很容易。
而 express 呢?
并没有抽象出这些切面,所以代码没有规范,复用也比较困难。
这就是 nest 提供的第二种架构能力:AOP。
nest 的第三种架构能力是可以任意切换平台。
前面说,nest 的底层是 express,其实并不准确,nest 并没有和 express 耦合。
它所有的上层代码都是基于一个抽象的接口的:
而这个接口有 express 和 fastify 两种实现:
分别放在 @nestjs/platform-express 和 @nestjs/platform-fastify 包里。
默认用的是 express:
可以灵活切换 http 的底层平台。
不只是 http 可以切换具体的底层平台,websocket 也是,可以切换 socket.io 和 ws:
通过这层层抽象,就达到了不依赖任何一个底层平台的效果:
哪怕有一天,有一个新的 http 库取代了 express,那对 nest 有影响么?
没有,只要加一个新平台的适配器就可以了。
这就是 nest 的架构的强大之处。
而且不只是可以灵活切换 http 和 ws 平台这么简单。
你写的一些 Guard、Exception Filter、Interceptor、Pipe 甚至可以用在 websocket 和微服务里:
可以跨多种上下文来复用代码:
它提供了 ArgumentsHost 类,在切面里拿到它之后,可以判断出当前是什么上下文:
然后切换到对应的上下文来写后面的的逻辑:
这样就实现了切面的跨上下文复用。
那 express 呢?
并没有这种跨多种平台复用代码的一些抽象。
这就是 nest 提供的第三种架构能力:可以任意切换底层平台和执行上下文。
有了这三种架构特性,代码自然会变得松散耦合、易于扩展,易于维护,所以这种 node 框架才叫做企业级开发框架。
总结
nest 是在 express 之上封装的一层,提供了很多架构的能力:
- IOC:自己实现了模块机制,可以导入导出 provider,实现自动依赖注入,简化了对象的创建
- AOP:抽象了 Guard、Interceptor、Pipe、Exception Filter 这 4 种切面,可以通过切面抽离一些通用逻辑,然后动态添加到某个流程中
- 任意切换底层平台:nest 基于 ts 的 interface 实现了不和任何底层平台耦合,http 可以切换 express 和 fastify,websocket 可以切换 socket.io 和 ws。而且 4 种切面也实现了可以跨 http、websocket、微服务来复用。
明显能感受到,用了 nest 之后,代码会变得很容易维护:通用逻辑都放在切面里复用、不同的业务模块放到不同的 Module 里,依赖自动注入,上层不改一行代码就可以切换底层平台。
而且 nest 基于这个架构提供了对各种方案的集成,比如 mq、redis 的集成、比如 graphql 和 websocket、比如 jwt 等。
架构优雅、各种解决方案开箱即用,这就是 nest 声称自己是企业级开发框架的原因。它对标的是 java 里的 spring。
相比之下,express 虽然也能实现各种功能,但是在架构方面,在其他方案的集成的简便性方面,还是不够的。
如果你要开发一个相对复杂的 node 服务,还是用 nest 吧。