为什么从egg.js到nest.js

13,975 阅读7分钟

背景

进入部门工作后,接触到的node.js服务端框架,是egg.js,后面基于扩展增加了很多插件,比如:@Controller @Service等注解,还有针对egg-framework 定制化部门使用的底层framework。

但是,随着时间的迁移,egg已经不太能满足我们的开发效率和开发模式,主要有以下几点:

  • 对typescript支持度不够,这是由于egg.js本身就不是typescript开发
  • egg.js封装web架构,约定大于编码,如:强制将web应用分级为: controller、service、middleware、extend等,自由度相对比较弱,当你需要定制化开发内容,你需要深入了解egg.js的整个运行原理才能实现
  • 虽然部门内部定制化开发 @Controller @Service等注解,减少路由配置,但是这一块插件还存在一些隐藏规则,需要开发注意

当然egg.js运行的web应用还是比较稳定,而且相关插件生态也比较丰富,只是当egg.js迭代更新速度在2020年后就逐步放缓,更不上变化,我们就需要迎接一些新的框架来满足要求。

框架对比

我从近两年听到或者网上收集的,基于Node.js的框架主要有以下几个:

  • 基础框架,基本上还是以express、koa、Fastify.js等为主
  • egg.js,以MVC为架构的web框架
  • nest.js,以Ioc 控制反转作为核心概念的web框架,对typescript支持友好
  • nuxt.js,以Vue.js作为SSR服务端渲染核心的web框架,最新是Nuxt3(以Vue3为核心)
  • next.js,以React.js作为SSR服务端渲染核心的
  • Meteor.js,full-stack javascript平台,最大的特点是当数据发生改变的时候,所有依赖该数据的地方自动发生相应的改变。
  • Fastify.js,号称最快的node.js web框架,特点是内置了基于 JSON schema 的 validation 和 serialization,比JSON.stringify还快的json序列化算法,虽然是借助借助第三方库 ajv。
  • strapi.js,快速生成API接口的web框架,同时实现各种后端所需要的鉴权、权限、文件上传等轮子

对比一下,我们主要用来开发后端api接口,不需要SSR,不需要过于重或过于轻量的框架,因此最后挑选了nest.js。

nest.js

Nest (NestJS) 是一个用于构建高效、可扩展的Node.js服务器端应用程序的框架。它使用渐进式 JavaScript,构建并完全支持TypeScript(但仍然允许开发人员使用纯 JavaScript 进行编码)并结合了 OOP(面向对象编程)、FP(函数式编程)和 FRP(函数式响应式编程)的元素。

术语介绍:

  • 什么是渐进式?简单说,就一开始你不需要了解它的全部功能,能快速上手,有些功能特性不用也可以正常使用。
  • OOP 面向对象编程,万物皆可用对象来描述,如: class Dog{ say(return 'one one!')}
  • FP 函数式编程,以函数作为入口,而不是去声明一个对象类,如: say('one one!')
  • RP 响应式编程,一种面向数据流和变化传播的编程范式,如:a = 5; b=6; c=a+b; ab变化的时候,c会随之变化
  • FRP 函数式响应式编程,依赖数据流的函数式编程,如:str='one one~'; say(str), 当str变化,会自动触发say

基础概念

Nest.js的核心是基于IoC控制反转 + DI 依赖注入 去实现类的声明和实例化的。如果你了解过Spring Boot其实很容易上手nest.js。

Module

Module 其实是nest.js用来将一个web应用拆分成各个子模块的分类规定,web应用根模块一般叫app.module.ts,官方设计图如下:

Module应该由以下几个部分组成:

  • providers: 允许交给模块实例化的类,包括不限于Service等
  • controllers:必须实例的controller类
  • imports: 模块依赖其他模块
  • exports:模块对外提供的方法类

Controller

Controller就一个作用,分割路由,调用处理方法,返回http请求结果。

支持写法:

  • @Controller('test')
  • @Get() @Post() @Put() @Del()代表各种请求方法(http Method)
  • 还支持一些特殊写法: @Session() @Body(key?: string) @Param(key?: string)

Provider

Provider其实就是不仅仅是Service层,还包括:Sql的Dao层、工具方法等提供。它和其他层关系如下图:

写法:

  • @Injectable() 声明该类是一个Provider,允许其他类实现依赖注入
  • @Optional() 允许构造不传
  • @Inject() 自动依赖注入

Middleware

Middleware中间件,其实和egg.js的中间件概念一样,就是当http请求来了之后,被中间件处理一遍之后才会到对应的Controller层。

