背景

之前基于 Egg.js 开发了几个项目,发现每个项目中都有配置文件、数据库连接操作、数据模型定义、微信登录授权处理等功能,而做新项目时总会复制之前的项目来删删改改,有时候在 A 项目中添加了一个实用的功能,B 项目中添加一个很酷的工具函数,但是过后就忘了,没有一个可以统一沉淀的地方,故做了一个脚手架,基于 Egg.js 封装了数据库连接、自动生成接口文档、字段校验等功能,一来新项目开发可开箱即用,二来也便于技术沉淀。

定位

基于 Egg.js 的后端脚手架。

基于 Egg.js 封装了一层,将常用的功能聚合在一起,更靠近业务,如果 Egg.js 是面,那这个后端脚手架就是面条。

功能介绍

模块 功能介绍 进度
数据库操作 内置 ORM egg-sequelize 插件作为数据库操作库,可适配 Postgres、MySQL、MariaDB、SQLite 数据库
接口路由配置 内置 egg-router-plus 插件解决了路由命名空间问题
自动生成接口文档 内置 egg-swagger-doc 插件,按照约定书写接口注释即可自动生成接口使用文档
字段校验 内置 egg-validate-plus 插件作为字段校验,即可以单独使用,也可以配合 egg-swagger-doc 插件使用,既自动生成接口文档又实现了字段校验
多环境配置 内置了本地开发环境、测试环境和生产环境配置,切换和扩展都是非常便捷的
跨域配置 多环境下设置了不同的跨域配置,应对各个环境下对跨域策略的不同诉求
异常处理 统一封装处理抛出的错误,不将错误直接抛出给前端,提高安全性和比较好的前端使用体验
工具库 封装了菜单树处理、版本号对比、uid 生成等常用的工具函数,开发更便捷
单元测试 完整的单元测试,代码更健壮
微信小程序登录 内置了微信小程序授权登录和获取手机号的全流程,可与微信小程序无缝对接
eslint 内置了比较严格和完整的 eslint 规则,保证了代码规范,协同开发更高效

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
.
├── LICENSE
├── README.md # 搞事之前请先读我
├── app # 主程序代码目录
│ ├── contract # 自动生成接口文档需要的数据结构
│ ├── controller # 控制器
│ ├── extend # 框架扩展
│ ├── middleware # 中间件
│ ├── model # 数据模型
│ ├── public # 公共静态资源
│ ├── router.js # 路由配置
│ └── service # 服务
├── app.js # 入口
├── appveyor.yml
├── config
│ ├── config.default.js # 默认配置,包含自定义的应用配置、安全策略、cookie、跨域、权限等
│ ├── config.local.js # 本地环境特有的配置项
│ ├── config.prod.js # 生产环境特有的配置项
│ ├── config.test.js # 测试环境特有的配置项
│ ├── config.unittest.js # 单元测试环境特有的配置项
│ └── plugin.js # 插件,可在里面自定义启用禁用某些插件
├── jsconfig.json
├── note
│ ├── database.sql # 创建数据库脚本
│ ├── note.md # 项目开发笔记
│ └── tables.sql # 创建 user 表脚本
├── package.json
├── test # 单元测试
│ └── app
└── yarn.lock

项目配置

若没有数据库,可执行下面的 sql 新建一个数据库:

1
CREATE DATABASE IF NOT EXISTS template_node_egg DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;

