背景介绍
这篇文章是之前写的文章的补充,由于之前写的文章有人评论说没有鉴权、动态路由、权限控制等功能,但是这些功能都是需要后台来配合的,这其中牵扯很多:
- 鉴权:也就是登录,需要单独写一篇文章来说;
- 动态路由:也就是根据用户的角色来动态生成路由,这个也需要单独写一篇文章来说;
- 权限控制:也就是根据用户的角色权限来控制页面功能,这个也需要单独写一篇文章来说;
- 项目架构:也需要单独拿出来介绍;
- 项目内部的细节实现:也需要单独拿出来介绍。
所以之前的文章只有前端相关的内容,任何涉及后端的内容都没有,其中包括登录页面的相关的功能都没有写;
写的都是架构相关的,例如技术栈、项目结构、页面布局、代码规范等等;
因为没有牵扯到后端,所以至少也是必须的网络请求封装没有写,路由跳转拦截控制没有写,状态管理控制也没有写,因为这些都是需要数据来支持的,也就是后端相关的内容;
所以这篇文章就是之前文章的补充,主要是后端相关的内容,但是这其中还得牵扯到前端的内容,前端是后端数据的消费者,所以这些内容必须配合的来写;
技术选型
这一篇是后端的内容,所以主要是后端的技术选型,前端的技术选型在之前的文章中已经介绍过了,这里就不再赘述了;
- 后端技术栈
- 语言: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
的使用,随缘更新,不想等的可以直接看仓库,里面有完整的代码。