1、基础环境
1.1、操作系统
window10
1.2、数据库
mysql
1.3、开发工具
WebStorm
1.4、egg.js开发环境的依赖项(直接贴我的package.json文件部分了),如下:
"egg": {
"declarations": true
},
"dependencies": {
"egg": "^2.15.1",
"egg-scripts": "^2.11.0",
"egg-sequelize": "^5.2.0",
"egg-validate": "^2.0.2",
"moment": "^2.24.0",
"mysql2": "^1.7.0",
"uuid": "^3.3.3"
},
"devDependencies": {
"autod": "^3.0.1",
"autod-egg": "^1.1.0",
"egg-bin": "^4.11.0",
"egg-ci": "^1.11.0",
"egg-mock": "^3.21.0",
"eslint": "^5.13.0",
"eslint-config-egg": "^7.1.0",
"factory-girl": "^5.0.4",
"sequelize-cli": "^5.5.1"
},
"engines": {
"node": ">=10.0.0"
},
1.5、测试工具
这部分测试可以使用POSTMAN进行,也可以使用断言库进行,推荐使用断言进行测试
2、建表语句
这部分只拿一张表作为示例:角色表,下面是建表语句
CREATE TABLE tf_sys_roler(
id CHAR(36) PRIMARY KEY COMMENT '角色编号' DEFAULT 1,
role_name CHAR(24) COMMENT '角色名称',
role_code CHAR(24) COMMENT '角色code',
created_by CHAR(16) COMMENT '创建人',
created_time TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建日期',
updated_by CHAR(16) COMMENT '修改人',
updated_time TIMESTAMP NULL COMMENT '修改日期',
del_flag CHAR(2) COMMENT '是否删除:1表示已删除,0表示未删除'
)ENGINE= MYISAM CHARACTER SET utf8;
3、Restful API风格的接口说明
接口无外乎就是增删改查,再此基础上还可以衍生出对应的批量操作等,下面是controller层定义的方法名称和CRUD的对应(这方法名称最好严格按照restful的来,否则会很纠结一直404)
查询:
GET请求 ,对应的方法名称是:index()
新增:
POST请求,对应的方法名称是:create()
更新:
PUT请求,对应的方法名称是:update()
删除:
DELETE请求,对应的方法名称是:destroy()
controller的方法命名如下图:
4、工程的目录结构,如下图:
因为我是用的egg脚手架生成的工程目录,所以我这部分没有什么特殊的
脚手架命令:
npm init egg --type=simple
下面对部分目录进行一些必要的说明
4.1、目录结构说明:
这部分,每个目录可以参照官方的文档说明来看,如下图所示
目录结构上面,建议按照官方的这样来写,特别是起步阶段,方便快速开始
几点说明:
1、如果想要按照对应的模块分目录,也是可行的,比如我这边,角色属于系统管理部分,我想把他放在sys目录下,就可以如下图这样布局:
获取的时候,带上sys就行,如下:
app.router.resources('sys_role', '/api/v2/role', app.controller.sys.role);
但是,如果是model模块下,如果获取记得把首字母大写,目录和模块都需要首字母大写,否则,会出现undefined的错误的
const role = await ctx.model.Sys.Role.create({ role_name, role_code });
4.2、db.js模块的说明
这个db.js模块是app下的一个模块,本不是官方建议的这样的一个模块,但是出于以下几点的考虑:
1、因为数据库中,每一个表都有主键ID、创建人、创建时间等公共字段,这样我把这些公共的部分抽到一个单独的模块中,然后其他的model模块在使用的时候在调用这个公共模块可以避免很多的重复字段定义
2、主键ID,系统要求是做到以UUID的形式存在,抽到一个公共的模块中,也可以避免重复定义操作和误操作带来的不必要的麻烦,其他的公共字段也有出于这部分考虑的需要
3、app.model.define这个模块定义,需要将默认的createAt和updateAt字段去除,而使用自定义的created_by和updated_by
详细的db.js的代码如下:
'use strict';
const uuidv1 = require('uuid/v1');
const moment = require('moment');
function generateUUID() {
return uuidv1().replace(/-/g, '');
}
/**
* @author guxuhua
* @param app type egg实例
* @param name type 表名称
* @param attributes type 表字段
* * */
function defineModel(app, name, attributes) {
const { UUID, STRING, DATE } = app.Sequelize;
const attrs = {};
for (const key in attributes) {
const value = attributes[key];
if (typeof value === 'object' && value.type) {
value.allowNull = value.allowNull && true;
attrs[key] = value;
} else {
attrs[key] = {
type: value,
allowNull: true,
};
}
}
// 定义主键ID
attrs.id = {
type: UUID,
primaryKey: true,
defaultValue: () => {
return generateUUID();
},
};
// 是否删除1表示已删除,0表示未删除
attrs.del_flag = {
type: STRING,
defaultValue: () => {
return '0';
},
};
// 定义创建人
attrs.created_by = {
type: STRING,
};
// 创建日期
attrs.created_time = {
type: DATE,
defaultValue: () => {
return moment(Date.now()).format('YYYY-MM-DD HH:mm:ss');
},
};
// 更新人
attrs.updated_by = {
type: STRING,
};
// 更新日期
attrs.updated_time = {
type: DATE,
defaultValue: () => {
return moment(Date.now()).format('YYYY-MM-DD HH:mm:ss');
},
};
return app.model.define(name, attrs, {
// 去除createAt updateAt
timestamps: true,
// 使用自定义表名
freezeTableName: true,
// 将createdAt设置为false,若不设置,字段会在查询等操作中作为字段出现,会报字段不存在
createdAt: false,
// 将updatedAt设置为false,否则会报字段不存在
updatedAt: false,
});
}
module.exports = { defineModel };
5、启用的egg-sequelize模块和数据的配置
5.1:安装依赖
需要用到mysql2和egg-sequelize两个模块,所以需要执行如下的命令安装依赖:
npm install --save egg-sequelize mysql2
5.2:启用egg-sequelize
在plugin.js模块下,添加如下配置就表示启用了
/** 启用egg-sequelize */
exports.sequelize = {
enable: true,
package: 'egg-sequelize',
};
5.3、添加mysql的配置
在config.default.js模块下添加这部分的配置,如下:
// 添加mysql的配置项
config.sequelize = {
// 数据库类型
dialect: 'mysql',
// host
host: '127.0.0.1',
// 端口号
port: '3306',
// 用户名
username: 'root',
// 密码
password: '!QAZ2wsx',
// 数据库名
database: 'ics_db',
// 设置时区为东8区
timezone: '+08:00',
};
另外,因为是开发阶段,建议把logger模式启用成debug模式,方便追踪问题
// 添加日志级别为debug
config.logger = {
level: 'DEBUG',
outputJSON: true,
encoding: 'utf-8',
consoleLevel: 'DEBUG',
};
6、业务功能代码说明
6.1、model代码
这部分的代码,熟悉java的朋友们可以理解成实体类,详细代码如下:
'use strict';
/**
* 角色表:tf_sys_roler对应的model层*/
// 导入db.js模块
const db = require('../../db');
module.exports = app => {
// 使用sequelize的STRING字段类型
const { STRING } = app.Sequelize;
const Role = db.defineModel(app, 'tf_sys_roler', {
// 角色名称
role_name: { type: STRING, length: 24, allowNull: false },
// 角色的code
role_code: { type: STRING, length: 24, unique: true, allowNull: false },
});
return Role;
};
6.2、service层代码
其实不用上service层代码,直接在controller层也是可以通过
ctx.model.Sys.Role
这样的方式调用到model层的代码实现数据的增删改查的,但是考虑到数据的操作会有一堆的校验等逻辑操作,如果都放在controller中,controller会显得太过于臃肿,所以将数据的获取、校验(非空、格式等校验)等放在controller,而将数据的逻辑校验、数据查询等直接和数据库有关联的操作放在service层,代码如下:
'use strict';
/**
* 角色表:tf_sys_roler对应的Service层*/
// 导入egg的service模块
const Service = require('egg').Service;
const moment = require('moment');
class RoleService extends Service {
/**
* 新增一条角色记录
* @param role_name 角色名称
* @param role_code 角色编号
* */
async create({ role_name, role_code }) {
const ctx = this.ctx;
ctx.logger.debug(`>>>>>开始新增一条角色信息,角色名称是:${role_name},角色编号是:${role_code}<<<<<`);
// 参数进行非空校验
if (role_name === null || role_name === '' || undefined === role_name) {
throw new Error('请求参数【role_name】不允许为空');
}
if (role_code === null || role_code === '' || undefined === role_code) {
throw new Error('请求参数【role_code】不允许为空');
}
// 新增之前,判断role_code是否唯一:({ role_code: role_code, del_flag: '0' });
const exit = await ctx.model.Sys.Role.findAll({
where: {
role_code,
del_flag: '0',
},
});
if (exit && exit.length > 0) {
throw new Error(`渠道编号:${role_code}已经存在了`);
}
const role = await ctx.model.Sys.Role.create({ role_name, role_code });
if (role) {
return role.id;
}
throw new Error('角色新增失败');
}
/**
* 逻辑删除(更新del_flag字段的值)指定的角色信息
* @param id 角色的主键
* */
async delete({ id }) {
const ctx = this.ctx;
ctx.logger.debug(`>>>>>开始根据id:${id}删除角色信息<<<<<`);
// 先根据id查询角色对象:根据主键查询
const exit = await ctx.model.Sys.Role.findByPk(id);
if (!exit) {
ctx.logger.debug(`>>>>>根据主键:${id}无法查询到角色信息<<<<<`);
throw new Error('无法获取到指定的角色信息');
}
// 是否已经删除
if (exit.del_flag === '1') {
throw new Error('当前的角色信息已经被删除了');
}
// del_flag = 1表示删除
const del_flag = '1';
// 更新时间
const updated_time = moment(Date.now()).format('YYYY-MM-DD HH:mm:ss');
await exit.update({ del_flag, updated_time });
return true;
}
/**
* 更新角色信息
* @param id 角色对应的主键
* @param role_code 角色编号
* @param role_name 角色名称
* */
async update({ id, role_code, role_name }) {
const ctx = this.ctx;
ctx.logger.debug('>>>>>开始更新角色信息<<<<<');
// 先根据ID查询指定的角色
const exit = await ctx.model.Sys.Role.findByPk(id);
if (!exit) {
throw new Error('无法获取到指定的角色信息');
}
// 是否已经删除
if (exit.del_flag === '1') {
throw new Error('当前的角色信息已经被删除了');
}
// 可以获取到,和入参的role_code、role_name对比,是否存在变更
if (role_code === exit.role_code && role_name === exit.role_name) {
throw new Error('角色信息与现在的一致,无需变更');
}
// 如果code发生变更,校验是否与现在的一致,如果不一致,需要校验新的是否存在重复
if (role_code !== exit.role_code) {
ctx.logger.debug(`>>>>>入参code:${role_code}和库里现有的:${exit.role_code}不一致,开始校验新的入参是否存在重复<<<<<<`);
const queryByCode = await ctx.model.Sys.Role.findAll({
where: {
role_code,
del_flag: '0',
},
});
if (queryByCode && queryByCode.length > 0) {
throw new Error(`渠道编号:${role_code}已经存在了`);
}
}
// 更新时间
const updated_time = moment(Date.now()).format('YYYY-MM-DD HH:mm:ss');
// 都校验完成,执行更新
await exit.update({ role_code, role_name, updated_time });
return true;
}
/**
* 按条件查询角色信息:分页查询,默认查询未删除
* @param offset 起始页
* @param limit 每页展示条数
* @param del_flag 是否删除1表示已删除,0表示未删除
* @param role_code 角色编号
* @param role_name 角色名称
* */
async query({ offset = 0, limit = 10, del_flag = '0', role_code, role_name }) {
const ctx = this.ctx;
ctx.logger.debug('开始查询角色信息');
return ctx.model.Sys.Role.findAndCountAll({
offset,
limit,
del_flag,
role_code,
role_name,
order: [[ 'created_time', 'desc' ]],
});
}
}
module.exports = RoleService;
6.3、controller层代码
这部分的代码就是控制路由分发过来的请求,调用对应的service层代码,在返回响应信息的,详细代码如下
'use strict';
/**
* 角色表:tf_sys_roler对应的Controller层
* */
// 导入egg.Controller
const Controller = require('egg').Controller;
class RoleController extends Controller {
/**
* 按条件查询角色信息
* */
async index() {
const ctx = this.ctx;
const query = {
limit: ctx.helper.parseInt(ctx.query.limit),
offset: ctx.helper.parseInt(ctx.query.offset),
role_code: ctx.query.code,
role_name: ctx.query.name,
del_flag: ctx.query.del_flag,
};
ctx.logger.debug(`>>>>>开始分页查询用户信息,开始页是:${query.offset},每页展示条数:${query.limit}<<<<<`);
ctx.logger.debug(`>>>>>角色编号是:${query.role_code},角色名称是:${query.role_name},删除标识位:${query.del_flag}<<<<<`);
ctx.body = await ctx.service.sys.role.query(query);
}
/**
* 新增一条角色记录
* */
async create() {
const ctx = this.ctx;
ctx.logger.debug('>>>>>开始创建一条角色记录<<<<<<');
// 创建的请求参数
const createParam = {
role_name: ctx.request.body.name,
role_code: ctx.request.body.code,
};
const id = await ctx.service.sys.role.create(createParam);
if (id) {
ctx.body = id;
} else {
ctx.body = '角色信息创建失败';
}
}
/**
* 修改角色记录
* */
async update() {
const ctx = this.ctx;
ctx.logger.debug(`>>>>>开始根据编号:${ctx.params.id}更新一条角色记录<<<<<`);
// 获取请求参数:body中获取
const updateParam = {
id: ctx.params.id,
role_code: ctx.request.body.code,
role_name: ctx.request.body.name,
};
ctx.body = await ctx.service.sys.role.update(updateParam);
}
/**
* 删除一条角色记录
* */
async destroy() {
const ctx = this.ctx;
ctx.logger.debug(`>>>>>开始根据编号:${ctx.params.id}删除一条角色记录<<<<<`);
const deleteParam = {
id: ctx.params.id,
};
ctx.body = await ctx.service.sys.role.delete(deleteParam);
}
}
module.exports = RoleController;
6.4、路由的配置
因为使用的restful风格的接口,路由中就要resources做了一个映射,如下:
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
// 角色的API
app.router.resources('sys_role', '/api/v2/role', app.controller.sys.role);
};
7、测试
这部分,我通过编写测试代码来进行测试,而不是直接通过postman这个工具来发起测试,一方面可以让自己更好的理解egg或者说更好的理解node,另一方面也显得高大上点不是
另外,关于测试,官方的文档也有提及,这里我也是按照官方的那套来写我自己的测试程序的
测试模块的代码目录如下:
7.1、首先通过factory-girl这个模块来创建测试数据方便的是创建完了也可以迅速清除掉,避免带来过多的测试用的垃圾数据
7.1.1、安装依赖:
npm install --save-dev factory-girl
7.1.2、编写role.factories.js模块快速为测试程序创建并且测试结束后清除数据,代码如下
'use strict';
const { factory } = require('factory-girl');
module.exports = app => {
app.factory = factory;
factory.define('role', app.model.Sys.Role, {
role_name: factory.sequence('Role.name', n => `name_${n}`),
role_code: factory.sequence('Role.code', n => `name_${n}`),
});
};
7.2、编写.setup.js
,引入 factory,并确保测试执行完后清理数据,避免被影响,代码如下:
'use strict';
const { app } = require('egg-mock/bootstrap');
const factories = require('./role.factories');
before(() => {
// defined app.factory for build test data
factories(app);
});
afterEach(async () => {
// clear database after each test case
await Promise.all([
app.model.Sys.Role.destroy({ truncate: true, force: true }),
]);
});
7.3、编写业务测试的模块:role.test.js,代码如下:
'use strict';
const { assert, app } = require('egg-mock/bootstrap');
describe('test/app/controller/role.test.js', () => {
describe('GET /role', () => {
it('查询角色信息', async () => {
await app.factory.createMany('role', 3);
const res = await app.httpRequest().get('/api/v2/role?limit=2');
assert(res.status === 200);
assert(res.body.count === 3);
assert(res.body.rows.length === 2);
assert(res.body.rows[0].role_name);
assert(res.body.rows[0].role_code);
});
});
describe('POST /role', () => {
it('新增角色信息', async () => {
const res = await app.httpRequest().post('/api/v2/role').send({
name: '断言测试',
code: 'assert',
});
assert(res.status === 200);
console.log(res.text);
assert(res.text);
});
});
describe('PUT /role', () => {
it('更新角色信息', async () => {
// 需要先新增,然后用该数据测试
const add = await app.httpRequest().post('/api/v2/role').send({
name: '断言测试1',
code: 'assert1',
});
assert(add.status === 200);
const res = await app.httpRequest().put(`/api/v2/role/${add.text}`).send({
name: '断言测试2',
code: 'assert2',
});
assert(res.status === 200);
assert(res.text);
});
});
describe('DELETE /role', () => {
it('删除角色信息', async () => {
// 先新增,然后删除掉
const add = await app.httpRequest().post('/api/v2/role').send({
name: '断言测试1',
code: 'assert1',
});
const res = await app.httpRequest().delete(`/api/v2/role/${add.text}`);
assert(res.status === 200);
assert(res.text);
});
});
});
7.4、执行测试
控制台直接输入命令:npm test就可以执行测试了,当然,他会把test目录下的所有的带有describe的标识的都给执行了,执行结果如下:
好了,如上就是我分享的一个使用egg和他的ORM插件:egg-sequelize实现的一个restful风格的API接口