今年二月份的时候忙于夜大的毕业设计,做了一个密钥管理生成的小项目。
当时的前端使用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则充当了项目的主入口文件,在这里可以自行配置最终给前端调用的接口。
// router.js
const baseURL = '/api'
module.exports = app => {
const {
router,
controller,
middleware,
config
} = app;
const jwt = middleware.jwt(config.jwt);
router.get('/', controller.home.index);
// 一些查询
router.get(`${baseURL}/dict/roles`, controller.home.roles);
...
// 登录
router.post(`${baseURL}/login`, controller.login.login);
// 首页
router.get(`${baseURL}/dashboard/userroles`, jwt, controller.dashboard.userroles);
...
这种模式确实会让开发变得简单,比如我需要获取请求头的token可以直接在app的全局变量中获取,但是这也会导致app的功能过于庞杂有一点臃肿。
Nest.js的目录结构大概是这样的
相比之下,Nest.js的结构可以说更清晰一些,main.ts作为项目的入口文件,而Nest比Egg多一项的概念则是模块,即Module – Controller – Service的结构,app.module.ts导入并整合各个模块,最终将这个大模块用于main.ts中
// app.module.ts
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MailerModule } from '@nestjs-modules/mailer';
import { PugAdapter } from '@nestjs-modules/mailer/dist/adapters/pug.adapter';
import { UsersModule } from './users/users.module';
...
@Module({
imports: [
SequelizeModule.forRoot({
dialect: 'mysql',
host: '127.0.0.1',
port: 3306,
username: 'root',
password: '1234567',
database: 'mykms',
autoLoadModels: true,
synchronize: true,
}),
MailerModule.forRoot({
transport: {
host: "smtp.qq.com",
port: "465",
auth: {
user: "1534739331@qq.com",
pass: "qaurwlgawczubace"
}
},
defaults: {
from: '陈晟 <1534739331@qq.com>',
},
}),
UsersModule,
DictModule,
LoginModule,
DashboardModule,
ScheduleModule.forRoot(),
...
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
// main.ts
import { NestFactory } from '@nestjs/core';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
import { AppModule } from './app.module';
import { AuthService } from 'src/auth/auth.service';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const refAuthService = app.get<AuthService>(AuthService);
app.useGlobalInterceptors(new TransformInterceptor(refAuthService));
app.useGlobalFilters(new HttpExceptionFilter(refAuthService));
app.setGlobalPrefix('api');
await app.listen(7001);
}
bootstrap();
相较而言,Nest的思路和组织更为清晰,Nest的思路其实就是万物皆模块,如果一个功能用一个模块不能实现,那就再一个模块,Nest的另一个思路是功能皆装饰,这个会在后面再说。Nest这样设计的好处很明显,代码更容易组织,缺点就是如果控制不好会写的过于零散,除非自己设置,很多东西也不是全局的,比如token,我需要在每一个具体的请求中从具体的请求头获取,而Egg就可以从全局app中获取,这个见仁见智吧。
具体请求的编写
我以一个POST的请求为例,使用Egg的话请求接口的具体地址是在router.js中编写实现,同时,该请求需要token权限才能访问,那么Egg的思路是将token作为中间件,作为参数传到router.js中具体的post的请求上
// router.js
// 在全局变量中获取中间件和配置项
const {
router,
controller,
middleware,
config
} = app;
// 生成中间件token
const jwt = middleware.jwt(config.jwt);
...
// 在具体的请求中添加token中间件
router.post(`${baseURL}/users/add`, jwt, controller.users.create);
然后,路由函数会调用controller对应的方法,controller会再从对应的service中找到对应的方法执行并返回结果
// controller/users.js
async create() {
const {
ctx,
app
} = this;
const body = ctx.request.body;
const result = await ctx.service.users.create(body);
if (!!result.code && result.code != 200) {
if (!body.id) {
// 插入操作文档
await this.ctx.service.home.addOperaLog({
url: '/app/users/add',
method: 'POST',
action: '新增',
status: 0
});
} else {
// 插入操作文档
await this.ctx.service.home.addOperaLog({
url: '/app/users/update',
method: 'PUT',
action: '修改',
status: 0
});
}
this.result(null, result.code, result.success, result.message);
} else {
// 发送邮件
const {
roles
} = await ctx.service.home.roles();
let title = body.id ? '修改' : '创建';
let rolesText = "";
let userRoles = body.roles;
userRoles.forEach(id => {
for (let i = 0; i < roles.length; i++) {
if (id == roles[i].code) {
rolesText += `${roles[i].text}, `;
break;
}
}
});
// console.log(body.email);
const mailresult = await app.email.sendEmail(
`账号${title}成功`,
`您的账号已${title}成功`,
`${body.email}`,
[{
data: `<html> <p>您的账户已${title}成功</p> <p>登录名:<b>${body.loginName}</b></p> <p>密码:<b>${body.password}</b></p> <p>角色:<b>${rolesText}</b></p> <p>如有疑问请联系管理员</p> </html>`,
alternative: true
}]
);
if (!body.id) {
// 插入操作文档
await this.ctx.service.home.addOperaLog({
url: '/app/users/add',
method: 'POST',
action: '新增',
status: 1
});
} else {
// 插入操作文档
await this.ctx.service.home.addOperaLog({
url: '/app/users/update',
method: 'PUT',
action: '修改',
status: 1
});
}
this.result(mailresult);
}
}
// service/users.js
// 插入/修改一条数据
async create(params) {
const users = await this.app.mysql.select('users');
let insertData = JSON.parse(JSON.stringify(params));
insertData.roles = insertData.roles.join(",");
insertData.hexPassword = getMd5Data(params.password);
if (!params.hasOwnProperty('id')) {
insertData.id = `user_${users.length}_${Math.random().toFixed(5)}`;
}
const findUsers = await this.app.mysql.select('users', {
where: {
loginName: params.loginName
}
});
// console.log('findUsers: ', findUsers);
if (params.hasOwnProperty('id')) {
// 更新操作
if (findUsers.length === 0 || (findUsers.length === 1 && findUsers[0].id === params.id)) {
const result = await this.app.mysql.update('users', insertData);
if (result.affectedRows === 1) {
return {
result
};
} else {
return {
code: 500,
success: false,
message: '更新失败'
}
}
} else {
return {
code: 4001,
success: false,
message: '登录名重复'
}
}
} else {
// 新增操作
if (findUsers.length > 0) {
return {
code: 4001,
success: false,
message: '登录名已存在'
}
} else {
const result = await this.app.mysql.insert('users', insertData);
if (result.protocol41) {
return {
result
};
} else {
return {
code: 500,
success: false,
message: '添加失败'
}
}
}
}
}
正如我之前所说,Nest是万物皆模块,所以Nest要想实现相应功能,需要先定义好users的模块,再将users的controller和service用于模块里并导出
// users/users.module.ts
...
@Module({
imports: [
SequelizeModule.forFeature([Users, Roles, Usertokens]),
MailModule,
OperalogModule
],
providers: [UsersService],
controllers: [UsersController]
})
export class UsersModule {}
那么Nest怎样实现同样地址的路由,这则需要在controller中实现,不同于Egg的路由有一个固定的方法,Nest引入了装饰器的思想,Controller作为路由的主入口代表第一级,而具体的Post/Get装饰器中定义的字符串为第二级。而关于权限的拦截也是基于装饰器的思想,使用UseGuards装饰器在具体路由的上方填入token进行拦截处理。
// users/users.controller.ts
import { Controller, Get, Post, Put, UseGuards, Request, Delete } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/guards/jwt.guard';
import { UsersService } from './users.service';
import { OperalogService } from 'src/operaLog/operaLog.service';
@Controller('users') // controller装饰器里的users是路由名称的第一级
export class UsersController {
constructor( private readonly usersService: UsersService, private readonly operalogService: OperalogService, ) {}
...
// 新增用户
@UseGuards(JwtAuthGuard) // 通过装饰器注入token拦截
@Post('add') // Post装饰器表示该请求为Post亲请求里面的add表示第二级也就是/users/add
async add(@Request() req): Promise<any> {
const result:any = await this.create(req);
if (result && result.code == 200) {
await this.addUserLog(1, req);
} else {
await this.addUserLog(0, req);
}
return result;
}
}
我们可以看到这里Egg和Nest的思想不同处,Egg基于全局通过中间件对事物进行处理,Nest基于具体模块通过装饰器对事物进行处理
其他异同
数据的返回问题
通常情况下,我们会对数据库查询的内容包装一层再返回给前端,这里Egg和Nest的处理思路有很大不同,Egg在controller通过ctx.result返回,如果在service中强行赋值一个自己给的错误码则可以修改数据返回的状态码,如之前举例中的users.service错误码赋值500是可以返回回来的,平常情况下则可以自定义一个基础的BaseController返回包装的数据结构。
// controller/base.js
const Controller = require('egg').Controller;
class BaseController extends Controller {
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的生成
token这里Egg和Nest的思路也是不一样的,Egg有专门的生成token的插件,利用插件生成token后在全局使用,生成之后可以使用一个中间件作为拦截,判断当token失效以后返回401
// middleware/jwt.js
module.exports = options => {
return async function jwt(ctx, next) {
const token = ctx.request.header.authorization;
let decode;
if (token) {
try {
// 解码token
decode = ctx.app.jwt.verify(token, options.secret);
const user = await ctx.app.mysql.get('usertokens', {
loginName: decode.loginName
});
if (token == user.token) {
await next();
} else {
ctx.status = 401;
ctx.body = {
message: '该账户已在其他设备登录',
};
return;
}
} catch (error) {
if (error.message == 'jwt expired') {
ctx.status = 401;
}
ctx.body = {
message: error.message,
};
return;
}
} else {
ctx.status = 401;
ctx.body = {
message: '没有token',
};
return;
}
};
};
Nest则已经秉持模块的思想,可以定一个名为auth的模块,叫其他名字也行,在该文件夹下使用官方的方法生成token并引入到auth的service中使用,最后将token作为拦截器在具体的接口之上用拦截装饰器导入
// auth/strategies/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { secret } from 'src/common/conmstr';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: secret,
});
}
async validate(payload: any) {
return { token: payload.token }
}
}
那要怎样在token失效后返回错误消息退出呢?我目前做到的也就是我之前提到的定义一个全局拦截器,在失效之后返回自定义的401错误码交由前端处理。
// /common/interceptors/transform.interceptor.ts
import {
Injectable,
NestInterceptor,
CallHandler,
ExecutionContext,
} from '@nestjs/common';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { AuthService } from 'src/auth/auth.service';
import { secret } from '../conmstr';
interface Response<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
constructor( private readonly authService: AuthService, ) {}
async intercept( context: ExecutionContext, next: CallHandler<T>): Promise<Observable<Response<T>>> {
const request = context.switchToHttp().getRequest();
const path = request.route.path;
let hasToken = true;
const mesData = {
code: 200,
message: path === '/api/login' ? '登录成功' : '请求成功',
success: true,
}
// 获取并解析token
if (request.headers.authorization) {
const usertokens = await this.authService.findUsertokens();
const token = (request.headers.authorization.split(' '))[1]; // request.headers.authorization;
const decode = this.authService.verifyToken(token, secret);
const user = usertokens.find((item) => { return item.loginName == decode.loginName });
if (user.token !== token) {
hasToken = false;
}
}
return next.handle().pipe(
map((data: any) => {
if (!hasToken && path != '/api/login') {
data = null;
mesData.message = '该账户已在其他设备登录';
mesData.code = 401;
mesData.success = false;
} else if (!hasToken && path === '/api/login') {
Object.keys(data).forEach(key => { mesData[key] = data[key] });
} else if (hasToken && data && data.code) {
Object.keys(data).forEach(key => { mesData[key] = data[key] });
} else if (!data) {
mesData.message = '请求失败';
mesData.code = 1;
mesData.success = false;
}
return {
data,
...mesData
};
}),
);
}
}
目前有一个问题我不知道怎么处理,就是Egg生成的token,前端请求时Authrotion不需要携带”Bearer”,但是Nest生成的token前端必须要携带”Bearer”。这也是前端部分我唯一修改的一处代码,我查了很多资料,目前没想到合适的办法让Nest的请求和Egg保持一致。
// 前端 /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
...
// 审核
async audit(params) {
const {
app
} = this;
let subData = {
id: params.id,
status: params.result,
reason: params.reason,
auditDate: app.mysql.literals.now
}
const result = await this.app.mysql.update('theKeys', subData); // 利用Egg自带的插件更新
if (result.affectedRows === 1) {
return {
result
};
} else {
return {
code: 500,
success: false,
message: '更新失败'
}
}
}
// 用户可操作性密钥
async userKeys(params) {
const user = await this.app.mysql.get('users', {
loginName: params.loginName
});
// console.log('user: ', user);
let sql = `select id,keyName from theKeys where keyUser like "%${user.id}%" and status=2`;
const list = await this.app.mysql.query(sql);
return list;
}
但是Nest的数据库操作就稍微复杂了一点,Nest没有有个比较官方的方案,在Mysql上目前可以配合官方插件使用第三方库TypeORM或者Sequelize操作数据库,我这里使用的是Sequelize,但不管使用哪个库,nest都不能直接查询或修改数据,而是要为每一个表建立相应的数据模型,再在不同的模块service下通过模型操作数据。比如,首先我们先定义用户数据表的模型
// common/models/users.model.ts
import { Column, Model, Table } from 'sequelize-typescript';
@Table({
tableName: 'users',
timestamps: false,
freezeTableName: true
})
export class Users extends Model<Users> {
@Column({ primaryKey: true })
id: string;
@Column
loginName: string;
@Column
userName: string;
@Column
password: string;
@Column
roles: string;
@Column
email: string;
@Column
hexPassword: string;
}
然后在需要使用模型的module和service里分别引入,module需要在impots里声明使用了该模型
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
...
@Module({
imports: [
SequelizeModule.forFeature([Users]), // 表示使用了Users模型
...
],
...
})
export class UsersModule {}
在对应的servcice里再次引入模型,并在constructor定义模型变量并具体使用
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { Sequelize } from 'sequelize-typescript';
import { Users } from 'src/common/models/users.model';
...
@Injectable()
export class UsersService {
constructor( ... @InjectModel(Users) private readonly usersModel: typeof Users, // 定义具体的使用变量 ) {}
...
// 修改密码
async editPassword(params) {
const user = await this.usersModel.findOne({ where: { id: params.userId } });
if (user.password !== params.oldPass) {
return {
code: 500,
success: false,
message: '旧密码有误'
}
} else {
let insertData = {
id: params.userId,
password: params.newPass,
hexPassword: getMd5Data(params.newPass)
}
const result = await this.usersModel.update(insertData, { where: {id: insertData.id} });
const userEditer = await this.usersModel.findOne({ where: { id: params.userId } });
console.log('userEditer: ', userEditer);
if (result && result[0] === 1) {
await this.sendMail(userEditer);
return {
code: 200,
success: true,
message: '密码修改成功'
}
} else {
return {
code: 500,
success: false,
message: '更新失败'
}
}
}
}
}
也就是说,如果想对数据库的具体数据进行操作,前提是必须要事先实现对应的数据模型,这一点倒是比Egg相对严谨一些。
小结
以上就是我对Egg和Nest分别开发相同项目的一些比较感受,在我看来Egg的思想是便捷,一个像我这样对后端不怎么了解的开发者只要按照文档,使用对应的插件就可以开发出一个还不错的后端项目。而Nest则更加强调规范,严格的三层架构,基于模块的思想和装饰器的广泛使用,确保了开发者能编写出通用的代码,但是对于开发者对项目的组织能力也有一定的要求。如果是一个长期维护的后端项目,显然当下比较流行的Nest.js还是更胜一筹的。
今天的文章Egg.js & Nest.js 简易上手比较分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/17222.html