然后导入 user 数据表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/*
Navicat Premium Data Transfer

Source Server : 本地
Source Server Type : MySQL
Source Server Version : 50722
Source Host : localhost:3306
Source Schema : template_node_egg

Target Server Type : MySQL
Target Server Version : 50722
File Encoding : 65001

Date: 02/01/2022 23:22:25
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`open_id` varchar(255) NOT NULL COMMENT '同微信的 openid',
`union_id` varchar(255) DEFAULT NULL COMMENT '同微信的 unionid,作为预留字段,不一定有值',
`nick_name` varchar(255) NOT NULL COMMENT '昵称,同微信的 nickName',
`password` varchar(255) DEFAULT NULL COMMENT '登录密码,作为预留字段,不一定有值',
`avatar_url` text NOT NULL COMMENT '头像,同微信的 avatarUrl',
`phone` varchar(255) DEFAULT NULL COMMENT '手机号,可能为空',
`gender` int(11) DEFAULT NULL COMMENT '性别,可能为空',
`country` varchar(255) DEFAULT NULL COMMENT '国家,可能为空',
`province` varchar(255) DEFAULT NULL COMMENT '省份,可能为空',
`city` varchar(255) DEFAULT NULL COMMENT '城市,可能为空',
`language` varchar(255) DEFAULT NULL COMMENT '语言,可能为空',
`logged_at` datetime DEFAULT NULL COMMENT '最后登录时间',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of user
-- ----------------------------
BEGIN;
INSERT INTO `user` VALUES (1, 'o8FXk5E4u7hwaguN6kSq-KPXApJ1', NULL, '测试账号', NULL, 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2F5b0988e595225.cdn.sohucs.com%2Fimages%2F20180520%2F0473e00bdfd2476fbe0c228a45a1652c.jpeg&refer=http%3A%2F%2F5b0988e595225.cdn.sohucs.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1628131130&t=05ee794a54bad8edd2fd8bb2536db5b9', NULL, NULL, NULL, NULL, NULL, NULL, '2022-01-01 23:59:59', '2022-01-01 23:59:59', '2022-01-01 23:59:59');
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

然后 yarn dev 启动开发,若是第一次启动,可能需要在 config/config.local.js 修改一下密码配置:

1
2
3
4
5
6
7
8
9
10
11
12
config.sequelize = {
// 连接用户
username: 'root',
// 连接密码
password: '',
// 连接的数据库,可根据需要改成已有的数据库
database: 'template_node_egg',
// 连接地址
host: '127.0.0.1',
// 数据库类型
dialect: 'mysql'
}

如果已有的数据库里面 user 表字段跟 model/user.js 中定义的对不上,需要自行修改,否则会报错

应用配置

自定义的应用配置,修改后全局生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 接口前缀名称,跟随业务系统修改
const apiPrefixName = 'api'
// 接口完整前缀
const apiPrefix = `/${apiPrefixName}`
// 后端接口前缀,跟随业务系统修改
const manageApiPrefixName = 'manage'
// 后端接口前缀
const manageApiPrefix = `/${manageApiPrefixName}`
const userConfig = {
// 应用名称,用于日志文件目录指定、cookie 的 key 指定,具有唯一性,默认是 app.name,也可以改成其他字符串
appName: app.name,
apiPrefixName,
apiPrefix,
manageApiPrefixName,
manageApiPrefix,
// 默认的 code 码和错误提示信息配置,只需要改这一个地方即可
resCode: {
// 成功的 code 标识
success: {
code: 0
},
// 出错的 code 标识和提示
error: {
code: 602, message: '参数异常'
},
// 服务器异常的 code 标识和提示
serverError: {
code: 500, message: '服务器异常'
},
// 未登录的 code 标识和提示
notLogged: {
code: 601, message: '请先登录后再操作'
}
}
}

脚手架内置了应用(例如小程序)接口服务和中后台接口服务,其中的字段配置基本不需要改。中后台接口服务是可选的,若是不想要放在一起,可直接忽略。

应用接口服务使用 ctx.session.[keyName] 来设置和获取值,例如要将 id 字段设置为 cookie,那么设置 cookie 的写法就是:

1
ctx.session.id = 1

获取 cookie 就是:

1
ctx.service.user.info(ctx.session.id)

而中后台接口服务获取需要使用 ctx.cookies.get(key, cookie) 方法,具体用法参见这里ctx.cookies 使用文档

安全策略配置

  • 脚手架默认启用了 Egg.js 提供的安全策略。

  • 脚手架默认允许跨域,而在生产环境关闭此设置,同时自动添加了白名单,开发启动更加便捷:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 前端端口,跟随实际情况修改
const port = 9001
const domainWhiteList = [
...new Set([
`http://127.0.0.1:${port}`,
`http://localhost:${port}`,
// 服务启动时尝试自动获取本机 IP 设置白名单
`http://${getLocalhost()}:${port}`
])
]
config.security = {
domainWhiteList
}
// 默认允许跨域,生产环境关闭此设置
config.cors = {
origin: '*',
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH'
}

权限配置

带登录体系的接口服务,基本都需要指定一部分不需要登录就能访问的接口,脚手架已内置了跟登录相关的白名单接口:

1
2
3
4
5
6
[
`${apiPrefix}/user/mock`,
`${apiPrefix}/user/login`,
`${apiPrefix}/user/logout`,
`${apiPrefix}/user/phone`
]

自动生成文档配置

内置 egg-swagger-doc 插件,按照约定书写接口注释即可自动生成接口使用文档。所有的数据模型结构都在 app/contract 中定义。

具体使用文档参见这里egg-swagger-doc 使用文档,这里就不赘述了。

若不需要此功能,也可在 config/plugin.js 中禁用自动生成文档功能:

1
2
3
4
5
6
{
swaggerdoc: {
enable: false,
package: 'egg-swagger-doc'
}
}

开发

一般开发一个全新接口,需要完成以下四步:

新增模型

app/model 中新增一个 log.js 文件并添加数据模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
'use strict'

/**
* 日志表
*/

