Egg的主要特点是:奉行『约定优于配置』,按照一套统一的约定进行应用开发。
Egg 在 Koa 的基础上进行增强最重要的就是基于一定的约定,根据功能差异将代码放到不同的目录下管理。也就是说,目录和文件一定要按照规定来,不能自己随心所欲地设定。
但是,很多设定官方文档没有阐述得很详细,遇到问题的时候,需要去网上搜索,或者github上下载源码看。
Loader 实现了这套约定,并抽象了很多底层 API 可以进一步扩展。
具体见官网的说明:
https://eggjs.org/zh-cn/advanced/loader.html
官网上,可以找到比较基础的说明。我觉得,如果想快速入门:
(1) 可以先写一个Demo,知道怎么跑起来的
(2) 直接跟项目,着手解决项目问题。
▪ 快速初始化
• 我们推荐直接使用脚手架,只需几条简单指令,即可快速生成项目(npm >=6.1.0):
> mkdir egg-example && cd egg-example> npm init egg --type=simple> npm i
• 启动项目:
> npm run dev
▪ 简易项目
目的:从Mysql获取数据,对外提供restful服务
• 通过 npm 初始化一个项目
> mkdir egg-demo && cd egg-demo> npm init egg> npm i
• 安装egg-sequelize
用来操作mysql数据库
> npm install --save egg-sequelize mysql2
• 在 config/plugin.js 中引入 egg-sequelize 插件
exports.sequelize = {
enable: true,package: 'egg-sequelize',
};exports.security = {
enable: false
}
• 在 config/config.default.js 中编写 sequelize 配置
当然,DEV,PRD环境的数据库连接不同,可以不写在config.default.js中,开发时写在config.local.js,测试时写在config.unittest.js,prd写在config.prod.js。
config.sequelize = {
dialect: 'mysql', // support: mysql, mariadb, postgres, mssql
database: 'demo',
host: '192.168.xxx.xx',
port: 3306,
username: 'xxx',
password: 'xxx',
};
• 编写 Model
即与数据库关联的类,app/model/user.js。
module.exports = app => {const { STRING, INTEGER, DATE } = app.Sequelize;const User = app.model.define('users', {
id: { type: INTEGER, primaryKey: true, autoIncrement: true },
name: STRING(30),
age: INTEGER,
createdAt: DATE,
updatedAt: DATE,
});return User;
};
• 编写 Controller
即CRUD的数据访问,app/controller/users.js
const Controller = require('egg').Controller;function toInt(str) {if (typeof str === 'number') return str;if (!str) return str;return parseInt(str, 10) || 0;
}class UserController extends Controller {async index() {const ctx = this.ctx;const query = { limit: toInt(ctx.query.limit), offset: toInt(ctx.query.offset) };
ctx.body = await ctx.model.User.findAll(query);
}async show() {const ctx = this.ctx;
ctx.body = await ctx.model.User.findByPk(toInt(ctx.params.id));
}async create() {const ctx = this.ctx;const { name, age } = ctx.request.body;const user = await ctx.model.User.create({ name, age });
ctx.status = 201;
ctx.body = user;
}async update() {const ctx = this.ctx;const id = toInt(ctx.params.id);const user = await ctx.model.User.findByPk(id);if (!user) {
ctx.status = 404;return;
}const { name, age } = ctx.request.body;await user.update({ name, age });
ctx.body = user;
}async destroy() {const ctx = this.ctx;const id = toInt(ctx.params.id);const user = await ctx.model.User.findByPk(id);if (!user) {
ctx.status = 404;return;
}await user.destroy();
ctx.status = 200;
}
}
module.exports = UserController;
• 编写 router
即给外部调用的restful接口,app/router.js。
module.exports = app => {const { router, controller } = app;
router.get('/', controller.home.index);
router.resources('user', '/user', controller.user);
};
controller.home.index即访问controller目录下的home.js文件,里面的index()方法。
这里的resources有点特殊,包括CRUD。
• 创建数据库
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';
• 插入数据
INSERT INTO users (id, name, age, created_at, updated_at) VALUES
(1, '张三', 18, '2020-10-21 23:29:39', '2020-10-21 23:29:39'),
(2, '李四', 19, '2020-10-21 23:29:39', '2020-10-21 23:29:39');
· 通过以上的设定,整个项目就完成了,运行:
> npm run dev
· postman 访问
get http://127.0.0.1:7001/user?limit=5&offset=2
▪ 实际项目
以下是一个项目的结构,主要通过查询mysql数据,对外提供restful服务。
• 配置文件
· dev 环境加载一个配置的加载顺序如下,后加载的会覆盖前面的同名配置。
-> config.default.js
-> config.local.js
另外,config.local.js 中可以引入 config.mysql.js
const sequelizeConfig = require('./config.mysql')
· prod 环境加载一个配置的加载顺序如下。
-> config.default.js
-> config.prod.js
• 插件
· 例如:开启egg-sequelize,实现对mysql的操作
exports.sequelize = {
enable: true,package: 'egg-sequelize'
}
· 例如:开启egg-redis 插件
exports.redis = {
enable: true,package: 'egg-redis',
}
• Router (路由)
主要用来描述请求 URL 和具体承担执行动作的 Controller 的对应关系, 框架约定了 app/router.js 文件用于统一所有路由规则。
• 控制器(Controller)
我们通过 Router 将用户的请求基于 method 和 URL 分发到了对应的 Controller 上,负责解析用户的输入,处理后返回相应的结果:
(1) 获取用户通过 HTTP 传递过来的请求参数。
(2) 校验、组装参数。
(3) 调用 Service 进行业务处理,必要时处理转换 Service 的返回结果,让它适应用户的需求。
(4) 通过 HTTP 将结果响应给用户。
• 服务(Service)
简单来说,Service 就是在复杂业务场景下用于做业务逻辑封装的一个抽象层,提供这个抽象有以下几个好处:
(1) 保持 Controller 中的逻辑更加简洁。
(2) 保持业务逻辑的独立性,抽象出来的 Service 可以被多个 Controller 重复调用。
(3) 将逻辑和展现分离,更容易编写测试用例,测试用例的编写具体可以查看这里。
可以看到,SQL一般都在service中书写。
• 中间件(Middleware)
Egg 的中间件形式和 Koa 的中间件形式是一样的,都是基于洋葱圈模型。每次我们编写一个中间件,就相当于在洋葱外面包了一层。
例如:指定只有当 body 大于配置的 threshold 时才进行 gzip 压缩,我们要在 app/middleware 目录下新建一个文件 gzip.js,
// app/middleware/gzip.js
const isJSON = require('koa-is-json');const zlib = require('zlib');module.exports = options => {return async function gzip(ctx, next) {await next();// 后续中间件执行完成后将响应体转换成 gziplet body = ctx.body;if (!body) return;// 支持 options.thresholdif (options.threshold && ctx.length < options.threshold) return;if (isJSON(body)) body = JSON.stringify(body);// 设置 gzip body,修正响应头const stream = zlib.createGzip();
stream.end(body);
ctx.body = stream;
ctx.set('Content-Encoding', 'gzip');
};
};
· 全局使用中间件
中间件编写完成后,我们还需要手动挂载,在应用中使用中间件,需要加载上面的 gzip 中间件,在 config.default.js 中加入下面的配置就完成了中间件的开启和配置:
module.exports = {// 配置需要的中间件,数组顺序即为中间件的加载顺序
middleware: [ 'gzip' ],// 配置 gzip 中间件的配置
gzip: {
threshold: 1024, // 小于 1k 的响应体不压缩
},
};
· router 中使用中间件
以上方式配置的中间件是全局的,会处理每一次请求。如果你只想针对单个路由生效,可以直接在 app/router.js 中实例化和挂载,如下:
module.exports = app => {const gzip = app.middleware.gzip({ threshold: 1024 });
app.router.get('/needgzip', gzip, app.controller.handler);
};
• 定时任务
所有的定时任务都统一存放在 app/schedule 目录下,每一个文件都是一个独立的定时任务,可以配置定时任务的属性和要执行的方法。
定时任务可以指定 interval 或者 cron 两种不同的定时方式。
· interval
通过 schedule.interval 参数来配置定时任务的执行时机,定时任务将会每间隔指定的时间执行一次。
module.exports = {
schedule: {// 每 10 秒执行一次
interval: '10s',
},
};
· cron
通过 schedule.cron 参数来配置定时任务的执行时机,定时任务将会按照 cron 表达式在特定的时间点执行。
module.exports = {
schedule: {// 每三小时准点执行一次
cron: '0 0 */3 * * *',
},
};
* * * * * *
┬ ┬ ┬ ┬ ┬ ┬
│ │ │ │ │ |
│ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun)
│ │ │ │ └───── month (1 - 12)
│ │ │ └────────── day of month (1 - 31)
│ │ └─────────────── hour (0 - 23)
│ └──────────────────── minute (0 - 59)
└───────────────────────── second (0 - 59, optional)
如上所述,egg.js的大概开发流程就是这样,比较简单,效率的较高,具体的操作,大家可找官网的资料,或者github上的案例。