likes
comments
collection

从0-1搭建基于egg的后台系统

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

本文涉及知识点

egg、mysql、sequelize、swagger

egg是什么?

Egg.js 是阿里开源的node后端开发框架,为企业级框架和应用而生,可以帮助开发团队和开发人员降低开发和维护成本。

egg的特点

  1. 约定优于配置:目录结构清晰简洁,瞄一眼就知道该在哪里写哪类逻辑
  2. 高可扩展:内置对象、插件中间件的扩展都非常简单、清晰。
  3. 各种鉴权、安全、数据库支持、RESTful等该有的都有,且稳定可靠;
  4. 多进程Cluster很好的解决了JavaScript单线程的缺陷,大大提升了egg的性能。且Master、Agent、Worker又很好的解决了进程间的通信问题。
  5. 开发容易上手、快速敏捷、插件多且成熟;部署简单、快速、体积小资、源消耗少;运行稳定可靠。
  6. 文档非常友好;
  7. 框架得到阿里内部大面积应用建设的考验。

起步

我们推荐使用脚手架,快速初始化

mkdir egg-example && cd egg-example
npm init egg --type=simple (一路回车)
npm i

从0-1搭建基于egg的后台系统

  • 项目结构及重要文件介绍

从0-1搭建基于egg的后台系统

  • egg插件配置
'use strict';

/** @type Egg.EggPlugin */
module.exports = {
  // had enabled by egg
  // static: {
  //   enable: true,
  // }
};
  • egg全局配置
/* eslint valid-jsdoc: "off" */

'use strict';

/**
 * @param {Egg.EggAppInfo} appInfo app info
 */
module.exports = appInfo => {
  /**
   * built-in config
   * @type {Egg.EggAppConfig}
   **/
  const config = exports = {};

  // use for cookie sign key, should change to your own and keep security
  config.keys = appInfo.name + '_1640307948700_2928';

  // add your middleware config here
  config.middleware = [];

  // add your user config here
  const userConfig = {
    // myAppName: 'egg',
  };

  return {
    ...config,
    ...userConfig,
  };
};

  • router入口
'use strict';

/**
 * @param {Egg.Application} app - egg application
 */
module.exports = app => {
  const { router, controller } = app;
  router.get('/', controller.home.index);
};

  • controller
'use strict';

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

class HomeController extends Controller {
  async index() {
    const { ctx } = this;
    ctx.body = 'hi, egg';
  }
}

module.exports = HomeController;

然后使用npm run dev,就可以在浏览器中查看我们的服务了

从0-1搭建基于egg的后台系统


项目前开发前准备工作(mac安装mysql)

在 Web 应用方面 MySQL 是最常见,最好的关系型数据库之一。非常多网站都选择 MySQL 作为网站数据库。

  • mysql 安装
  1. 本机安装mysql(需要先安装homebrew) brew install mysql
  2. 启动mysql mysql.server start
  3. 免密登陆mysql -u root
  4. 修改默认密码(8.0版本以上)alter user 'root'@'localhost' identified with mysql_native_password by 'root';
  5. 控制行出现Query OK, 0 rows affected (0.00 sec)表示安装成功
  6. exit退出,并重新登陆mysql -u root -p
  • 推荐使用图形化工具navicat premium管理我们的数据库 (百度搜索navicat premium安装)

从0-1搭建基于egg的后台系统

egg项目中使用sequelize

egg提供egg-mysql插件直接操作数据库进行开发,而在一些较为复杂的应用中,我们可能会需要一个 ORM 框架来帮助我们管理数据层的代码。而在 Node.js 社区中,sequelize 是一个广泛使用的 ORM 框架,它支持 MySQL、PostgreSQL、SQLite 和 MSSQL 等多个数据源。

安装并配置 egg-sequelize 插件(它会辅助我们将定义好的 Model 对象加载到 app 和 ctx 上)和 mysql2 模块:

npm install --save egg-sequelize mysql2

在 config/plugin.js 中引入 egg-sequelize 插件

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

在 config/config.default.js 中编写 sequelize 配置

config.sequelize = {
  dialect: 'mysql',
  host: '127.0.0.1',
  port: 3306,
  database: 'egg-sequelize-doc-default',
};

我们还可以在config下新建多个配置页面(如config/config.unittest.js ),不同的环境使用不同的数据源地址与数据库

exports.sequelize = {
  dialect: 'mysql',
  host: '127.0.0.1',
  port: 3306,
  database: 'egg-sequelize-doc-unittest',
};

完成上面的配置之后,sequelize 的就初始化完成了。egg-sequelize 和 sequelize 还支持更多的配置项,可以在他们的文档中找到。

