项目重构,从零开始搭建一套新的后台管理系统(后端版)
背景介绍
这篇文章是之前写的文章的补充,由于之前写的文章有人评论说没有鉴权、动态路由、权限控制等功能,但是这些功能都是需要后台来配合的,这其中牵扯很多:
- 鉴权:也就是登录,需要单独写一篇文章来说;
- 动态路由:也就是根据用户的角色来动态生成路由,这个也需要单独写一篇文章来说;
- 权限控制:也就是根据用户的角色权限来控制页面功能,这个也需要单独写一篇文章来说;
- 项目架构:也需要单独拿出来介绍;
- 项目内部的细节实现:也需要单独拿出来介绍。
所以之前的文章只有前端相关的内容,任何涉及后端的内容都没有,其中包括登录页面的相关的功能都没有写;
写的都是架构相关的,例如技术栈、项目结构、页面布局、代码规范等等;
因为没有牵扯到后端,所以至少也是必须的网络请求封装没有写,路由跳转拦截控制没有写,状态管理控制也没有写,因为这些都是需要数据来支持的,也就是后端相关的内容;
所以这篇文章就是之前文章的补充,主要是后端相关的内容,但是这其中还得牵扯到前端的内容,前端是后端数据的消费者,所以这些内容必须配合的来写;
技术选型
这一篇是后端的内容,所以主要是后端的技术选型,前端的技术选型在之前的文章中已经介绍过了,这里就不再赘述了;
-
后端技术栈
- 语言:nodejs
- 框架:express
- 数据库:postgresql
- 鉴权:JWT
比较简单,没有上什么花里胡哨的技术,本来就是重构,并不是炫技。
目录结构
├── api # 三层架构
│ ├── dao # 数据访问层
│ │ └── system # 系统管理
│ │ └── user.js # 用户管理
│ ├── models # 数据模型
│ │ ├── system # 系统管理
│ │ │ └── user.js # 用户管理
│ │ └── associate.js # 统一维护关联关系
│ └── routers # 路由
│ ├── system # 系统管理
│ │ └── user.js # 用户管理
│ └── index.js # 路由统筹注册文件
├── assets # 静态资源
├── common # 全局公共文件
├── public # 公开资源
├── utils # 工具函数
│ ├── jwt # jwt
│ │ ├── jwt_middleware.js # jwt 中间件
│ │ └── jwt_store.js # jwt 存储
│ └── result # 全局返回结果
│ ├── code_enum.js # 全局返回枚举
│ └── index.js # 全局返回结果
├── .gitignore # git忽略文件
├── app.js # 入口文件
├── package.json # 项目依赖
├── README.md # 项目介绍
└── webpack.config.js # webpack配置文件
命名规范
后台管理API
为三层架构,分别为dao
、models
、routers
。
common
和utils
为全局公共文件,他们的作用差不多,可以合并到一起,也可以根据自己的喜好来。
公司要求变量名全都是小写,单词之间用下划线分割,例如:user_name
。
其他的命名规范,例如文件夹、文件名等,都是根据功能模块来命名的,例如:system
(系统管理功能模块)。
dao
dao
层为数据访问层,主要负责数据库的增删改查操作,以及一些复杂的业务逻辑。
models
models
层为数据模型层,主要负责数据的校验,以及数据的转换。
routers
routers
层为路由层,主要负责路由的注册,以及路由的处理。
上面的三层架构,每一层根据功能模块进行划分,每一个功能模块都有一个对应的文件夹,文件夹下面有对应功能的文件。
例如上面的目录结构中:
dao
层 ->system
(系统管理功能模块)文件夹 ->user.js
(用户管理功能)文件models
层 ->system
(系统管理功能模块)文件夹 ->user.js
(用户管理功能)文件routers
层 ->system
(系统管理功能模块)文件夹 ->user.js
(用户管理功能)文件
禁止在router
层直接操作数据库,所有的数据库操作都在dao
层进行。
代码规范
这里没有安装eslint
,所以代码规范需要自己遵守。
数据库配置
公司要求使用的是postgresql
数据库,所以这里就使用postgresql
数据库,数据库连接使用sequelize
来连接。
sequelize
是一个ORM
框架,可以让我们不用写sql
语句,就可以操作数据库。
我这里是将数据库相关写到了common
文件夹下的sequelize
目录下,然后在app.js
中引入,然后在app.js
中进行初始化。
const {Sequelize} = require("sequelize");
const pg = require("pg");
const minimist = require("minimist");
const argv = minimist(process.argv.slice(2), {string: ["_"]});
let sequelize = {};
const _config = {
database: "db_cms",
username: "postgres",
password: "123456",
options: {
host: "127.0.0.1",
port: 5432,
dialect: "postgres", /* 选择 'mysql' | 'mariadb' | 'postgres' | 'mssql' 其一 */
logging: false,
define: {
freezeTableName: true,
createdAt: "created_time",
updatedAt: "updated_time",
timestamps: true,
},
// dialectOptions: {
// charset: 'utf8',
// dateStrings: true,
// typeCast: true
// },
// timezone: '+08:00'
}
};
const init_sequelize = (config) => {
config = config || _config;
sequelize = new Sequelize(
config.database,
config.username,
config.password,
{
...config.options,
dialectModule: pg
}
);
// 异步挂载关联关系
Promise.resolve().then(() => {
require("../../api/models/associate");
});
return sequelize;
};
init_sequelize();
if (argv.NODE_ENV && argv.NODE_ENV === "development") {
sequelize.sync().then(() => {
console.log("数据库初始化成功");
});
}
module.exports = {
init_sequelize,
sequelize
};
这个代码就不需要解释了吧,就是初始化数据库连接,看不明白的可以去看看sequelize
的文档:sequelize
sequelize
有一个很棒的特性就是可自定义实体,也就是模型,这一块上面的目录结构中的api/models
文件夹就是用来存放模型的。
并且可以对模型(表空间)创建关联关系,这一块上面的目录结构中的api/models/associate.js
文件就是用来维护关联关系的。
上面导出了init_sequelize
方法,但是其实可以不需要导出,因为在这个里面已经自执行了,因为我是准备做多数据源切换的,所以会导出用于重置数据库连接,当然示例代码将这一块移除了,因为这一块不是重点;
数据库的模型啥的都不用说了,就是定义模型,然后定义关联关系,这里就不贴代码了,大家可以自己去看看,文末会有仓库地址。
路由配置
项目使用的技术栈是express
,所以路由配置也是使用express
的路由配置;
express
也就不多介绍了吧,看的懂的不用说,看不懂的不是一两句话能说清楚的,文档地址:express;
路由配置在api/routers
文件夹下,当我们写完路由相关代码之后,都需要导出然后在app.js
中引入,然后在app.js
中进行注册。
我这里把注册的一步全都放在了api/routers/index.js
文件中,然后在app.js
中引入,然后在app.js
中进行注册。
// 系统管理
const user = require("./system/user");
const role = require("./system/role");
const menu = require("./system/menu");
const dict = require("./system/dict");
module.exports = function (app) {
// 系统管理
app.use("/api/user", user);
app.use("/api/role", role);
app.use("/api/menu", menu);
app.use("/api/dict", dict);
};
这里导出的是一个方法,这个方法接收一个参数,就是express
的app
对象,然后在这个方法中进行路由的注册。
全局返回
全局返回是为了统一返回格式,我这里在utils
文件夹下有一个result
文件夹,这里面的文件就是用来统一返回格式的。
code_enum.js
:这个文件是用来定义返回码的,这里面的返回码是根据业务来定义的,这里面的返回码是我自己定义的,大家可以根据自己的业务来定义返回码;result.js
:这个文件是用来定义返回格式的,这里面的返回格式是我自己定义的,大家可以根据自己的业务来定义返回格式;
code_enum.js
文件代码如下,这个就是模仿TS
的enum
的语法写的,其实完全没必要:
const code_enum = {
SUCCESS: 0, // 成功
0: "SUCCESS",
ERROR: 500, // 通用错误
500: "ERROR",
ERROR_PARAM: 501, // 参数错误
501: "ERROR_PARAM",
ERROR_DB: 502, // 数据库错误
502: "ERROR_DB",
ERROR_AUTH: 503, // 权限错误
503: "ERROR_AUTH",
ERROR_TOKEN: 504, // token错误
504: "ERROR_TOKEN",
ERROR_FILE: 505, // 文件错误
505: "ERROR_FILE",
ERROR_UNKNOWN: 506, // 未知错误
506: "ERROR_UNKNOWN",
ERROR_NOT_FOUND: 507, // 未找到
507: "ERROR_NOT_FOUND",
ERROR_NOT_SUPPORT: 508, // 不支持
508: "ERROR_NOT_SUPPORT",
ERROR_NOT_LOGIN: 509, // 未登录
509: "ERROR_NOT_LOGIN",
ERROR_NOT_PERMISSION: 510, // 无权限
510: "ERROR_NOT_PERMISSION",
ERROR_NOT_EXIST: 511, // 不存在
511: "ERROR_NOT_EXIST",
ERROR_EXIST: 512, // 已存在
512: "ERROR_EXIST",
ERROR_NOT_ALLOW: 513, // 不允许
513: "ERROR_NOT_ALLOW",
ERROR_NOT_VALID: 514, // 无效
514: "ERROR_NOT_VALID",
ERROR_NOT_MATCH: 515, // 不匹配
515: "ERROR_NOT_MATCH",
ERROR_NOT_ENOUGH: 516, // 不足
516: "ERROR_NOT_ENOUGH",
};
const message = {
SUCCESS: "成功",
0: "成功",
ERROR: "服务器内部错误",
500: "服务器内部错误",
ERROR_PARAM: "参数错误",
501: "参数错误",
ERROR_DB: "数据库错误",
502: "数据库错误",
ERROR_AUTH: "权限错误",
503: "权限错误",
ERROR_TOKEN: "token错误",
504: "token错误",
ERROR_FILE: "文件错误",
505: "文件错误",
ERROR_UNKNOWN: "未知错误",
506: "未知错误",
ERROR_NOT_FOUND: "未找到",
507: "未找到",
ERROR_NOT_SUPPORT: "不支持",
508: "不支持",
ERROR_NOT_LOGIN: "未登录",
509: "未登录",
ERROR_NOT_PERMISSION: "无权限",
510: "无权限",
ERROR_NOT_EXIST: "不存在",
511: "不存在",
ERROR_EXIST: "已存在",
512: "已存在",
ERROR_NOT_ALLOW: "不允许",
513: "不允许",
ERROR_NOT_VALID: "无效",
514: "无效",
ERROR_NOT_MATCH: "不匹配",
515: "不匹配",
ERROR_NOT_ENOUGH: "不足",
516: "不足",
};
code_enum.getMsg = function (code) {
return message[code] || message[Code_enum.ERROR_NOT_FOUND];
};
module.exports = code_enum;
result.js
文件代码如下:
class Result {
/**
* 构造函数
* 重载:
* 1. new Result(code, message, data)
* 2. new Result(code, data)
*
* @param {number|string} code
* @param {string|object?} message
* @param {any?} data
*/
constructor(code, message, data) {
if (code == null) {
throw new Error("code is null");
}
if (typeof message === "object") {
const temp = data;
data = message;
if (typeof data === "string") {
message = temp;
} else {
message = null;
}
}
if (message == null) {
message = code_enum.getMsg(code);
}
this.code = code;
this.message = message;
this.data = data;
if (this.code === code_enum.ERROR) {
console.error(this.message, this.data);
}
}
/**
* 静态函数,成功返回
* 重载:
* 1. success(data)
* 2. success(data, message)
* 3. success(message, data)
*
* @param {string|object} message 消息内容
* @param {any?} data 数据
* @return {Result}
*/
static success(message, data) {
if (typeof message === "object") {
if (typeof message === "object") {
const temp = data;
data = message;
if (typeof data === "string") {
message = temp;
} else {
message = null;
}
}
}
if (message == null) {
message = code_enum.getMsg(code_enum.SUCCESS);
}
return new Result(code_enum.SUCCESS, message, data);
}
/**
* 静态函数,失败返回
* 重载:
* 1. fail(data)
* 2. fail(data, message)
* 3. fail(message, data)
*
* @param {string|object} message 消息内容
* @param {any?} data 数据
* @return {Result}
*/
static fail(message, data) {
if (typeof message === "object") {
if (typeof message === "object") {
const temp = data;
data = message;
if (typeof data === "string") {
message = temp;
} else {
message = null;
}
}
}
if (message == null) {
message = code_enum.getMsg(code_enum.ERROR);
}
return new Result(code_enum.ERROR, message, data);
}
send(res) {
res.send(this);
}
}
module.exports = Result;
这两个文件都直接挂在全局下,不需要导出,直接使用即可。
global.Result = Result;
global.code_enum = code_enum;
// 使用
Result.success("成功", {name: "张三", age: 18}).send(res);
Result.success({name: "张三", age: 18}).send(res);
Result.fail("失败", {name: "张三", age: 18}).send(res);
Result.fail({name: "张三", age: 18}).send(res);
new Result(code_enum.SUCCESS, "成功", {name: "张三", age: 18}).send(res);
这样我们在路由中就可以直接使用了。
router.get("/test", (req, res) => {
db.action().then(data => {
Result.success(data).send(res);
}).catch(err => {
Result.fail(err).send(res);
})
});
app.js
这次就写这么多,有了这个框架之后,我们才能进行后续的开发,然后附上app.js
文件代码。
const express = require("express");
const http = require("http");
const Result = require("./utils/result");
const code_enum = require("./utils/result/code_enum");
const socket_io = require("./common/socket.io");
// 定义全局变量
global.Result = Result;
global.code_enum = code_enum;
global.SECRET = "xxx";
global.jwt_header_key = "token";
const app = express();
// 防止 content-encoding 请求头错误报错
app.use((req, res, next) => {
if (req.headers["content-encoding"] === "utf-8") {
delete req.headers["content-encoding"];
}
next();
// express.json({limit: "50mb"})(req, res, next);
});
// 解析 application/json
app.use(express.json());
// 解析 urlencoded
app.use(express.urlencoded({extended: false}));
// jwt 中间件拦截器
const {
unless,
jwt_error_handler,
jwt_renew
} = require("./utils/jwt/jwt_middleware");
app.use('/api', unless, jwt_error_handler);
// token 续期
app.use(jwt_renew);
// 公共资源访问
app.use("/public", express.static(__dirname + "/public"));
// 路由注册
const router = require("./api/routes");
router(app);
// 全局错误处理
process.on("uncaughtException", (e) => {
console.error(e); // Error: uncaughtException
// do something: 释放相关资源(例如文件描述符、句柄等)
// process.exit(1); // 手动退出进程
});
const server = http.createServer(app);
socket_io(server);
// 启动服务
const port = process.env.PORT || 3000;
server.listen(port, () => {
console.log("http://localhost:" + port);
});
这里会有很多上面没有提到的代码,这些代码后续再说,这里只是为了让大家知道,这个框架是怎么写的。
下一篇就开始写登录相关的代码,也就是jwt
的使用,随缘更新,不想等的可以直接看仓库,里面有完整的代码。
项目地址
转载自:https://juejin.cn/post/7221057111091085371