创建一个 egg 项目
egg 中文官网 在官网中可以查到创建一个 egg 项目只需要三步命令行
mkdir egg-example && cd egg-example // 这一步甚至可以省略
npm init egg --type=simple
npm i
通过上面三步命令行就可以创建出一个 egg 项目了,然后通过 npm run dev 命令运行代码。
路由管理
为了方便项目的路由管理可以在 app 目录下面创建一个 router 目录,这里面专门放置路由文件。类似于这样的文件夹。并且在 router.js 文件中通过 require('./router/user')(app); 导入
Cors 设置
如果项目中有使用 post 方式去请求,并且还是通过 postman 形式的话,不设置 cors,他会报错。因此得设置一下 cors 这样才不会出错。
- 通过命令行
npm i -S egg-cors安装 egg-cors 的包 - 在
config/plugin.js文件中添加以下代码
'use strict';
/** @type Egg.EggPlugin */
module.exports = { cors: {
enable: true,
package: 'egg-cors'
}
};
- 在 config/config.default.js 中添加以下代码,自此通过外部访问的 post 请求就可以正常访问了
/* eslint valid-jsdoc: "off" */
'use strict';
/**
* @param {Egg.EggAppInfo} appInfo app info
*/
module.exports = appInfo => {
// 关闭csrf 开启跨域
config.security = {
csrf: {
enable: false,
},
// 这个是添加白名单
domainWhiteList: []
}
config.cors = {
origin: "*",
allowMethods: 'GET, PUT, POST, DELETE, PATCH'
}
return {
...config,
...userConfig,
};
};
Sequelize 设置
- 经过命令行
npm install --save-dev sequelize-cli安装 sequelize-cli 工具 - 在项目根目录下面创建一个
.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'),
};
- 安装
npm i -S egg-mysql egg-sequelize, 并且在 config/plugin.js和 config/config.default.js 两个文件中去配置
// config/plugin.js
'use strict';
/** @type Egg.EggPlugin */
module.exports = {
mysql: {
enable: true,
package: 'egg-mysql',
},
sequelize: {
enable: true,
package: 'egg-sequelize',
}
};
// config/config.default.js
/* eslint valid-jsdoc: "off" */
'use strict';
/**
* @param {Egg.EggAppInfo} appInfo app info
*/
module.exports = appInfo => {
// sequelize 设置
config.sequelize = {
dialect: 'mysql',
host: '127.0.0.1',
port: 3306,
username: 'root',
password: 'admin123',
timezone: '+8:00',
database: 'eggapi',
define: {
// 取消数据表名复数
freezeTableName: true,
// 自动写入时间戳 created_at updated_at
timestamps: true,
// 自动生成软删除时间戳
// paranoid: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
// deletedAt: 'deleted_at',
// 所有驼峰命名格式化
underscored: true
}
};
return {
...config,
...userConfig,
};
};
- 初始化 migrations 配置文件
npx sequelize init:confignpx sequelize init:migrations
通过上面这两个命令行会在根目录下创建一个 database 文件夹,并且会在 database 文件夹下创建一个 migrations 文件夹 和 一个 config.json 配置文件。
- 修改
/database/config.json文件,将自己对应的数据库用户名,密码,表名全都写进去
{
"development": {
"username": "数据库的用户名",
"password": "数据库的密码",
"database": "数据库的表名",
"host": "127.0.0.1",
"dialect": "mysql",
"timezone": "+8:00"
},
"test": {
"username": "root",
"password": "admin123",
"database": "eggapi",
"host": "127.0.0.1",
"dialect": "mysql",
"timezone": "+8:00"
},
"production": {
"username": "root",
"password": "admin123",
"database": "eggapi",
"host": "127.0.0.1",
"dialect": "mysql",
"timezone": "+8:00"
}
}
- 以上的配置项全都配置完成以后,接下来创建子表
- 运行
npx sequelize migration:generate --name=init-子表名字命令行,会在database/migrations底下创建一个 migration 文件,文件格式是 一个时间戳+ ‘-init-’ + 子表名 (${timestamp}-init-users.js) - 在这个文件中去写入这个表对应的格式 ,代码如下
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
up: async (queryInterface, Sequelize) => {
const { INTEGER, DATE, STRING, ENUM } = Sequelize;
await queryInterface.createTable('user', {
// UNSIGNED 这个属性是 无符号 的意思,也就是这个类型的值为非负数
// primaryKey 这是主键的意思
// auto_increment 自动递增
// allowNull 是否允许为 null
// defaultValue 默认值
// comment 为字段或者列添加注释
// unique 唯一标识
// 如果 type 为 ENUM 枚举类型的话,那么得给 values 赋值上 数组选项
id: { type: INTEGER(20).UNSIGNED, primaryKey: true, autoIncrement: true },
username: { type: STRING(30), allowNull: false, defaultValue: '', comment: '用户名称', unique: true },
password: { type: STRING(200), allowNull: false, defaultValue: '' },
avatar_url: { type: STRING(200), allowNull: false, defaultValue: '' },
sex: { type: ENUM, values: ['男', '女', '保密'], allowNull: false, defaultValue: '男', comment: '用户性别' },
created_at: DATE,
updated_at: DATE,
});
},
// 在执行数据库降级时调用的函数,删除 user 表
down: async (queryInterface) => {
await queryInterface.dropTable('user');
},
};
- 运行
npx sequelize db:create来创建数据库表 - 运行
npx sequelize db:migrate将 migrations 和 mysql进行关联
设置数据模型
- 在 app/model 下创建对应子表名的文件
- 在这个文件中写入以下代码,里面的代码其实跟在 migration 里面写的差不多,几乎是照搬过来的。自此数据模型搭建成功
module.exports = app => {
const { STRING, INTEGER, DATE, ENUM } = app.Sequelize;
const User = app.model.define('user', {
id: { type: INTEGER(20).UNSIGNED, primaryKey: true, autoIncrement: true },
username: { type: STRING(30), allowNull: false, defaultValue: '', comment: '用户名称', unique: true },
password: { type: STRING(200), allowNull: false, defaultValue: '' },
avatar_url: { type: STRING(200), allowNull: false, defaultValue: '' },
sex: { type: ENUM, values: ['男', '女', '保密'], allowNull: false, defaultValue: '男', comment: '用户性别' },
created_at: DATE,
updated_at: DATE,
});
return User;
}
Sequelize 增删改查(CRUD)
- 增
- 新增一个数据,创建数据模型以后,可以通过
this.app.model.User.create({username: '测试', password: '123456', sex: "男"})通过这个方法就能创建数据
async create() {
const { ctx } = this;
let res = await this.app.model.User.create({
username: '测试2',
password: '1234567',
sex: "男"
})
ctx.body = {
msg: 200,
data: res
}
}
- 批量新增,可以通过
this.app.model.User.bulkCreate([])来批量新增数据,代码如下
async create() {
const { ctx } = this;
// 这里的 try catch 是因为在一开始设计子表的时候, username必须是唯一不能重复的,因此这里用 try catch 进行包裹一下
let res = [];
try {
res = await this.app.model.User.bulkCreate([
{
username: '测试10',
password: '1234567',
sex: "男"
},
{
username: '测试11',
password: '1234567',
sex: "男"
},
{
username: '测试12',
password: '1234567',
sex: "男"
},
{
username: '测试13',
password: '1234567',
sex: "男"
},
{
username: '测试14',
password: '1234567',
sex: "男"
},
])
} catch(e) {
res = {
message: e.errors[0].message
}
}
ctx.body = {
msg: 200,
data: res
}
}
- 查
2.1 查找单个
- 通过主键的形式去查找
this.ctx.model.User.findByPk(主键id),也可以通过this.app.model.User.findByPk(主键id)这两个都是一样的效果,都会找到对应主键id的数据。 - 通过 findOne 条件查找的方式去查,
this.ctx.model.User.fineOne({where: {username: '测试'}})通过这个方法就可以查找到对应的数据。如果是用findOne的where形式去查找数据的话,有可能会查找到多条数据,他只会返回第一条数据。
2.2 查询所有数据
- 可以通过
this.ctx.model.User.findAll()查找所有的数据 - 可以通过
this.ctx.model.User.findAndCountAll()来查找所有数据,这个查找到的数据还有 count 数量 - findAll 和 findAndCountAll 里面的配置都是一样的,可以通过 where 去查找,代码如下 并且
result = await ctx.model.User.findAndCountAll({
where: {
username: {
[Op.like]: "%测试%"
}
}
});
sequelize 还提供了 Op可以用来模糊查询之类的方法,常用的方法有以下几种。
[Op.and]:{a:5} // 且 a=5
[Op.or]:[{a:5},{a:6}] // a=5或者a=6
[Op.gt]:6 // id > 6
[Op.gte]:6 // id >=6
[Op.lt]:10 // id <6
[Op.lte]:10 // id <=10
[Op.ne]:20 // id != 20
[Op.eq]:3 // =3
[Op.not]:true // 不是true
[Op.between]:[1,10] // 在1和10之间,这个区间范围内全部选中
[Op.notBetween]:[1,10] // 不在1和10之间
[Op.in]:[1,2] // 在 [1,2]之中,这几个全都选中
[Op.notIn]:[1,2] // 不在[1,2]中
[Op.like]:'%adm' // 模糊查询,包含 '%adm'
[Op.notLike]:'%adm' // 不包含 '%adm'
[Op.iLike]:'%adm' // 包含'%adm' (不区分大小写)(仅限PG)
[Op.notILike]:'%adm' //不包含 '%adm' (不区分大小写)(仅限PG)
[Op.startswith]:'adm' // 类似 'adm%',以 'adm'开头
[Op.endswith]:'adm' // 类似 '%adm',以adm结尾
[Op.substring]:'adm' // 类似 '%adm%',包含有'adm'
[Op.regexp]:'^[a|d|m]' // 匹配正则表达式 (仅限Mysql/PG)
[Op.notRegexp]: '^[a|d|m]' // 不匹配正则表达式/!~ '^[a|d|m]' (仅限 MySQL/PG)
[Op.iRegexp]: '^[a|d|m]' // ~* '^[a|d|m]' (仅限 PG)
[Op.notIRegexp]: '^[a|d|m]' // !~* '^[a|d|m]' (仅限 PG)
[Op.like]: { [Op.any]: ['cat', 'hat']} // 包含任何数组['cat', 'hat'] - 同样适用于 iLike 和 notLike
[Op.overlap]: [1, 2] // && [1, 2] (PG数组重叠运算符)
[Op.contains]: [1, 2] // @> [1, 2] (PG数组包含运算符)
[Op.contained]: [1, 2] // <@ [1, 2] (PG数组包含于运算符)
[Op.any]: [2,3] // 任何数组[2, 3]::INTEGER (仅限PG)
[Op.col]: 'user.id' // 使用数据库语言特定的列标识符,其实就是子表对应的列
- 只想返回一些字段的结果,比如说一条数据里面包含了 username, password, id, create_at,但是我并不想返回 password 返回给他,这里可以通过 attributes 字段来确认哪些字段返回,或者排除掉哪些字段返回
// 包含
async list() {
let Op = this.app.Sequelize.Op;
let res = await this.ctx.model.findAndCountAll({
where: {
[Op.like]: "%测试%"
},
// 这个情况是只返回数据里面有 username 和 id 的
attributes: ["username", "id"] // [{username: '阿达', id: 1}]
})
}
// 排除掉
async list() {
let Op = this.app.Sequelize.Op;
let res = await this.ctx.model.findAndCountAll({
where: {
[Op.like]: "%测试%"
},
// 这个情况是排除掉这些字段,只返回剩下来的字段
attributes: {
exclude: ["password", "created_at", "update_at"]
} // [{username: '阿达', id: 1}]
})
}
- 查询排序,可以通过给 findAndCountAll 或者 findAll 里面通过添加order配置进行排序,order 接受一个二维数组,里面的每一项数组代表着一个字段的排序,越在上面的字段排序优先级越高
async list() {
let Op = this.app.Sequelize.Op;
let res = await this.ctx.model.findAndCountAll({
where: {
[Op.like]: "%测试%"
},
// 这个情况是排除掉这些字段,只返回剩下来的字段
attributes: {
exclude: ["password", "created_at", "update_at"]
}, // [{username: '阿达', id: 1}]
order: [
["id", "DESC"]
]
})
}
// "DESC" 是倒序排序
// "ASC" 是升序排序
- 分页,这里的分页跟平时我们前端所传的字段相同,offset 代表的是偏移量(也就是第几页),limit 代表的是一页几条数据。
async list() {
let Op = this.app.Sequelize.Op;
let { page, offset, limit } = this.ctx.query
page = page && Number(page);
limit = limit? Number(limit): 5
offset = (page - 1) * limit;
let res = await this.ctx.model.findAndCountAll({
where: {
[Op.like]: "%测试%"
},
attributes: {
exclude: ["password", "created_at", "update_at"]
},
order: [
["id", "DESC"]
],
offset,
limit
})
}
- 改。修改某一条数据的话,得先通过findByPk,findOne去查找到对应的数据
- 通过调用查找到的数据 .update() 方法可以进行更新,代码如下
async update() {
const { ctx } = this;
// 通过路径的后缀去获取对应的 id 类似于 http://xxx.com/api/update/1
let id = ctx.params.id;
// 获取请求体中的数据,也就是前端部分所传过来的数据
let params = ctx.request.body;
let data = await ctx.model.User.findByPk(id); // 这里直接通过主键调用,也可以通过findOne
let res = await data.update(params); // 将前端传过来的数据进行更新
/*
* 可以给 update 传第二个参数, fields代表哪些字段可以被修改
* 这里给 fields 传入 username 字段,代表 username 字段可以被修改,其他都不行
* data.update(params, { fields: ["username"] })
*/
ctx.body = {
msg: '200',
data: res
}
}
- 查找到数据以后,得手动修改这条数据里面的值,在通过 save() 的形式去触发更新,但这个方式很麻烦也很不优雅,代码如下
async update() {
const { ctx } = this;
let id = ctx.params.id;
let data = ctx.model.User.findByPk(id);
// 通过这种手动赋值的方式来修改数据,一个字段还好,那要是多个字段或者不确定个数的字段的话,就很不优雅
data.username = "测试01";
let res = await data.save();
/*
* 跟上面一样 但是 save 的 fields 有个区别 如果我在代码中还修改了 password 的话,save 的返回值是被修改后的 password,但是在数据库里面,他并没有被修改
* data.save({ fields: ["username"] })
*/
ctx.body = {
msg: "200",
data: res
}
}
- 删
- 单独删除一条数据,如果是只删一条数据的话,跟修改一样,得先通过查找对应的数据,然后在通过 destroy 的方法去删除
async destroy() {
const { ctx } = this;
let id = ctx.params.id;
let data = await ctx.model.User.findByPk(id);
if (!data) {
// 如果没有对应的数据的话处理
}
let res = await data.destroy();
ctx.body = {
msg: '200',
data: res
}
}
- 多条数据删除的话,其实也是调用 destroy 方法,但是不会再去查找了,而是通过 where 语句,用 sequelize 提供的Op属性来进行多个删除
async destroy() {
const { ctx } = this;
let Op = this.app.Sequelize.Op;
let res = ctx.model.User.destroy({
where: {
id: {
[Op.between]: [1, 10] // 这个是选中在1-10这个区间的所有数据
//[Op.in]: [1, 10] 这个是选中1和10的数据
}
}
})
}
修改器
修改器名字听着高大上,其实就是在数据模型里面在某个字段里面去设置对应值的 set 属性,修改器中修改数据通常都是最后调用 setDataValue 去修改数据。
password: {
type: STRING(200),
allowNull: false,
defaultValue: '',
// 这玩意就是修改器
set(val) {
let hash = val + Math.floor(Math.random() * 100);
this.setDataValue('password', hash);
}
},
获取器
获取器其实也是修改数据模型中某个字段对应的 get 属性,获取器最后如果要修改值的话,那她最后要调用 getDataValue 去获取值,然后把值给 return 出去
created_at: {
type: DATE,
get() {
// 就是通过 getDataValue() 去获取对应字段的值
let val = this.getDataValue('created_at')
return (new Date(val)).getTime();
}
},
获取器和修改器的区别
- 获取器并不影响原本数据库里面的数据,修改器会影响数据库里面的数据。
- 获取器一般是查的时候用,修改器一般是更新,新增的时候会用。
统一的错误处理
在 egg 里面可以通过在 app 目录下面创建一个 middleware 目录,这个目录主要是放置一些中间件的,我们可以把错误处理放在这里面,并且如果在 middleware 目录下创建完实例以后,还得去 config/config.default.js 文件中去进行配置。代码如下
// middleware/error_handler.js
module.exports = () => {
return async function errorHandler(ctx, next) {
try {
await next();
} catch(e) {
ctx.status = e.status;
ctx.body = {
msg: e.status,
data: e.message
}
}
}
}
// config/config.default.js
config.middleware = ["errorHandler"]
经过以上两个文件的配置就可以捕捉到错误了
中间件的配置
在上面的统一错误处理中我们用到了中间件,如果我只想匹配一段路由,那么这个时候就涉及到了中间件的配置。
config.errorHandler = {
enable: true, // 是否开启这个中间件,默认为 true 开启
match: '/user/list', // 匹配这段路由 match 接受数组,字符串和函数,数组的话就是在数组中的这几段路由都是符合条件的,函数的话可以通过正则去校验路径
ignore: 'user/create' // 除了这个路径 其他都匹配
}
参数验证
下载一个插件 egg-valparams,命令行是 npm i egg-valparams --save,安装完成以后得分别在 config/plugin.js 和 config/config.default.js 文件中去修改配置
// config/plugin.js
valparams: {
enable : true,
package: 'egg-valparams'
}
// config/config.default.js
config.valparams = {
locale: 'zh-cn',
throwError: true
};
// 在代码中去用
async create() {
const { ctx } = this;
let params = ctx.request.body;
ctx.validate({
username: {
type: "string", //类型
required: true, // 是否必填
desc: "用户名称", // 描述
defValue: "" // 默认值
}
})
let res = ctx.model.User.create(params);
ctx.body = {
msg: "200",
data: res
}
}
自此一个基本的 egg 项目搭建成功!!!