从0到1搭建完整的后台管理系统(后端篇)

前言

通过一些脚手架,搭建一个前端项目慢慢地变得越来越容易。自己如何从前端到后端完整地做一个项目,是大家越来越关心的问题。

今天,我们就一步一步地来实现一个后端项目的搭建。

毕竟是以一个前端的角度来实现的,对后端研究得还不够深入。不到之处,还忘大佬们补充指正,先在此谢过。

技术选型

习惯写 javascript 的我们,一说到写后端的代码。当然首选 nodejs。这里我也不拐弯抹角。我们使用基于 koa 框架进行二次封装的 eggjs 来进行后端项目开发。eggjs 有以下优点:

  • 统一的开发规范。规范大于配置的理念使每个功能都有特定的规范,开发和修改都更加简单。
  • 系统稳定性。天然具备进行管理和监控功能。
  • 生态好。需要用到的插件及功能都可以搜索到。开箱即用。
  • 多环境配置。可以自定义不同的环境所需要用到的不同配置。
  • 插件开发方便。根据插件开发规范,可以很容易开发一个新的插件。

功能清单

一个后端项目需要具备哪些功能?我们平时在使用时,似乎只用到了向它发送一个 api 请求,服务器返回给我们对应的数据。

就这???

我们都知道。后端,肯定没这么简单!!!

整理一下我所理解的后端基础功能,有以下几点:

  • 操作数据库
  • 权限验证
  • 缓存
  • 请求限流
  • 安全防护
  • 日志记录
  • ……

梳理了需要具备的基本功能,接下来,我们就一步一步地来实现这些功能吧~~~

初始化项目

进入开发目录,创建一个项目文件夹。执行:

mkdir website-manage-api

进入新建的文件夹。执行:

cd website-manage-api

使用命令初始化一个简单的项目,执行:

yarn create egg –type=simple

安装依赖。执行:

yarn

从0到1搭建完整的后台管理系统(后端篇)

启动项目,执行:

yarn dev

初始化 git

在远端仓库下新增一个仓库,取名为:website-manage-api,创建好后,复制仓库地址

从0到1搭建完整的后台管理系统(后端篇)
初始化项目

PS: 这里的截图是用的另外一个项目的,方法是一样的,容许在这里偷个懒~~~

进入本地项目,初始化 git。执行:

git init

git remote add origin https://github.com/****/website-manage-api.git

这里的 https://github.com/****/website-manage-api.git 是自己新建的仓库地址。

git add .

git commit -m ‘feat: init’

git push –set-upstream origin master

再进入远端的仓库中,就有初始化的项目代码了。后续的操作可以随便修改,有修改错的地方,执行 git checkout $filepath/$filename 回退回来就行。

添加 egg-jwt

权限验证采用 jwt 技术。所以这里先安装一下 egg-jwt。执行:

yarn add egg-jwt

config/plugin.js 下面添加

exports.jwt = {
  enabletrue,
  package: 'egg-jwt',
};

config/config/default.js 中添加配置

config.jwt = {
    secret: 'website-manage-secret', // jwt 需要用到的密钥
};

添加 md5

用于对登录,注册时给密码进行加密处理。执行:

yarn add md5

添加 context 的扩展

常见的方法是从 contextrequest 中获取参数。从 token 中获取用户名和用户id。从 request 中获取参数需要区分请求方法是 GET 还是其它。因为从 GET 方法中请求数据是从 query 中获取。其它方式是从 body 中获取。每处在获取参数时都要写很长的链式调用,所以将它提取为一个 context 的扩展。在用的时候直接使用 ctx.method() 就行。

// extend/context.js
'use strict';

module.exports = {
  params(key) {
    const method = this.request.method;
    if (method === 'GET') {
      return key ? this.query[key] : this.query;
    }
    return key ? this.request.body[key] : this.request;
  },

  get username() {
    const token = this.request.header['auth-token'];
    const tokenCache = token ? this.app.jwt.verify(token, this.app.config.jwt.secret) : undefined;
    return tokenCache ? tokenCache.username : undefined;
  },

  get userId() {
    const token = this.request.header['auth-token'];
    const tokenCache = token ? this.app.jwt.verify(token, this.app.config.jwt.secret) : undefined;
    return tokenCache ? tokenCache.userId : undefined;
  },
};

