简介:
egg 的定位是企业级 web 基础框架,基于koa封装,旨在帮助开发者孕育适合自己团队的框架。
特点:
- 提供基于 Egg 定制上层框架的能力
- 高度可扩展的插件机制
- 内置多进程管理
- 基于 Koa 开发,性能优异
- 框架稳定,测试覆盖率高
- 渐进式开发
1.初始化项目
要求: node >= 8 && npm >= 6.1.0
1 初始化项目
npm init
npm i egg egg-bin -D
2 初始化配置文件
// package.json
// 添加npm到package.json
{
"name": "egg-example",
"scripts": {
"dev": "egg-bin dev"
}
}
3.相关的内置对象及目录结构
2. Controller和Router
Controller
作用: Controller主要负责解析用户的输入,处理后返回相应的结果
所有的Controller文件都必须在app/controller目录下访问时可通过目录名级联访问。一般支持两种编写方式类(推荐) 和 方法
下面主要介绍类写法
// app/controller/home.js
const Controller = require('egg').Controller;
class HomeController extends Controller {
async index() {
this.ctx.body = 'Hello world!!!';
}
}
module.exports = HomeController
详解: Controller类继承于egg.Controller。其自带以下几个属性挂在this上:
- this.ctx: 请求的上下文Context对象的实例。
- this.app: Application对象的实例。
- this.service: 可以访问抽象出的业务层,等价于this.ctx.service。
- this.config: 应用运行时的配置项。
- this.logger: 可以打印出相关日志,与context logger类似,其还会加上打印日志的文件路径。
基类:可以通过按照类的方式再封装一层,内置一些常用的方法将其作为基类,后续的controller类继承该基类进行操作即可复用其内部的方法。
其中内部的一些操作像获取query/body、设置响应信息等请查看egg-controller文档
Router
作用: 主要用来描述请求URL和具体承担执行动作的Controller的对应关系。
框架规定了app/router.js文件用于统一所有路径规则。
// app/router.js
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
};
详解: router主要由以下五部分组成:
- verb: 用户出发动作(get/put/post/pach/redirect等)
- router-name(可选): 路由设定的别名,可通过Helper的辅助函数生产URL
- path-match: 路由URL路径
- middleware(可选): 可配置多个middleware(后面有介绍)。
- controller: 指定具体的controller。
更多详细用法可参考egg-router文档
解析: 通过结合上面的control文件,可以根据其目录名级联关系(home.js)来访问里面定义的index方法。这时候已经完成了最基本的egg编写,可以运行并查看结果
// 运行egg项目
npm run dev
打开浏览器http://localhost:7001即可查看效果。
3.中间件
因为koa是基于koa实现的,所有中间件形式和koa的中间件形式是一样的,都属于洋葱模型。
// app/middleware/robot.js
// options === app.config.robot
// 该中间件用于禁用百度爬虫
module.exports = (options, app) => {
return async function robotMiddleware(ctx, next) {
const source = ctx.get('user-agent') || '';
const match = options.ua.some(ua => ua.test(source));
if (match) {
ctx.status = 403;
ctx.message = 'Go away, robot.';
} else {
await next();
}
}
};
// config/config.default.js
// 注册中间件,决定middleware的顺序
exports.middleware = [
'robot'
];
// robot's configurations
exports.robot = {
ua: [
/Baiduspider/i
]
};
我们约定一个中间件一般放置在app/middleware目录下的单独文件,需要export一个普通的function,接受两个参数。
参数:
options: 中间件的配置项,框架会将 app.config[${middlewareName}] 传递进来。
app: 当前应用 Application 的实例。
全局中间件: 需要在config.default.js中加入配置。如在框架或插件中不支持config中配置,则可以通过app.config.coreMiddleware.unshift(中间件名)来添加中间件。
路由中使用: 路由中可以单独实例化并挂载,操作如下:
// app/router.js
module.export = app => {
const robot = app.middleware.root({ua:[/Baiduspider/i]});
app.router.get('/news', robot, app.controller.handler);
}
默认中间件: 一般框架自身会带有默认中间件,我们可以在config.default.js中通过其名称修改其中间件的配置。但是不可妄想配置应用层同名中间件来覆盖其默认中间件。因为:框架和插件的默认中间件会在应用层配置的中间件之前,不会被应用层中间件覆盖,如果应用层有自定义同名中间件,在启动时会报错。
通用配置: 可以通过配置来决定是否启用中间件,包括禁用默认中间件操作等。配置参数包括以下三个:
- enable: 控制中间件是否开启。
- match:设置只有符合某些规则的请求才会经过该中间件。
- ignore:设置符合某些规则不经过中间件。
match和ignore均支持字符串(以配置字符串为前缀的url都会匹配上,也支持字符串数组)、正则(匹配url路径)、函数(ctx作为参数,返回值判断),match和ignore不能同时配置。
4.插件
插件和中间件:
- 中间件加载有先后顺序,但是中间件自身无法管理这顺序。
- 中间件定位是拦截用户请求,并在它前后做一些事,更多一些逻辑处理等需要插件完成。
- 一些非常复杂的初始化逻辑,需在启动时完成,这显然不合适放到中间件去实现。
1.插件安装
// 安装
npm i egg-mysql --save
或在package.json配置
{
"dependencies": {
"egg-mysql": "^3.0.0"
}
}
注意:我们建议通过 ^ 的方式引入依赖,并且强烈不建议锁定版本。
2. 插件声明
// 在config/plugin.js中生命
exports.nunjucks = {
enable: true,
package: 'egg-view-nunjucks'
};
参数:
- {Boolean} enable: 是否开启插件,默认为true
- {String} page: 模块名称,
- {String} path: 插件的绝对路径
- {Array} env: 需要在指定环境中开启,会覆盖插件自身package.json中的配置
- 插件使用及配置
插件一般包含自己默认配置,也可在config.default.js覆盖原来的配置。
// config/config.default.js
exports.mysql = {
client: {
host: 'mysql.com',
port: '3306',
user: 'test_user',
password: 'test_password',
database: 'test',
},
};
app.mysql.query(sql, values);
5.模版渲染
这类主要用到的是Nunjucks来渲染。详情可参考Nunjucks文档
安装和配置
// 安装
npm i egg-view-nunjucks --save
// 启动插件
// config/plugin.js
exports.nunjucks = {
enable: true,
package: 'egg-view-nunjucks'
};
// 添加默认配置
// config/config.default.js
exports.view = {
defaultViewEngine: 'nunjucks',
mapping: {
'.tpl': 'nunjucks',
},
};
后续详情可参考egg模版解析
6.service
作用: Controller一般都不会产出数据,复杂的处理过程应抽象为业务逻辑层Service。
优点:
1.使controller中逻辑更简洁。
2.代码独立性和复用性。
3.逻辑分离,更容易编写测试用例。
4.同时Service是懒加载的,只有当访问它的时候框架才会去实例化它。
// app/service/news.js
const Service = require('egg').Service;
class NewsService extends Service {
async list(page = 1) {
const newList = []
// 逻辑操作
return newsList;
}
}
module.exports = NewsService;
// app/controller/news.js
...
const newsList = ctx.service.news.list(3)
...
将复杂的逻辑放到service里,controller可通过service[文件名]获取到相应的属性或方法,和router获取controller雷同。
7.框架扩展
开发中我们可以扩展已有的API的熟悉来方便开发
- Application
- Context
- Request
- Response
- Helper
方法扩展
// app/extend/application.js
module.exports = {
foo(param) {
// this 就是 app 对象,在其中可以调用 app 上的其他方法,或访问属性
},
};
属性扩展
// app/extend/application.js
const BAR = Symbol('Application#bar');
module.exports = {
get bar() {
// this 就是 app 对象,在其中可以调用 app 上的其他方法,或访问属性
if (!this[BAR]) {
// 实际情况肯定更复杂
this[BAR] = this.config.xx + this.config.yy;
}
return this[BAR];
},
};
8.启动自定义
框架提供了统一的入口文件(app.js)进行启动过程自定了,以及提供了一些生命周期钩子供开发人员处理。
- configWillLoad: 皮质文件即将加载,这是最后动态修改配置的时机
- configDidLoad: 配置文件加载完成
- didLoad: 文件加载完成
- willReady: 插件启动完毕
- didReady: worker 准备就绪
- serverDidReady: 应用启动完成
- beforeClose: 应用即将关闭
// app.js
class AppBootHook {
constructor(app) {
this.app = app;
}
async serverDidReady() {
// http / https server 已启动,开始接受外部请求
// 此时可以从 app.server 拿到 server 的实例
this.app.server.on('timeout', socket => {
// handle socket timeout
});
}
}
module.exports = AppBootHook;