likes
comments
collection
share

项目重构,从零开始搭建一套新的后台管理系统(后端版)

作者站长头像
站长
· 阅读数 72

背景介绍

这篇文章是之前写的文章的补充,由于之前写的文章有人评论说没有鉴权、动态路由、权限控制等功能,但是这些功能都是需要后台来配合的,这其中牵扯很多:

  1. 鉴权:也就是登录,需要单独写一篇文章来说;
  2. 动态路由:也就是根据用户的角色来动态生成路由,这个也需要单独写一篇文章来说;
  3. 权限控制:也就是根据用户的角色权限来控制页面功能,这个也需要单独写一篇文章来说;
  4. 项目架构:也需要单独拿出来介绍;
  5. 项目内部的细节实现:也需要单独拿出来介绍。

所以之前的文章只有前端相关的内容,任何涉及后端的内容都没有,其中包括登录页面的相关的功能都没有写;

写的都是架构相关的,例如技术栈、项目结构、页面布局、代码规范等等;

因为没有牵扯到后端,所以至少也是必须的网络请求封装没有写,路由跳转拦截控制没有写,状态管理控制也没有写,因为这些都是需要数据来支持的,也就是后端相关的内容;

所以这篇文章就是之前文章的补充,主要是后端相关的内容,但是这其中还得牵扯到前端的内容,前端是后端数据的消费者,所以这些内容必须配合的来写;

技术选型

这一篇是后端的内容,所以主要是后端的技术选型,前端的技术选型在之前的文章中已经介绍过了,这里就不再赘述了;

  • 后端技术栈

    • 语言: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为三层架构,分别为daomodelsrouters

commonutils为全局公共文件,他们的作用差不多,可以合并到一起,也可以根据自己的喜好来。

公司要求变量名全都是小写,单词之间用下划线分割,例如: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);

};

这里导出的是一个方法,这个方法接收一个参数,就是expressapp对象,然后在这个方法中进行路由的注册。

全局返回

全局返回是为了统一返回格式,我这里在utils文件夹下有一个result文件夹,这里面的文件就是用来统一返回格式的。

  • code_enum.js:这个文件是用来定义返回码的,这里面的返回码是根据业务来定义的,这里面的返回码是我自己定义的,大家可以根据自己的业务来定义返回码;
  • result.js:这个文件是用来定义返回格式的,这里面的返回格式是我自己定义的,大家可以根据自己的业务来定义返回格式;

code_enum.js文件代码如下,这个就是模仿TSenum的语法写的,其实完全没必要:

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的使用,随缘更新,不想等的可以直接看仓库,里面有完整的代码。

项目地址

gitee.com/zeng-h/cms

转载自:https://juejin.cn/post/7221057111091085371
评论
请登录