如何自己写扩展,可以参考:https://eggjs.org/zh-cn/basics/extend.html

扩展 helper

常见的方法可以扩展在 helper 中,需要的时候直接 ctx.helper.method() 就可以调用。

// extend/helper.js
'use strict';

const dayjs = require('dayjs');

module.exports = {
  base64Encode(str = '') {
    return new Buffer(str).toString('base64');
  },
  time() {
    return dayjs().format('YYYY-MM-DD HH:mm:ss');
  },
  timestamp(data) {
    return new Date(data).getTime();
  },
  // 从一个对象中排除一些值
  unPick(source, arr) {
    if (Array.isArray(arr)) {
      const obj = {};
      for (const i in source) {
        if (!arr.includes(i)) {
          obj[i] = source[i];
        }
      }
      return obj;
    }
  },
};

这里面在处理时间时,有使用到 dayjs,所以这里需要执行 yarn add dayjs

扩展 request

每次请求时,一些请求参数都会带在 request 中。比如需要获取请求头是否有带 auth-token 字段。对 request 进行扩展,获取 token 等信息时,就不用再使用很长的链式调用的方式来获取请求中所携带的内容了。

'use strict';

module.exports = {
  get token() {
    return this.get('auth-token');
  },
};

扩展 response

'use strict';

module.exports = {
  set token(token) {
    this.set('auth-token', token);
  },
};

添加 mysql 插件

数据库我们选择大家熟悉的 mysql 数据库。

执行:

yarn add egg-mysql

config/config.default.js 中配置

exports.mysql = {
  enabletrue,
  package: 'egg-mysql',
};

config/config.default.js 中配置连接数据库的参数

config.mysql = {
  app: true, // 挂载到 app 下面
  agent: false,
  client: {
    host: '127.0.0.1',
    port: 3306,
    user: 'root',
    password: 'root123456',
    database: 'website-manage',
  },
};

注意:这里需要先下载并安装 mysql 数据库。创建一个名为 website-manage 的数据库。并且在这个数据库中,创建一个 user 表,其中包含的字段如下所示:

从0到1搭建完整的后台管理系统(后端篇)
数据库
从0到1搭建完整的后台管理系统(后端篇)
用户表

创建用户表命令如下所示:

CREATE TABLE `user` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(20) DEFAULT NULL COMMENT '用户名',
  `password` varchar(64) DEFAULT NULL COMMENT '密码',
  `avatar` longtext COMMENT '头像',
  `phone` varchar(20) DEFAULT NULL COMMENT '电话',
  `sign` varchar(300) DEFAULT NULL COMMENT '签名',
  `createTime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
  `updateTime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

添加 egg-sequelize

数据库的基本操作是 CRUD,即为增删改查。这些操作在写 sql 语句时,都很简单。但如果想要进行多表查询,比如查询一个用户(user表)有哪些角色(role表),每个角色又具备哪些权限(authority表)时。这种 sql 语句写起来就更复杂了。并且不容易修改。而这些方法的操作,都有人帮我们先写好了库,它就是 sequelize

添加 egg-sequelize,执行:

yarn add egg-sequelize mysql2

config/plugin.js 中添加配置项

exports.sequelize = {
  enabletrue,
  package: 'egg-sequelize',
};

config/config.default.js 中添加配置

config.sequelize = {
    dialect: 'mysql', // 指定方言
    host: '127.0.0.1',
    port: 3306,
    username: 'root',
    password: 'root123456',
    database: 'website-manage',
    define: {
      timestamps: false,
      freezeTableName: true, // 冻结表名称,使用原始的表名称,不会因为使用了 sequelize 就发生变化
    },
};

添加 user 表的 model

对数据库的操作,我们不直接使用 egg-mysql 提供的方法,而是使用 egg-sequelize 提供的方法。egg-sequelize 要求我们写与数据库中的表一致的 model 结构。这里我们的 user 表的 model 如下所示:

// model/user.js

'use strict';

