今年二月份的时候忙于夜大的毕业设计,做了一个密钥管理生成的小项目。
当时的前端使用vue-cli + vue3实现,后端当时出于快速实现功能,用Egg.js开发了出来,最近出于学习的目的,学习了一下Nest.js,然后基于Nest.js我把之前做的后端的主要功能又用Nest.js实现了一遍。于是借着学习的契机回顾了一下Egg.js想写篇文章谈一下自己对这两个框架额使用感受。
主要实现思路
Egg.js基于Koa做的二次开发,Nest.js我用的是基于Express实现的版本,这二者的具体实现上还是有些差异,但整体上都是基于Controller - Service的实现思路。
可以先看看Egg.jsd的目录结构
Egg.js的实现思路较为简单,后端的所有功能最后都会绑定在全局变量app上,app就像一个巨大的容器,它包含了路由,控制器,服务,扩展,中间件等等,所有的东西都是全局的,router.js则充当了项目的主入口文件,在这里可以自行配置最终给前端调用的接口。
1 | javascript复制代码// router.js |
这种模式确实会让开发变得简单,比如我需要获取请求头的token可以直接在app的全局变量中获取,但是这也会导致app的功能过于庞杂有一点臃肿。
Nest.js的目录结构大概是这样的
相比之下,Nest.js的结构可以说更清晰一些,main.ts作为项目的入口文件,而Nest比Egg多一项的概念则是模块,即Module - Controller - Service的结构,app.module.ts导入并整合各个模块,最终将这个大模块用于main.ts中
1 | typescript复制代码// app.module.ts |
1 | typescript复制代码// main.ts |
相较而言,Nest的思路和组织更为清晰,Nest的思路其实就是万物皆模块,如果一个功能用一个模块不能实现,那就再一个模块,Nest的另一个思路是功能皆装饰,这个会在后面再说。Nest这样设计的好处很明显,代码更容易组织,缺点就是如果控制不好会写的过于零散,除非自己设置,很多东西也不是全局的,比如token,我需要在每一个具体的请求中从具体的请求头获取,而Egg就可以从全局app中获取,这个见仁见智吧。
具体请求的编写
我以一个POST的请求为例,使用Egg的话请求接口的具体地址是在router.js中编写实现,同时,该请求需要token权限才能访问,那么Egg的思路是将token作为中间件,作为参数传到router.js中具体的post的请求上
1 | javascript复制代码// router.js |
然后,路由函数会调用controller对应的方法,controller会再从对应的service中找到对应的方法执行并返回结果
1 | javascript复制代码// controller/users.js |
1 | javascript复制代码// service/users.js |
正如我之前所说,Nest是万物皆模块,所以Nest要想实现相应功能,需要先定义好users的模块,再将users的controller和service用于模块里并导出
1 | typescript复制代码// users/users.module.ts |
那么Nest怎样实现同样地址的路由,这则需要在controller中实现,不同于Egg的路由有一个固定的方法,Nest引入了装饰器的思想,Controller作为路由的主入口代表第一级,而具体的Post/Get装饰器中定义的字符串为第二级。而关于权限的拦截也是基于装饰器的思想,使用UseGuards装饰器在具体路由的上方填入token进行拦截处理。
1 | typescript复制代码// users/users.controller.ts |
我们可以看到这里Egg和Nest的思想不同处,Egg基于全局通过中间件对事物进行处理,Nest基于具体模块通过装饰器对事物进行处理
其他异同
数据的返回问题
通常情况下,我们会对数据库查询的内容包装一层再返回给前端,这里Egg和Nest的处理思路有很大不同,Egg在controller通过ctx.result返回,如果在service中强行赋值一个自己给的错误码则可以修改数据返回的状态码,如之前举例中的users.service错误码赋值500是可以返回回来的,平常情况下则可以自定义一个基础的BaseController返回包装的数据结构。
1 | javascript复制代码// controller/base.js |
Nest修改状态码则不够灵活,只能通过装饰器@HttpStatus修改,但是在一些全局处理上这样修改很复杂,比如token失效我希望状态码变成401这样就很难,我看了文档也没找到合适的办法,最终只能采取折中方案,在自定义的返回中写入一个状态码,前端以这个自定义的状态码为准进行判断,实际上之前我也是这么写的。
token的生成
token这里Egg和Nest的思路也是不一样的,Egg有专门的生成token的插件,利用插件生成token后在全局使用,生成之后可以使用一个中间件作为拦截,判断当token失效以后返回401
1 | javascript复制代码// middleware/jwt.js |
Nest则已经秉持模块的思想,可以定一个名为auth的模块,叫其他名字也行,在该文件夹下使用官方的方法生成token并引入到auth的service中使用,最后将token作为拦截器在具体的接口之上用拦截装饰器导入
1 | typescript复制代码// auth/strategies/jwt.strategy.ts |
那要怎样在token失效后返回错误消息退出呢?我目前做到的也就是我之前提到的定义一个全局拦截器,在失效之后返回自定义的401错误码交由前端处理。
1 | typescript复制代码// /common/interceptors/transform.interceptor.ts |
目前有一个问题我不知道怎么处理,就是Egg生成的token,前端请求时Authrotion不需要携带”Bearer”,但是Nest生成的token前端必须要携带”Bearer”。这也是前端部分我唯一修改的一处代码,我查了很多资料,目前没想到合适的办法让Nest的请求和Egg保持一致。
1 | javascript复制代码// 前端 /api/axios.js |
关于数据库
数据库我使用MySql,Egg操作数据库非常简单,Egg本身就可以找到mysql的数据库插件,大部分的查询和新增操作利用插件功能可以完成,小部分的查询编写sql可以实现,例如keys.js下的审核和查询等
1 | javascript复制代码// service/keys.js |
但是Nest的数据库操作就稍微复杂了一点,Nest没有有个比较官方的方案,在Mysql上目前可以配合官方插件使用第三方库TypeORM或者Sequelize操作数据库,我这里使用的是Sequelize,但不管使用哪个库,nest都不能直接查询或修改数据,而是要为每一个表建立相应的数据模型,再在不同的模块service下通过模型操作数据。比如,首先我们先定义用户数据表的模型
1 | typescript复制代码// common/models/users.model.ts |
然后在需要使用模型的module和service里分别引入,module需要在impots里声明使用了该模型
1 | typescript复制代码import { Module } from '@nestjs/common'; |
在对应的servcice里再次引入模型,并在constructor定义模型变量并具体使用
1 | typescript复制代码import { Injectable } from '@nestjs/common'; |
也就是说,如果想对数据库的具体数据进行操作,前提是必须要事先实现对应的数据模型,这一点倒是比Egg相对严谨一些。
小结
以上就是我对Egg和Nest分别开发相同项目的一些比较感受,在我看来Egg的思想是便捷,一个像我这样对后端不怎么了解的开发者只要按照文档,使用对应的插件就可以开发出一个还不错的后端项目。而Nest则更加强调规范,严格的三层架构,基于模块的思想和装饰器的广泛使用,确保了开发者能编写出通用的代码,但是对于开发者对项目的组织能力也有一定的要求。如果是一个长期维护的后端项目,显然当下比较流行的Nest.js还是更胜一筹的。
本文转载自: 掘金