一、Egg.js的行业背景
1.1 Egg.js是什么
Egg.js 为企业级框架和应用而生,Egg.js可以孕育出更多上层框架,帮助开发团队和开发人员降低开发和维护成本。
2.2 与社区框架有哪些差异
Express 是 Node.js 社区广泛使用的框架,简单且扩展性强,非常适合做个人项目。但框架本身缺少约定,标准的 MVC 模型会有各种千奇百怪的写法。Egg 按照约定进行开发,奉行『约定优于配置』,团队协作成本低。
Sails 是和 Egg 一样奉行『约定优于配置』的框架,扩展性也非常好。但是相比 Egg,Sails 支持 Blueprint REST API、WaterLine 这样可扩展的 ORM、前端集成、WebSocket 等,但这些功能都是由 Sails 提供的。而 Egg 不直接提供功能,只是集成各种功能插件,比如实现 egg-blueprint,egg-waterline 等这样的插件,再使用 sails-egg 框架整合这些插件就可以替代 Sails 了。
2.3 Egg和Koa之间的关系
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。采用的中间件是洋葱模型:
2.3.1 Egg 继承于 Koa
Koa 是一个非常优秀的框架,然而对于企业级应用来说,它还比较基础。 Egg 选择了 Koa 作为其基础框架,在它的模型基础上,进一步对它进行了一些增强。扩展和插件更为完善和便捷。
下面就一起快速学习Egg.js吧!
项目git地址: github.com/liujun8892/…
二、初识Egg.js
2.1 Egg.js的安装、配置、项目基本结构
2.1.1 安装
$ mkdir egg-example && cd egg-example
$ npm init egg --type=simple
$ npm i
$ npm run dev
$ open http://localhost:7001
- 一个Egg项目就已经初始化好啦,可以跑起来了:
2.1.2 项目目录结构
egg-project
├── package.json
├── app.js (可选)
├── agent.js (可选)
├── app
| ├── router.js
│ ├── controller
│ | └── home.js
│ ├── service (可选)
│ | └── user.js
│ ├── middleware (可选)
│ | └── response_time.js
│ ├── schedule (可选)
│ | └── my_task.js
│ ├── public (可选)
│ | └── reset.css
│ ├── view (可选)
│ | └── home.tpl
│ └── extend (可选)
│ ├── helper.js (可选)
│ ├── request.js (可选)
│ ├── response.js (可选)
│ ├── context.js (可选)
│ ├── application.js (可选)
│ └── agent.js (可选)
├── config
| ├── plugin.js
| ├── config.default.js
│ ├── config.prod.js
| ├── config.test.js (可选)
| ├── config.local.js (可选)
| └── config.unittest.js (可选)
└── test
├── middleware
| └── response_time.test.js
└── controller
└── home.test.js
2.1.3 内置的ctx对象
- ctx这个对象非常重要,请求来的参数,返回去的消息都需要通过ctx这个对象获取或者设置
- ctx是继承koa的context对象通过设置ctx.body, 可以改变返回信息
2.1.4. 尝试新写一个路由
在controller中
async list() {
const { ctx } = this;
ctx.body = {
code: 200,
data: [
{
id: '1',
name: '张三',
},
{
id: '2',
name: '李四',
},
{
id: '3',
name: '王五',
},
],
msg: '操作成功',
};
}
在router.js中匹配新写的路由
router.get('/list', controller.home.list);
效果:
- 一个新的路由就已经创建好啦
2.2 Egg.js的控制层、params和query传参、返回状态码设置
工欲善其事,必先利其器。为了使用egg的快捷键和代码提示,推荐使用vscode的egg插件。
2.2.1 控制层controller
配置新controller的路由
// 用户controller
router.get('/user/info', controller.user.info);
2.2.2 路由params传参方式
在router中通过 :id,接受参数
router.get('/user/findById/:id', controller.user.info);
在controller中,通过params拿参数
async findById() {
const { ctx, params } = this;
const userId = params;
const userlist = [
{
id: '1',
name: '张三',
},
{
id: '2',
name: '李四',
},
{
id: '3',
name: '王五',
},
];
const result = userlist.find(v => v.id === userId);
ctx.body = {
code: 200,
data: result,
msg: '操作成功',
};
}
2.2.3 query的形式传参
async findById2() {
const { ctx } = this;
const userId = ctx.query.id;
const userlist = [
{
id: '1',
name: '张三',
},
{
id: '2',
name: '李四',
},
{
id: '3',
name: '王五',
},
];
const result = userlist.find(v => v.id === userId);
ctx.body = {
code: 200,
data: result,
msg: '操作成功',
};
}
2.2.4 修改返回的状态码
2.3、post请求
- 实际业务中,为了接口请求的安全性,和多数据请求参数,多类型请求方式,附件,图片上传等多采用post请求。
- 下面就介绍如何在Egg中使用post请求
2.3.1 配置csrf跨域相关配置
安装跨域插件
npm i egg-cors --save
配置config下的plugin.js
'use strict';
/** @type Egg.EggPlugin */
module.exports = {
// had enabled by egg
// static: {
// enable: true,
// }
cors: {
enable: true,
package: 'egg-cors',
},
};
配置config下的config.default.js
// 关闭crsf,开启跨域
config.security = {
csrf: {
enable: false,
},
domainWhiteList: [ ],
};
// 允许跨域方法
config.cors = {
origin: '*',
allowMethods: 'GET, PUT, POST, DELETE, PATCH',
};
测试,创建一个post请求
async createUser() {
const { ctx } = this;
console.log(ctx.request.body, 'bodyield...1');
const result = {
username: ctx.request.body.username,
age: ctx.request.body.age,
};
ctx.body = {
code: 200,
data: result,
msg: '操作成功',
};
}
- 至此,post请求的相关环境就已经准备好了
2.4 egg.js 的路由分组
- 当们写很多controller,需要配置的路由很多,这时在一个文件中写所有的路由文件,这会使路由文件变得十分冗余,不易阅读。这时候需要我们把路由分模块划分,在总路由中再去引入。
2.4.1 路由分组
在新建一个router目录,按模块抽离路由
让后在router.js中通过require去引入
三、再识Egg.js
3.1 配置MySQL数据库
- 之前的文章都是静态的数据,要完成持久化的真实数据,就需要用到数据库,这里我们使用的是工作中使用最频繁的数据库MySQL,通过 egg-sequelize插件,就可以像操作一些对象属性一样,完成业务的增删改查,语义化的操作,而不用手动的去写SQL。
3.2.数据库迁移
3.2.1 前期准备
安装并配置egg-sequelize插件(它会辅助我们将定义好的 Model 对象加载到 app 和 ctx 上)和mysql2模块:
npm install --save egg-sequelize mysql2
3.2.2. 在config/plugin.js
中引入 egg-sequelize 插件
exports.sequelize = {
enable: true,
package: 'egg-sequelize',
};
3.2.3. 在config/config.default.js
config.sequelize = {
dialect: 'mysql',
host: '127.0.0.1',
username: 'root',
password: 'root',
port: 3306,
database: 'eggapi',
// 中国时区
timezone: '+08:00',
define: {
// 取消数据表名复数
freezeTableName: true,
// 自动写入时间戳 created_at updated_at
timestamps: true,
// 字段生成软删除时间戳 deleted_at
paranoid: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
deletedAt: 'deleted_at',
// 所有驼峰命名格式化
underscored: true
}
};
3.2.4. sequelize 提供了sequelize-cli工具来实现Migrations,我们也可以在 egg 项目中引入 sequelize-cli。
npm install --save-dev sequelize-cli
3.2.5. 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'),
};
3.2.6. 初始化 Migrations 配置文件和目录
ps: 注意准备php集成环境,windows推荐wamp,mac推荐mamp
windows: 集成环境下载:upupw.net
npx sequelize init:config
npx sequelize init:migrations
// npx sequelize init:models
3.2.7. 行完后会生成database/config.json
文件和database/migrations
目录,我们修改一下database/config.json
中的内容,将其改成我们项目中使用的数据库配置:
{
"development": {
"username": "root",
"password": "root",
"port": "8889"
"database": "eggapi",
"host": "127.0.0.1",
"dialect": "mysql",
"timezone": "+08:00"
}
}
3.2.8. 创建数据库
npx sequelize db:create
数据库创建成功
3.3 创建数据表、回滚数据
3.3.1 创建数据迁移表
npx sequelize migration:generate --name=init-user
1.执行完命令后,会在database / migrations / 目录下生成数据表迁移文件,然后定义
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
const { INTEGER, STRING, DATE, ENUM } = Sequelize;
// 创建表
await queryInterface.createTable('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: true, defaultValue: '' },
sex: { type: ENUM, values: ['男','女','保密'], allowNull: true, defaultValue: '男', comment: '用户性别'},
created_at: DATE,
updated_at: DATE
});
},
down: async queryInterface => {
await queryInterface.dropTable('user')
}
};
3.3.2 执行 migrate 进行数据库变更
# 升级数据库
npx sequelize db:migrate
# 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更
# npx sequelize db:migrate:undo
# 可以通过 `db:migrate:undo:all` 回退到初始状态
# npx sequelize db:migrate:undo:all
四、Egg.js实操
- 上面的三大步,已经完成了从controller到数据,再返回的基本架子了,平常在工作中,用到Egg.js的就是增、删、改、查。下面我们对每一部分,详细的实操:
4.1 新增相关接口和栗子🌰
4.1.1 新增一个用户
// app/controller/user.js
async createUser() {
const { ctx } = this;
const { username, password } = ctx.request.body;
const result = await ctx.model.User.create({ username, password });
ctx.body = {
code: 200,
data: result,
msg: '操作成功',
};
}
数据库中插入一条新用户数据
4.1.2 注册多个用户
// 创建多个用户
async createUserList() {
const { ctx } = this;
const { data } = ctx.request.body;
const result = await ctx.model.User.bulkCreate(data);
console.log(data, '1111');
ctx.body = {
code: 200,
data: result,
msg: '操作成功',
};
}
4.1.3 巧用set方法实现加密储存密码
- 在模型中利用set方法将密码改成加密密码 安装md5
npm install md5
修改model/user.js模型, 使用set方法
'use strict';
const md5 = require('md5');
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: '',
set(value) {
const enCrpty = md5(value + 'secret');
this.setDataValue('password', enCrpty);
},
},
avatar_url: { type: STRING(200), allowNull: true, defaultValue: '' },
sex: { type: ENUM, values: [ '男', '女', '保密' ], allowNull: true, defaultValue: '男', comment: '用户性别' },
created_at: DATE,
updated_at: DATE,
}, {
timestamps: true, // 是否自动写入时间戳
tableName: 'user', // 自定义数据表名称
});
return User;
};
后面的密码就是加密过了的了
// 查
4.2 查询相关的接口和栗子🌰
查询基本是一些详情和列表的业务,业务不同,查的方式也多种多样。
4.2.1 查一个findByPk方法
// 查单个
async findUserByUserId() {
const id = parseInt(this.ctx.params.id);
console.log(id);
const user = await this.ctx.model.User.findByPk(id);
if (!user) {
this.ctx.body = {
code: 200,
data: null,
msg: '操作成功',
};
return;
}
this.ctx.body = {
code: 200,
data: user,
msg: '操作成功',
};
}
4.2.2 where条件查询
- 条件查单个findOne方法,条件写在where里
// findOne条件查
async findUserByCondition() {
const user = await this.ctx.model.User.findOne({
where: {
id: 2,
sex: '女',
},
});
if (!user) {
this.ctx.body = {
code: 200,
data: null,
msg: '操作成功',
};
return;
}
this.ctx.body = {
code: 200,
data: user,
msg: '操作成功',
};
}
4.2.3 列表查询
- 查多个,使用findAll方法
// 查询多个
async findAll() {
const result = await this.ctx.model.User.findAll();
if (!result) {
this.ctx.body = {
code: 200,
data: null,
msg: '操作成功',
};
return;
}
this.ctx.body = {
code: 200,
data: result,
msg: '操作成功',
};
}
4.2.4 列表并计数查询
- 查多个,并计数,使用findAndCountAll
// 查询多个并计数
async findAndCountAll() {
const result = await this.ctx.model.User.findAndCountAll();
if (!result) {
this.ctx.body = {
code: 200,
data: null,
msg: '操作成功',
};
return;
}
this.ctx.body = {
code: 200,
data: result,
msg: '操作成功',
};
}
4.2.5 巧用get方法二次加工查询出来的值
- get方法修改查询出来的值
// model/user.js
created_at: {
type: DATE,
get() {
const val = this.getDataValue('created_at');
return 'zh' + val;
},
},
4.2.6 findAll 中where的使用
// 条件查多个
async findAllCondition() {
const Op = this.app.Sequelize.Op;
const result = await this.ctx.model.User.findAll({
where: {
// id大于3
id: {
[Op.gt]: 3,
},
// 只查男
sex: '男',
// 模糊搜用户名
username: {
[Op.like]: '%3%',
},
},
});
if (!result) {
this.ctx.body = {
code: 200,
data: null,
msg: '操作成功',
};
return;
}
this.ctx.body = {
code: 200,
data: result,
msg: '操作成功',
};
}
where还有以下常用的条件
where: {
id: {
[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 < 10
[Op.lte]: 10, // id <= 10
[Op.ne]: 20, // id != 20
[Op.between]: [6, 10], // 在 6 和 10 之间
[Op.notBetween]: [11, 15], // 不在 11 和 15 之间
[Op.in]: [1, 2], // 在 [1, 2] 之中
[Op.notIn]: [1, 2], // 不在 [1, 2] 之中
[Op.like]: '%hat', // 包含 '%hat'
[Op.notLike]: '%hat', // 不包含 '%hat'
[Op.iLike]: '%hat', // 包含 '%hat' (不区分大小写) (仅限 PG)
[Op.notILike]: '%hat', // 不包含 '%hat' (仅限 PG)
[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)
},
status: {
[Op.not]: false, // status 不为 FALSE
}
4.2.7 只查询某些需要的字段
- 限制字段 attributes
// 限制字段
async findAllLimtColumn() {
const result = await this.ctx.model.User.findAll({
attributes: [ 'id', 'username', 'password' ],
});
if (!result) {
this.ctx.body = {
code: 200,
data: null,
msg: '操作成功',
};
return;
}
this.ctx.body = {
code: 200,
data: result,
msg: '操作成功',
};
}
- 排除某些字段其他全部查 attributes/exclude
const result = await this.ctx.model.User.findAll({
attributes: {
exclude: [ 'password' ],
},
});
4.2.8 查询结构进行排序
- 排序 order,和attributes是同级的位置
order: [
[ 'updated_at', 'DESC' ],
[ 'id', 'DESC' ],
],
4.2.9 分页查询
- 分页查询。offset从那个开始,limit限制几条
// 分页查询
async findAllByPage() {
const { query } = this.ctx;
const limit = 5;
const offset = (query.page - 1) * limit;
const result = await this.ctx.model.User.findAll({
attributes: {
exclude: [ 'password' ],
},
order: [
[ 'updated_at', 'DESC' ],
[ 'id', 'DESC' ],
],
offset,
limit,
});
if (!result) {
this.ctx.body = {
code: 200,
data: null,
msg: '操作成功',
};
return;
}
this.ctx.body = {
code: 200,
data: result,
msg: '操作成功',
};
}
4.3 更新相关接口和栗子🌰
例如:用户修改了个人信息,这时我们需要同步更新数据库中数据
- 更新
// 更新用户
async updateUserInfo() {
const id = this.ctx.params.id ? parseInt(this.ctx.params.id) : 0;
const result = await this.ctx.model.User.findByPk(id);
if (!result) {
this.ctx.body = {
code: 200,
data: null,
msg: '未找到该用户',
};
return;
}
result.username = this.ctx.request.body.username;
const updateResult = result.save();
this.ctx.body = {
code: 200,
data: updateResult,
msg: '操作成功',
};
}
update一次可以更新多个字段,通过fileds可以限制只允许更新哪些字段,增加安全性
const updateParams = this.ctx.request.body;
const updateResult = await result.update(updateParams, {
fields: [ 'username' ],
});
4.4 删除相关接口和栗子🌰
4.4.1 删除单个
删除单个
// 删除
async destroy() {
const id = this.ctx.params.id ? parseInt(this.ctx.params.id) : 0;
const result = await this.ctx.model.User.findByPk(id);
if (!result) {
this.ctx.body = {
code: 200,
data: null,
msg: '未找到该用户',
};
return;
}
const destroyResult = await result.destroy();
this.ctx.body = {
code: 200,
data: destroyResult,
msg: '操作成功',
};
}
4.1.2 批量删除
- 批量删除 使用Op中的条件匹配,小于7的都会被删除
const Op = this.app.model.Sequelize.Op;
const destroyResult = await this.app.model.User.destroy({
where: {
id: {
[Op.lte]: 7,
},
},
});
五、Egg.js 进阶
5.1 异常处理
Egg中不对异常进行同一处理,一旦报错,就是Egg框架中返回的错误html页面,这既不友好,也不知道是为了什么报错。 异常处理
- 抛出异常
this.ctx.throw(500, '自定义出错信息');
5.2 中间件, 同一异常处理
5.2.1 先写错误处理的中间件
5.2.2 再配置中间件
5.2.3 先同一异常处理逻辑,这块可根据实际需求自由调整
module.exports = () => {
return async function errrorHanlder(ctx, next) {
try {
await next();
} catch (error) {
// 记录日志用
ctx.app.emit('error', error, ctx);
// 同一异常返回
ctx.status = error.status;
ctx.body = {
msg: 'fail',
data: error.message,
};
}
};
};
5.2.4 配置异常开关,过滤一些异常路由
// flag to enable your hanlder
config.errorHanlder = {
enable: true,
// match: '/user/findUserByUserId',
ignore: '/user/findUserByUserId',
};
5.3 参数验证
对于一些客户端的参数,有必要进行一些校验,保证了系统的安全性和数据的可靠性
// 1. 安装egg-valparams
npm i egg-valparams --save
// 2.配置
// config/plugin.js
valparams : {
enable : true,
package: 'egg-valparams'
},
// config/config.default.js
config.valparams = {
locale : 'zh-cn',
throwError: true
};
// 3.
// 创建单个用户
async createUser() {
const { ctx } = this;
ctx.validate({
username: { type: 'string', required: true, desc: '用户名' },
password: { type: 'string', required: true, desc: '密码' },
sex: { type: 'string', required: false, defValue: '男', desc: '性别' },
});
const { username, password } = ctx.request.body;
const result = await ctx.model.User.create({ username, password });
ctx.body = {
code: 200,
data: result,
msg: '操作成功',
};
}
可以看见上面的错误是英文的,我们需要中文的报错信息,去错误中间件中写一下,422状态码,特殊处理异常
if (error.status === 422) {
ctx.body = {
msg: 'fail',
data: error.message,
};
}
后记: 好啦,看到这里,首先要感谢耐心阅读的自己;不管你是把本文放进收藏夹,还是跟着撸一遍项目,相信你都有些收货吧!喜欢的朋友点赞+关注哦!下面是项目源码地址哦: git地址: github.com/liujun8892/…