module.exports = app => {
// Sequelize.js 提供的数据类型
const { STRING } = app.Sequelize

const Log = app.model.define(/* 数据表名称 */ 'log', {
// 数据表字段
content: {
// 字段类型
type: STRING(255),
// 字段注释
comment: '操作内容'
},
remarks: {
type: STRING(255),
allowNull: false,
comment: '备注'
},
actionType: {
type: STRING(255),
comment: '操作类型'
}
})

return Log
}

新增服务

app/service 中新增一个 log.js 文件并添加一个获取列表的数据库操作方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
'use strict'

const Service = require('egg').Service

class LogService extends Service {
// 获取日志列表
async logs(actionType) {
const { ctx } = this
try {
const data = await ctx.model.Log.findAll({
where: {
actionType
}
})
return ctx.helper.clone(data)
} catch (error) {
ctx.logger.error(error)
}
return false
}
}

module.exports = LogService

新增控制器

app/contract/dto.js 中新增一个 logInfo 模型:

1
2
3
4
5
6
7
8
9
{
// ...

logInfo: {
content: { type: 'string', description: '操作内容' },
remarks: { type: 'string', description: '备注' },
actionType: { type: 'string', description: '操作类型' }
}
}

然后在 app/controller 中新增一个 log.js 文件并添加一个获取列表的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
'use strict'

const Controller = require('egg').Controller

/**
* @controller Log 日志模块
*/
class LogController extends Controller {
/**
* @summary 获取操作记录列表
* @description 根据指定的筛选条件查询用户操作列表
* @router get /log/logs
* @response 0 logInfo 日志列表
*/
async logs() {
const { ctx } = this
const { actionType } = ctx.request.query

// 参数校验
const rules = {
actionType: { required: true, message: '操作类型不能为空' }
}
const passed = await ctx.validate(rules, ctx.request.query)
if (!passed) return

const data = await ctx.service.log.logs(actionType)
if (data) {
this.ctx.helper.success(data)
} else {
this.ctx.helper.error(null, '日志列表失败')
}
}
}

module.exports = LogController

新增接口

app/router.js 中新增一个路由接口:

1
2
// 获取日志列表
subRouter.get('/log/logs', controller.log.logs)

以上例子当然是比较简单的,但大体步骤就是这样。

当然一般新增一个接口时,是不需要再新增一个控制器文件的,基本都是往某个控制器里面添加一个方法,然后在服务里面添加一个数据库操作方法即可。

发布

发布之前,需要在 config/config.prod.js 中配置好数据库连接配置:

1
2
3
4
5
6
7
config.sequelize = {
username: 'root',
password: '',
database: 'template_node_egg',
host: '127.0.0.1',
dialect: 'mysql'
}

启动:

1
yarn start

停止:

1
yarn stop

使用 wy-cli 创建脚手架

全局安装:

1
npm i @wytxer/wy-cli -g

然后执行 wy init project-name 命令安装脚手架,在列表中选择后端脚手架:

1
❯ 后端脚手架(Node.js + Egg.js + Sequelize)

脚手架常用命令:

1
2
3
4
5
6
7
8
9
10
11
# 安装
yarn install

# 启动开发
yarn dev

# 生产环境启动
yarn start

# 生产环境停止
yarn stop

代码和文档

感谢阅读,脚手架以后会长期维护,欢迎下载使用:后端脚手架 GitHub 地址

脚手架使用文档也会保持同步更新。

公众号

程序员未央