初始化数据库和 Migrations

接下来我们设计和初始化一下我们的数据库。首先我们通过 mysql 命令在本地快速创建开发和测试要用到的两个 database:

mysql -u root -e 'CREATE DATABASE IF NOT EXISTS `egg-sequelize-doc-default`;'
mysql -u root -e 'CREATE DATABASE IF NOT EXISTS `egg-sequelize-doc-unittest`;'

我们可以在数据库中直接创建表(不推荐)

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key',
  `name` varchar(30) DEFAULT NULL COMMENT 'user name',
  `age` int(11) DEFAULT NULL COMMENT 'user age',
  `created_at` datetime DEFAULT NULL COMMENT 'created time',
  `updated_at` datetime DEFAULT NULL COMMENT 'updated time',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='user';

大型项目都是需要多人协作的,项目迭代过程中最好是记录每一个迭代的数据变化,而且也会经常需要在不同的环境下进行切换,我们可以使用Migrations来帮助我们进行管理。

sequelize 提供了 sequelize-cli 工具来实现 Migrations,我们也可以在 egg 项目中引入 sequelize-cli。

  • 安装 sequelize-clinpm install --save-dev sequelize-cli
  • 在 egg 项目中,我们希望将所有数据库 Migrations 相关的内容都放在 database 目录下,所以我们在项目根目录下新建一个 .sequelizerc 配置文件
'use strict';

const path = require('path');

module.exports = {
  config: path.join(__dirname, 'database/config.json'),
  'migrations-path': path.join(__dirname, 'database/migrations'),
  'seeders-path': path.join(__dirname, 'database/seeders'),
  'models-path': path.join(__dirname, 'app/model'),
};
  • 初始化 Migrations 配置文件和目录
