Egg.js基于Koa做的二次开发,Nest.js我用的是基于Express实现的版本,这二者的具体实现上还是有些差异,但整体上都是基于Controller-Service的实现思路。可以先看看Egg.jsd的目录结构
Egg.js的实现思路较为简单,后端的所有功能最后都会绑定在全局变量app上,app就像一个巨大的容器,它包含了路由,控制器,服务,扩展,中间件等等,所有的东西都是全局的,router.js则充当了项目的主入口文件,在这里可以自行配置最终给前端调用的接口。
Nest.js的目录结构大概是这样的
相比之下,Nest.js的结构可以说更清晰一些,main.ts作为项目的入口文件,而Nest比Egg多一项的概念则是模块,即Module-Controller-Service的结构,app.module.ts导入并整合各个模块,最终将这个大模块用于main.ts中
我以一个POST的请求为例,使用Egg的话请求接口的具体地址是在router.js中编写实现,同时,该请求需要token权限才能访问,那么Egg的思路是将token作为中间件,作为参数传到router.js中具体的post的请求上
//router.js//在全局变量中获取中间件和配置项const{router,controller,middleware,config}=app;//生成中间件tokenconstjwt=middleware.jwt(config.jwt);...//在具体的请求中添加token中间件router.post(`${baseURL}/users/add`,jwt,controller.users.create);然后,路由函数会调用controller对应的方法,controller会再从对应的service中找到对应的方法执行并返回结果
//users/users.module.ts...@Module({imports:[SequelizeModule.forFeature([Users,Roles,Usertokens]),MailModule,OperalogModule],providers:[UsersService],controllers:[UsersController]})exportclassUsersModule{}那么Nest怎样实现同样地址的路由,这则需要在controller中实现,不同于Egg的路由有一个固定的方法,Nest引入了装饰器的思想,Controller作为路由的主入口代表第一级,而具体的Post/Get装饰器中定义的字符串为第二级。而关于权限的拦截也是基于装饰器的思想,使用UseGuards装饰器在具体路由的上方填入token进行拦截处理。
通常情况下,我们会对数据库查询的内容包装一层再返回给前端,这里Egg和Nest的处理思路有很大不同,Egg在controller通过ctx.result返回,如果在service中强行赋值一个自己给的错误码则可以修改数据返回的状态码,如之前举例中的users.service错误码赋值500是可以返回回来的,平常情况下则可以自定义一个基础的BaseController返回包装的数据结构。
//controller/base.jsconstController=require('egg').Controller;classBaseControllerextendsController{result(data,code=200,isSuccess=true,message='操作成功'){this.ctx.body={code:code,success:isSuccess,message:message,data}}}module.exports=BaseController;Nest修改状态码则不够灵活,只能通过装饰器@HttpStatus修改,但是在一些全局处理上这样修改很复杂,比如token失效我希望状态码变成401这样就很难,我看了文档也没找到合适的办法,最终只能采取折中方案,在自定义的返回中写入一个状态码,前端以这个自定义的状态码为准进行判断,实际上之前我也是这么写的。
token这里Egg和Nest的思路也是不一样的,Egg有专门的生成token的插件,利用插件生成token后在全局使用,生成之后可以使用一个中间件作为拦截,判断当token失效以后返回401
//auth/strategies/jwt.strategy.tsimport{Injectable}from'@nestjs/common';import{PassportStrategy}from'@nestjs/passport';import{ExtractJwt,Strategy}from'passport-jwt';import{secret}from'src/common/conmstr';@Injectable()exportclassJwtStrategyextendsPassportStrategy(Strategy){constructor(){super({jwtFromRequest:ExtractJwt.fromAuthHeaderAsBearerToken(),ignoreExpiration:false,secretOrKey:secret,});}asyncvalidate(payload:any){return{token:payload.token}}}那要怎样在token失效后返回错误消息退出呢?我目前做到的也就是我之前提到的定义一个全局拦截器,在失效之后返回自定义的401错误码交由前端处理。
//前端/api/axios.js...//全局拦截添加token(Egg.js不需要Bearer,Nest.js需要Bearer,我目前不知道怎样在后端解决这个问题)if(store.state.token!=""){config.headers.common['Authorization']='Bearer'+store.state.token//config.headers.common["Authorization"]=store.state.token;}...关于数据库数据库我使用MySql,Egg操作数据库非常简单,Egg本身就可以找到mysql的数据库插件,大部分的查询和新增操作利用插件功能可以完成,小部分的查询编写sql可以实现,例如keys.js下的审核和查询等
//service/keys.js...//审核asyncaudit(params){const{app}=this;letsubData={id:params.id,status:params.result,reason:params.reason,auditDate:app.mysql.literals.now}constresult=awaitthis.app.mysql.update('theKeys',subData);//利用Egg自带的插件更新if(result.affectedRows===1){return{result};}else{return{code:500,success:false,message:'更新失败'}}}//用户可操作性密钥asyncuserKeys(params){constuser=awaitthis.app.mysql.get('users',{loginName:params.loginName});//console.log('user:',user);letsql=`selectid,keyNamefromtheKeyswherekeyUserlike"%${user.id}%"andstatus=2`;constlist=awaitthis.app.mysql.query(sql);returnlist;}但是Nest的数据库操作就稍微复杂了一点,Nest没有有个比较官方的方案,在Mysql上目前可以配合官方插件使用第三方库TypeORM或者Sequelize操作数据库,我这里使用的是Sequelize,但不管使用哪个库,nest都不能直接查询或修改数据,而是要为每一个表建立相应的数据模型,再在不同的模块service下通过模型操作数据。比如,首先我们先定义用户数据表的模型
import{Module}from'@nestjs/common';import{SequelizeModule}from'@nestjs/sequelize';...@Module({imports:[SequelizeModule.forFeature([Users]),//表示使用了Users模型...],...})exportclassUsersModule{}在对应的servcice里再次引入模型,并在constructor定义模型变量并具体使用
以上就是我对Egg和Nest分别开发相同项目的一些比较感受,在我看来Egg的思想是便捷,一个像我这样对后端不怎么了解的开发者只要按照文档,使用对应的插件就可以开发出一个还不错的后端项目。而Nest则更加强调规范,严格的三层架构,基于模块的思想和装饰器的广泛使用,确保了开发者能编写出通用的代码,但是对于开发者对项目的组织能力也有一定的要求。如果是一个长期维护的后端项目,显然当下比较流行的Nest.js还是更胜一筹的。