module.exports = app => {
  const { STRING, INTEGER, TEXT, DATE } = app.Sequelize;

  const User = app.model.define('user', {
    id: { type: INTEGER, primaryKey: true, autoIncrement: true },
    username: STRING,
    password: STRING,
    avatar: TEXT('long'),
    phone: STRING,
    sign: STRING,
    createTime: DATE,
    updateTime: DATE,
  });

  return User;
};

更多关于 sequelize 的方法,比如表与表之间一对多、多对一、多对多关系的建立。可以参考:https://www.sequelize.com.cn/core-concepts/assocs

添加 BaseController 和 BaseService

controller 中对数据进行成功或者失败的数据返回时,都需要统一设置 codemessage。在 service 中操作数据库时,也有可能执行失败,失败后需要打印错误信息,并返回 null。由于每处的操作都一样,所以我们将它们提取为两个基础类。

// controller/base.js

'use strict';

const Controller = require('egg').Controller;

class BaseController extends Controller {
  success(data) {
    this.ctx.body = {
      code: 200,
      data,
      success: true,
      message: '',
    };
  }

  error(message, code = 500) {
    this.ctx.body = {
      code,
      message,
      success: false,
    };
  }
}

module.exports = BaseController;
// service/base.js

'use strict';

const Service = require('egg').Service;

class BaseService extends Service {
  async run(callback) {
    const { ctx, app } = this;
    try {
      if (callback) {
        return callback(ctx, app);
      }
    } catch (error) {
      console.log('log:error-------------: ', error);
      return null;
    }
  }
}

module.exports = BaseService;

项目中的 controllerservice 之前是继承的 eggControllereggService。这里就直接继承 BaseControllerBaseService,这样我们就能使用 BaseControllerBaseSerice 中提供的方法了。

添加用户注册

在前面中,我们有创建了一张用户表,但是用户表中什么数据都没有,为方便后续开发登录以及权限验证等功能。我们需要创建一些真实的用户数据,所以,这一步,先开发注册接口。

controller 目录下新建 user.js 文件

async register() {
    const { ctx, app } = this;
    const params = ctx.params();
    const user = await ctx.service.user.getUser(params.username);
    if (user) {
      this.error('用户已经存在');
      return;
    }
    const result = await ctx.service.user.add({
      ...params,
      password: md5(params.password, app.config.salt),
      createTime: ctx.helper.time(),
    });

    if (result) {
      const token = await this.jwtSign({
        id: result.id,
        username: result.username,
      });
      this.success({    // BaseController 中提供的方法
        ...this.parseResult(ctx, result),
        token,
      });
    } else {
      this.error('用户注册失败!');
    }
  }

service 目录下新建 user.js,并添加方法 add

async add(params) {
    return this.run(async ctx => await ctx.model.User.create(params));
  }

router.js 中添加路由配置

module.exports = app => {
  const { router, controller } = app;
  router.post('/api/user/register', controller.user.register); 
};

添加 egg-redis

上一步将注册接口写好了,下一步当然应该是要开发登录接口。但在登录后,我们需要将一些用户的信息进行缓存起来,这里我们先来添加缓存需要用到的数据库 redis

yarn add egg-redis

配置 config/plugin.js

exports.redis = {
  enabletrue,
  package: 'egg-redis',
};

config/config.default.js 配置 redis 连接

config.redis = {
    client: {
        port: 6379,
        host: '127.0.0.1',
        password: 'redis123456',
        db: 0, // 选择默认的数据库 0
    },
};

config/config.default.js 中配置 redis 缓存时间

  const userConfig = {
    salt: 'website-manage-salt',
    redisExpire: 60 * 60 * 24,
  };

更多 redis 信息。可以参考:https://www.redis.net.cn/order/

添加登录

  • 查询用户是否存在
  • 如果存在,将 user.iduser.username 保存到 redis
  • token 返回给前端