npx sequelize init:config
npx sequelize init:migrations
  • 执行完后会生成 database/config.json 文件和 database/migrations 目录,我们修改一下 database/config.json 中的内容,将其改成我们项目中使用的数据库配置
{
  "development": {
    "username": "root",
    "password": null,
    "database": "egg-sequelize-doc-default",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "test": {
    "username": "root",
    "password": null,
    "database": "egg-sequelize-doc-unittest",
    "host": "127.0.0.1",
    "dialect": "mysql"
  }
}

此时 sequelize-cli 和相关的配置也都初始化好了,我们可以开始编写项目的第一个 Migration 文件来创建我们的一个 users 表了。npx sequelize migration:generate --name=init-users

执行完后会在 database/migrations 目录下生成一个 migration 文件(${timestamp}-init-users.js),我们修改它来处理初始化 users 表:

'use strict';

module.exports = {
  // 在执行数据库升级时调用的函数,创建 users 表
  up: async (queryInterface, Sequelize) => {
    const { INTEGER, DATE, STRING } = Sequelize;
    await queryInterface.createTable('users', {
      id: { type: INTEGER, primaryKey: true, autoIncrement: true },
      name: STRING(30),
      age: INTEGER,
      created_at: DATE,
      updated_at: DATE,
    });
  },
  // 在执行数据库降级时调用的函数,删除 users 表
  down: async queryInterface => {
    await queryInterface.dropTable('users');
  },
};

databases/migrations中的文件写好之后,我们执行 migrate 进行数据库变更npx sequelize db:migrate,此时,我们的数据库初始化就完成了。(这一系列代替我们手工创建)

model层逻辑编写

我们可以在app/model/目录下手工编写model层,但是如果数据库表多的话,写起来非常耗时间

  • 使用第三方库自动生成modelnpm i sequelize-automate -D
  • 新建自动生成model的配置文件
'use strict';

module.exports = {
  dbOptions: {
    database: 'xxx',
    username: 'root',
    password: '',
    dialect: 'mysql',
    host: 'localhost',
    port: 3306,
    logging: false,
    timezone: '+08:00',
    dialectOptions: {
      dateStrings: true,
      typeCast: true,
    },
    define: {
      // 时间戳
      timestamps: true,
      // 时间驼峰转换
      underscored: true,
      // 参数停止 Sequelize 执行自动复数化 表名= 模型名称
      freezeTableName: true,
      charset: 'utf8',
      dialectOptions: {
        collate: 'utf8_general_ci',
      },
    },
  },
  options: {
    type: 'egg',
    dir: 'app/model',
    camelCase: true, // Models 文件中代码是否使用驼峰发命名
    fileNameCamelCase: true, // Model 文件名是否使用驼峰法命名,默认文件名会使用表名,如 `user_post.js`;如果为 true,则文件名为 `userPost.js`
  },
};
  • package的script可以加一个钩子并且在创建完成后自动执行eslint格式化一下 "sequelize-automate -c sequelize-automate.config.js && eslint --ext .js app/model --fix

从0-1搭建基于egg的后台系统

  • 出现上图,则表示成功创建model,可以在我们代码的model中查看了

  • 这里是我们model的说明

从0-1搭建基于egg的后台系统


业务逻辑编写

我们再详细介绍一些app文件夹中的内容

  • router: 用于配置 URL 路由规则(可以是文件夹也可以放在一个文件中)
  • controller:用于存放控制器 接收、校验、处理 HTTP 请求参数 向下调用服务(Service)处理业务 通过 HTTP 将结果响应给用户
  • service:用于编写业务逻辑层(可选,我们使用service层通过model模型,进行crud)
  • model:用于存放数据库模型(可选)
  • middleware: 用于编写中间件 (可选,我们可以添加gzip、慢接口查询等中间件)
  • schedule: 用于设置定时任务 (可选)
  • public: 用于放置静态资源 (可选)
  • view: 用于放置模板文件 (可选,服务端渲染)
  • extend: 用于框架的扩展 (可选,可以写公共的方法,比如获取当前时间,格式转换等)

基础controller示例

const Controller = require('egg').Controller;
class BaseController extends Controller {
  get user() {
    return this.ctx.session.user;
  }
  success(data, msg = '') {
    this.ctx.body = { msg, data, code: 200 };
  }
  error(msg = '', code = -1) {
    this.ctx.body = { msg, code };
  }
  notFound(msg) {
    this.ctx.throw(404, msg || 'not found');
  }
}
module.exports = BaseController;

业务controller示例

const Controller = require('../core/baseController');
class UserController extends Controller {
  async getUser() {
    const { ctx } = this;
    const params = ctx.request.body;
    try {
      const data = await ctx.service.user.find(params);
      this.success(data);
    } catch (e) {
      this.error(e.message);
    }
  }

业务service示例

// app/service/user.js
const Service = require('egg').Service;
class UserService extends Service {
  async find(params) {
    const { ctx: { model } } = this;
    const startPage = params && params.startPage || 1;
    const pageSize = params && params.pageSize || 10;
    // 默认查找生效数据
    const where = { state: '1' };
    // 使用sequelize查找器查找(推荐)
    const { count: total, rows } = await model.User.findAndCountAll({
      where, // WHERE 条件
      orders: [[ 'publishTime', 'desc' ]], // 排序方式
      limit: pageSize, // 返回数据量
      offset: (startPage - 1) * pageSize, // 数据偏移量
    });
    return {
      total,
      list: rows,
    };
  }
  }
}

module.exports = UserService;

至此,这个项目的业务逻辑部分就写完了

swagger

一个完成的项目还需要接口文档,我们使用swagger-egg自动生成

  • 安装npm i swagger-egg
  • egg配置与egg插件配置
  • controller中加上注释及schema使用

redis引入

服务端复杂的数据库查询会严重降低访问速度,我们需要将一些常用的接口数据进行存储,用户在获取数据的时候,直接将数据从内存中读取,不进行数据库查询,可以大大提高服务器性能。存储方式主要有文件存储和内存存储,文件存储通过fs模块写入读取txt文件,不多说;这里使用redis内存存储。

  • 安装brew install redis
  • 配置文件路径: vim /usr/local/etc/redis.conf
  • 手动启动和停止redis: brew services restart redis
  • 启动成功消息: ==> Successfully started redis (label: homebrew.mxcl.redis)
  • 配置文件和插件配置好之后service层这样使用(建议封装成RedisService使用)
// 方式一:直接使用
await this.app.redis.set('userInfo', 'xx');
let userInfo = await this.app.redis.get('userInfo');
// 方式二:通过service使用
let userInfo = await this.service.redis.get('userInfo');
if (!userInfo) {
	// @TODO 通过数据库获取
	const sqlData = await model.user.find('userInfo')
	this.service.redis.set('userInfo', sqlData)
}

结尾

基于以上的步骤我们应用就建好了,生产环境直接start运行即可。 后面有时间会讲下环境部署与配置,docker-compose构建多个服务,docker-swarm & k8s 部署多台服务器,修改 nginx,实现负载均衡


参考链接

sequelize中文文档 www.sequelize.com.cn/

egg中文文档 eggjs.org/zh-cn/

sequelize-automate github.com/nodejh/sequ…

egg-sequelize github.com/eggjs/egg-s…

egg-redis github.com/eggjs/egg-r…