写法:

  • implements NestMiddleware,必须实现NestMiddleware接口,以及内部方法use(req: Request, res: Response, next: NextFunction),同时内部方法必须调用next
  • Module层注册中间件,这里需要可以设置
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats');
  }
}
  • 了解 MiddlewareConsumer,中间件消费者工具类,主要把中间件加上一些配置项功能,如:forRoutes支持路由匹配,exclude不包含路由
  • 函数式声明中间件,因为Middleware是基于expres,所以写法与express基本上一致
import { Request, Response, NextFunction } from 'express';

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`Request...`);
  next();
};

ExceptionFilter

Filter过滤器,这个应该是所有web框架都具备的功能,拦截用户请求和web返回数据。在Nest.js中,只实现ExeptionFilter,你也可以基于这个去自定义自己的异常过滤器,具体如下图:

写法:

  • @Catch(HttpException) class HttpExceptionFilter implements ExceptionFilter 实现自定义异常过滤器
  • @UseFilters(new HttpExceptionFilter()) 能给具体接口包裹上一层自定义的异常过滤器

Pipe

Pipe 管道流,是指的Http请求里的内容数据流,它支持数据验证、数据转换等功能,有点类似Filter的功能。

写法:

  • @Param('id', ParseIntPipe) id: number,将参数id转换为number类型
  • class ValidationPipe implements PipeTransform自定义 Pipe,同时必须实现方法transform(value: any, metadata: ArgumentMetadata)
  • @UsePipes(new Pipe()),支持在controller配置自定义的Pipe

Guard

Guard 守卫,也是处Http请求中的一层特殊中间件,但是与中间件不同的时候,中间件不知道next()是去哪个执行代码,而Guard则可以获取ExecutionContext实例,可以获知整个请求的生命周期和内置内容,通常用来接口登录和权限控制。

写法:

  • class AuthGuard implements CanActivate
  • 必须实现方法canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean>
  • 注册使用:@UseGuards Controller层使用, app.useGlobalGuards(new RolesGuard()); 全局注册

Interceptor

Interceptor是面向切面编程理念影响的概念,它允许你在方法执行前后扩展原有函数功能,如:改变返回结果,扩展基本功能等,常用的场景:添加常规日志。

写法:

  • class LoggingInterceptor implements NestInterceptor,自定义实现
  • intercept(context: ExecutionContext, next: CallHandler): Observable<any>方法实现,同时需要返回对应结果next()
  • 注册使用:@UseInterceptors(LoggingInterceptor)可以在类或方法前进行注册, app.useGlobalInterceptors(new LoggingInterceptor());全局注册

其他

  • 自定义参数装饰器createParamDecorator,可以从request对象中抽取固定的参数。
  • applyDecorators 可以将多个装饰器 方法合在一起验证,然后形成一个新的注装饰器

PS: 装饰器是什么?

  • 装饰器在JavaScript中暂时是没有,只有TypeScript才可以实现一种语法糖
  • 装饰器使用 @expression 的形式,其中 expression 必须能够演算为在运行时调用的函数,其中包括装饰声明信息。
  • 自定义装饰器代码如下:
// 这是一个装饰器工厂——有助于将用户参数传给装饰器声明
function f() {
  console.log("f(): evaluated");
  return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("f(): called");
  }
}

function g() {
  console.log("g(): evaluated");
  return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("g(): called");
  }
}

class C {
  @f()
  @g()
  method() {}
}

// f(): evaluated
// g(): evaluated
// g(): called
// f(): called

生命周期

Nest.js的生命周期分为三个阶段:初始化、运行和终止,下图详细生命周期的各个子阶段:

允许监听的生命周期函数:

  • onModuleInit(), 模块初始化时候调用
  • onApplicationBootstrap(),所有模块都准备好了,但是在web应用正式启用前会被调用
  • onModuleDestroy(),模块准备被停止
  • beforeApplicationShutdown(), web应用准备被停止之前
  • onApplicationShutdown(),web应用被停止之后,在进程退出之前

上手实战

第一步安装:

$ npm i -g @nestjs/cli
$ nest new project-name --strict

生成项目结构

src
|-- app.controller.spec.ts // controller层的单元测试
|-- app.controller.ts // controller层 控制路由接口层
|-- app.module.ts // 应用根模块
|-- app.service.ts // service层 给controller提供各种业务处理方法
|-- main.ts // 入口文件

运行

$ yarn
$ yarn start:dev

打开 http://localhost:3000 就可以访问了。

后面有篇文章叫《从nest.js中了解Ioc和DI》,大家想了解可以去看看。

个人博客:qborfy.com