async login() {
    const { ctx } = this;
    const { username, password } = ctx.params();
    const user = await ctx.service.user.getUser(username, password);
    if (user) {
      const token = await this.jwtSign({
        id: user.id,
        username: user.username,
      });
      this.success({
        ...this.parseResult(ctx, user),
        token,
      });
    } else {
      this.error('该用户不存在');
    }
  }`

router.js 中添加登录路由

module.exports = app => {
  const { router, controller } = app;
  ...
  router.post('/api/user/login', controller.user.login);
  ...
};

具体的代码可以查看文章末尾给出的 github 地址。

添加 egg-auth 插件

用户在请求接口时,需要先判断用户是否登录。如果没有登录,就不再继续执行后面的查询操作,直接返回用户未登录的错误信息给前端。我们将权限验证相关的代码写成一个中间件 egg-auth

egg 的中间件建议使用 egg-middlewarnName 的命名方式来开发。

lib/plugin 下面新建一个以 egg 开头的 egg-auth/app 文件夹。文件夹下面包含 package.jsonmiddleware 文件夹。

package.json 中包含当前插件的命名和对外的命名。

// package.json

{
  "name""egg-auth",
  "eggPlugin": {
    "name""auth"
  }
}

middleware 下面就是具体的实现代码。

// lib/plugin/egg-auth/middleware/auth.js
'use strict';

module.exports = options => {
  return async (ctx, next) => {
    const url = ctx.request.url;
    const token = ctx.request.token;
    const tokenCache = await ctx.app.redis.get(ctx.username);
    if (tokenCache !== token && !options.exclude.includes(url.split('?')[0])) {
      ctx.body = {
        code: 1001,
        message: '用户未登录',
      };
    } else {
      await next();
    }
  };
};

注意这个文件的文件名与 package.json 中的 eggPlugin.name 下面的名字保持一致。

config/config.default.js 中添加 auth 的排除项。

config.auth = {
    exclude: [ '/api/user/login''/api/user/register' ],
  };

这里添加的配置内容,可以通过插件里的 options 获取到。

开发好后,在 config/plugin.js 中添加配置。注意这个是本地的插件,所以使用的是 path 的方式。pathpackage 两者互斥,只能使用其中的一种。path 主要用于自己开发的插件,package 主要用于别人开发的并且可以直接通过 yarn add egg-pluginName 安装的插件。

exports.auth = {
  enabletrue,
  path: path.join(__dirname, '../lib/plugin/egg-auth'),
};

app.js 中添加插件的引入。项目根目录下面如果没有 app.js,就需要新建一个 app.js 文件。代码如下:

'use strict';

module.exports = app => {
  const mids = app.config.coreMiddleware;
  app.config.coreMiddleware = [
    ...mids,
    'auth',
  ];
};

注意 package.json 中,eggPlugin 下面的 name 需要与插件中 middleware 下面的文件名、config/plugin.js中的配置、app.js 中间键的名字一致。

想详细了解 app.js 。可以参考:https://eggjs.org/zh-cn/basics/app-start.html

其它插件

介绍了 egg-auth 插件的开发,其它插件的开发也是大同小异,所以不再详细讲解。其它的基础插件还包括:

  • requestLimit 接口限流。防止短时间内,接口被大量暴力请求。
  • allowHosts 允许的主机。防止其它主机直接调用我们的接口。
  • notFound 接口是否存在。判断接口是否存在。
  • interfaceCache 接口缓存。将一些不变的接口进行缓存进来,第二次请求后,就不用再请求数据库,减少查询时间。
  • httpLog 中间件。将每次的请求记录保存起来,方面后面定位问题所在。

这些插件都有实现,在项目的源码中可以查看。

github 地址

最后,送上 github 地址。欢迎大家 fork or star

https://github.com/xiangming25/website-manage-api

总结

项目从最初的搭建到后面基础功能的完善。特别是在文章中间的 md5contextrequestresponse 进行扩展、jwtsequelize 等处。看似毫无关系,甚至还有一头雾水的感觉。但为什么我要写在其它功能的前面?是因为我在一步一步实现功能的同时。也在一步一步地去修改和完善最初做得不好的点。最后总结时,发现那些功能都是需要提前准备好的,所以写在了文章靠前部分。

这一次的总结,只浅显地实现和总结了后端的基础功能。后端所需要掌握的知识和技能绝不是这么简单。这,只是一个开始!!!

文中写得不好的地方,请大家不吝指正!!!


原文始发于微信公众号(前端学习总结):从0到1搭建完整的后台管理系统(后端篇)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/83149.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!