前言
egg 是什么?
egg 是阿里出品的一款 node.js 后端 web 框架,基于 koa 封装,并做了一些约定。
egg 和 koa 是什么关系?
koa 是 egg 的基础框架,egg 是对 koa 的增强。
本文内容:
- 创建简单的Egg项目
- 如何搭建服务(结合目录约定来看MVC)
- 目录约定
- 服务核心设计理念(Application)
- 加载插件
- 错误处理
- 生命周期
- 框架扩展
- 定制框架
一、创建简单的Egg项目
$ npm init egg --type=simple --registry=china
# 或者
$ yarn create egg --type=simple --registry=china
创建完毕之后,目录结构基本如下:
├── app
│ ├── controller
│ │ └── home.js
│ └── router.js
├── config
│ ├── config.default.js
│ └── plugin.js
├── package.json
这就是最小化的 egg 项目。
关于启动项目,egg有提供托管工具
- 开发环境
egg-bin
类似nodemon - 生产环境
egg-script
类似pm2
二、如何搭建服务(结合目录约定来看MVC)
1. 目录约定
上面创建的项目只是最小化结构,一个典型的 egg 项目有如下目录结构:
egg-project
├── package.json
├── app.js (可选)
├── app/
| ├── router.js # 用于配置 URL 路由规则
| ├── router/ # 所有router的集合
│ ├── controller/ # 用于存放控制器(解析用户的输入、加工处理、返回结果)
│ ├── service/ (可选) # 用于编写业务逻辑层
│ ├── util/ (可选) # 集合公共工具方法
│ ├── constant/ (可选) # 集合常量以及ts变量声明
│ ├── middleware/ (可选) # 用于编写中间件
│ ├── schedule/ (可选) # 用于设置定时任务
│ ├── model/ (可选) # 用于存放数据库模型
│ ├── public/ (可选) # 用于放置静态资源
│ ├── view/ (可选) # 用于放置模板文件
│ └── extend/ (可选) # 用于框架的扩展
│ ├── helper.js (可选)
│ ├── request.js (可选)
│ ├── response.js (可选)
│ ├── context.js (可选)
│ ├── application.js (可选)
│ └── agent.js (可选)
├── config/
| ├── plugin.js # 用于配置需要加载的插件
| ├── config.{env}.js # 用于编写配置文件(env 可以是 default,prod,test ...)
├── bin/ (可选)
| ├── run.sh # 用于启动服务
│ └── stop.sh/ # 用于停止服务
├── logs/ (可选)
├── test/(可选)
├── build.sh (可选) # 构建
├── control.sh (可选) # 控制如何服务启动
├── tsconfig.json (可选)
└── .eslintrc (可选)
这是由 egg 框架或内置插件约定好的,是阿里总结出来的最佳实践,虽然框架也提供了让用户自定义目录结构的能力,但是依然建议大家采用阿里的这套MVC方案。在接下来的篇章当中,会逐一讲解上述约定目录和文件的作用。
2. 服务核心设计理念(Application)
Application
└── app/
├── router/ # 所有router的集合
├── router.js # 用于配置 URL 路由规则,定义了 请求路径(URL) 和 控制器(Controller) 之间的映射关系
├── controller/ # 用于存放控制器(解析用户的输入、加工处理、返回结果)
├── service/ (可选) # 用于编写业务逻辑层
├── util/ (可选) # 集合公共工具方法
├── constant/ (可选) # 集合常量以及ts变量声明
├── middleware/ (可选) # 用于编写中间件
├── schedule/ (可选) # 用于设置定时任务
├── model/ (可选) # 用于存放数据库模型
├── public/ (可选) # 用于放置静态资源
├── view/ (可选) # 用于放置模板文件
└── extend/ (可选) # 用于框架的扩展
├── helper.js (可选)
├── request.js (可选)
├── response.js (可选)
├── context.js (可选)
├── application.js (可选)
└── agent.js (可选)
2.1 路由(Router)
路由定义了 请求路径(URL) 和 控制器(Controller) 之间的映射关系,即用户访问的网址应交由哪个控制器进行处理。
2.1.1 配置Router
我们打开 app/router.js
看一下, 路由文件导出了一个函数,接收 app 对象作为参数
module.exports = app => {
const { router, controller } = app
router.get('/', controller.home.index)
};
通过下面的语法定义映射关系:
router.verb('匹配path', controllerAction)
其中 `verb` 一般是 HTTP 动词的小写,例如:
- HEAD - `router.head`
- OPTIONS - `router.options`
- GET - `router.get`
- PUT - `router.put`
- POST - `router.post`
- PATCH - `router.patch`
- DELETE - `router.delete` 或 `router.del`
除此之外,还有一个特殊的动词 router.redirect
表示重定向。
router.redirect('/', '/home/index', 302)
1)匹配path
主要有以下三种形式:固定path、动态path、正则;
module.exports = app => {
const { router, controller } = app
// 当用户访问 news 会交由 controller/news.js 的 index 方法进行处理
router.get('/news', controller.news.index)
// 通过冒号 `:x` 来捕获 URL 中的命名参数 x,放入 ctx.params.x
router.get('/user/:id/:name', controller.user.info)
// 通过自定义正则来捕获 URL 中的分组参数,放入 ctx.params 中
router.get(/^\/package\/([\w-.]+\/[\w-.]+)$/, controller.package.detail)
}
2)controllerAction
controllerAction
指得是通过点(·
)链式调用指定 controller
目录下某个文件内的某个具体函数 (commonjs来的),例如:
controller.home.index # 映射到 controller/home.js 文件的 index 方法
controller.v1.user.create # 映射到 controller/v1/user.js 文件的 create 方法
2.2.2 分组管理Router
当项目越来越大之后,路由映射会越来越多,我们可能希望能够将路由映射按照文件进行拆分,这个时候有两种办法:
-
手动引入,即把路由文件写到
app/router
目录下,然后再app/router.js
中引入这些文件。示例代码(推荐,方便自定义):// app/router.js module.exports = app => { require('./router/news')(app) require('./router/admin')(app) }; // app/router/news.js module.exports = app => { app.router.get('/news/list', app.controller.news.list) app.router.get('/news/detail', app.controller.news.detail) }; // app/router/admin.js module.exports = app => { app.router.get('/admin/user', app.controller.admin.user) app.router.get('/admin/log', app.controller.admin.log) };
-
使用
egg-router-plus
插件自动引入app/router/**/*.js
,并且提供了 namespace 功能:// app/router.js module.exports = app => { const subRouter = app.router.namespace('/sub') subRouter.get('/test', app.controller.sub.test) // 最终路径为 /sub/test }
2.2 控制器(Controller)
Controller 负责解析用户的输入,处理后返回相应的结果
Controller 可以调用任何一个 Service 上的任何方法,值得注意的是:Service 是懒加载的,即只有当访问到它的时候框架才会去实例化它。
在 Controller 中会做如下几件事情:
- 接收、校验、处理 HTTP 请求参数
- 向下调用服务(Service)处理业务
- 通过 HTTP 将结果响应给用户
一个真实样例
const { Controller } = require('egg');
class CommonDb extends Controller {
/**
* 通用查询
*/
async commonQuery(ctx: Context): Promise<void> {
console.info(ctx.params)
const data = await this.service.commonDbService.queryData({
...(ctx.request.body || {}),
dbName: ctx.params.dbName,
tableName: ctx.params.tableName,
})
ctx.body = CommonRes.getSuccessRes(data)
}
}
module.exports = CommonDb;
自定义Controller基类
由于 Controller 是类
,因此可以通过自定义基类的方式封装常用方法,例如:
// app/core/base_controller.js
const { Controller } = require('egg');
class BaseController extends Controller {
get user() {
return this.ctx.session.user;
}
success(data) {
this.ctx.body = { success: true, data };
}
notFound(msg) {
this.ctx.throw(404, msg || 'not found');
}
}
module.exports = BaseController;
然后让所有 Controller 继承这个自定义的 BaseController:
// app/controller/post.js
const Controller = require('../core/base_controller');
class PostController extends Controller {
async list() {
const posts = await this.service.listByUser(this.user);
this.success(posts);
}
}
this.ctx获取上下文
在 Controller 中通过 this.ctx
可以获取上下文对象,方便获取和设置相关参数,例如:
ctx.query
:URL 中的请求参数(忽略重复 key)ctx.quries
:URL 中的请求参数(重复的 key 被放入数组中)ctx.params
:Router 上的命名参数ctx.request.body
:HTTP 请求体中的内容ctx.request.files
:前端上传的文件对象ctx.getFileStream()
:获取上传的文件流ctx.multipart()
:获取multipart/form-data
数据ctx.cookies
:读取和设置 cookiectx.session
:读取和设置 sessionctx.service.xxx
:获取指定 service 对象的实例(懒加载)ctx.status
:设置状态码ctx.body
:设置响应体ctx.set
:设置响应头ctx.redirect(url)
:重定向ctx.render(template)
:渲染模板
2.3 服务(Service)
Service 是具体业务逻辑的实现,一个封装好的 Service 可供多个 Controller 调用,而一个 Controller 里面也可以调用多个 Service,虽然在 Controller 中也可以写业务逻辑,但是并不建议这么做,代码中应该保持 Controller 逻辑简洁,仅仅发挥「桥梁」作用。
通常情况下,在 Service 中会做如下几件事情:
- 处理复杂业务逻辑
- 调用数据库或第三方服务(例如 GitHub 信息获取等)
- 在 Controller 中可以直接调用
一个简单的 Service 示例,结合上述 commonDBController:
// app/service/commonDbService.ts
const { Service } = require('egg').Service;
class CommonDbService extends Service {
/**
* 查询数据
*/
public async queryData(params: DbCondition) {
DbUtil.allowHandleDB(params?.dbName)
const sql = DbUtil.generateQuerySql(params)
// 表限制
const data = await this.app.mysql.get(params?.dbName).query(sql)
return data
}
}
module.exports = CommonDbService;
Service 文件必须放在 app/service
目录,支持多级目录,访问的时候可以通过目录名级联访问。
Service 里面的函数,可以理解为某个具体业务逻辑的最小单元,Service 里面也可以调用其他 Service,值得注意的是:Service 不是单例,是 请求级别 的对象,框架在每次请求中首次访问 ctx.service.xx
时延迟实例化,所以 Service 中可以通过 this.ctx 获取到当前请求的上下文。
2.4 工具集合(Util)
集合各种工具方法。
2.5 中间件(appMiddleware)
egg 约定一个中间件是一个放置在
app/middleware
目录下的单独文件,它需要导出一个普通的函数
Egg的两种中间件
egg 把中间件分成应用层定义的中间件(app.config.appMiddleware
)和框架默认中间件(app.config.coreMiddleware
)
# appMiddleware
[ 'aaaa' ]
# coreMiddleware
[
'meta',
'siteFile',
'notfound',
'static',
'bodyParser',
'overrideMethod',
'session',
'securities',
'i18n',
'eggLoaderTrace'
]
其中那些 coreMiddleware 是 egg 帮我们内置的中间件,默认是开启的,如果不想用,可以通过配置的方式进行关闭:
module.exports = {
i18n: {
enable: false
}
}
配置中间件
该函数需要导出一个普通的函数,该函数接受两个参数:
options
: 中间件的配置项,框架会将app.config[${middlewareName}]
传递进来。app
: 当前应用 Application 的实例。
该函数返回的是一个async/await koa中间件。
如下所示:
module.exports = (options, app) => {
return async function (ctx, next) {
const startTime = Date.now()
await next()
const consume = Date.now() - startTime
const { threshold = 0 } = options || {}
if (consume > threshold) {
console.log(`${ctx.url}请求耗时${consume}毫秒`)
}
}
}
启用中间件
1)全局启用
然后在 config.default.js
中使用:
module.exports = {
// 配置需要的中间件,数组顺序即为中间件的加载顺序
middleware: [ 'aaaa' ],
// aaaa 中间件的 options 参数
aaaa: {
enable: true
},
}
2)指定路由中使用中间件
例如只针对 /api
前缀开头的 url 请求使用某个中间件的话,有两种方式:
-
在
config.default.js
配置中设置 match 或 ignore 属性:module.exports = { middleware: [ 'aaaa' ], aaaa: { threshold: 1, match: '/api' }, };
-
在路由文件
router.js
中引入
在绑定router中绑定URL和Controller之间,加入中间件。
```
module.exports = app => {
const { router, controller } = app
// 在 controller 处理之前添加任意中间件
router.get('/api/home', app.middleware.slow({ threshold: 1 }), controller.home.index)
}
```
2.6 定时任务(Schedule)
一个复杂的业务场景中,不可避免会有定时任务的需求,比如定时更新redis。
2.7 数据库模型(Models)
用于存放数据库模型,特别注意,可以在这里维护数据库字段说明(ts等)
2.8 静态资源(Public)
用于放置静态资源
2.9 模板(View)
用于放置模板文件
egg 框架内置了 egg-view 作为模板解决方案,并支持多种模板渲染,例如 ejs、handlebars、nunjunks 等模板引擎,每个模板引擎都以插件的方式引入,默认情况下,所有插件都会去找 app/view
目录下的文件,然后根据 config\config.default.js
中定义的后缀映射来选择不同的模板引擎
egg 框架内置了 egg-view 作为模板解决方案,并支持多种模板渲染,例如 ejs、handlebars、nunjunks 等模板引擎,每个模板引擎都以插件的方式引入,默认情况下,所有插件都会去找 app/view
目录下的文件,然后根据 config\config.default.js
中定义的后缀映射来选择不同的模板引擎:
config.view = {
defaultExtension: '.nj',
defaultViewEngine: 'nunjucks',
mapping: {
'.nj': 'nunjucks',
'.hbs': 'handlebars',
'.ejs': 'ejs',
},
}
上面的配置表示,当文件:
- 后缀是
.nj
时使用 nunjunks 模板引擎 - 后缀是
.hbs
时使用 handlebars 模板引擎 - 后缀是
.ejs
时使用 ejs 模板引擎 - 当未指定后缀时默认为
.html
- 当未指定模板引擎时默认为 nunjunks
接下来我们安装模板引擎插件:
$ npm i egg-view-nunjucks egg-view-ejs egg-view-handlebars --save
# 或者
$ yarn add egg-view-nunjucks egg-view-ejs egg-view-handlebars
然后在 config/plugin.js
中启用该插件:
exports.nunjucks = {
enable: true,
package: 'egg-view-nunjucks',
}
exports.handlebars = {
enable: true,
package: 'egg-view-handlebars',
}
exports.ejs = {
enable: true,
package: 'egg-view-ejs',
}
然后添加 app/view
目录,里面增加几个文件:
app/view
├── ejs.ejs
├── handlebars.hbs
└── nunjunks.nj
代码分别是:
<!-- ejs.ejs 文件代码 -->
<h1>ejs</h1>
<ul>
<% items.forEach(function(item){ %>
<li><%= item.title %></li>
<% }); %>
</ul>
<!-- handlebars.hbs 文件代码 -->
<h1>handlebars</h1>
{{#each items}}
<li>{{title}}</li>
{{~/each}}
<!-- nunjunks.nj 文件代码 -->
<h1>nunjunks</h1>
<ul>
{% for item in items %}
<li>{{ item.title }}</li>
{% endfor %}
</ul>
然后在 Router 中配置路由:
module.exports = app => {
const { router, controller } = app
router.get('/ejs', controller.home.ejs)
router.get('/handlebars', controller.home.handlebars)
router.get('/nunjunks', controller.home.nunjunks)
}
接下来实现 Controller 的逻辑:
const Controller = require('egg').Controller
class HomeController extends Controller {
async ejs() {
const { ctx } = this
const items = await ctx.service.view.getItems()
await ctx.render('ejs.ejs', {items})
}
async handlebars() {
const { ctx } = this
const items = await ctx.service.view.getItems()
await ctx.render('handlebars.hbs', {items})
}
async nunjunks() {
const { ctx } = this
const items = await ctx.service.view.getItems()
await ctx.render('nunjunks.nj', {items})
}
}
module.exports = HomeController
我们把数据放到了 Service 里面:
const { Service } = require('egg')
class ViewService extends Service {
getItems() {
return [
{ title: 'foo', id: 1 },
{ title: 'bar', id: 2 },
]
}
}
module.exports = ViewService
访问下面的地址可以查看不同模板引擎渲染出的结果:
GET http://localhost:7001/nunjunks
GET http://localhost:7001/handlebars
GET http://localhost:7001/ejs
你可能会问,ctx.render 方法是哪来的呢?没错,是由 egg-view 对 context 进行扩展而提供的,为 ctx 上下文对象增加了 render
、renderView
和 renderString
三个方法,代码如下:
const ContextView = require('../../lib/context_view')
const VIEW = Symbol('Context#view')
module.exports = {
render(...args) {
return this.renderView(...args).then(body => {
this.body = body;
})
},
renderView(...args) {
return this.view.render(...args);
},
renderString(...args) {
return this.view.renderString(...args);
},
get view() {
if (this[VIEW]) return this[VIEW]
return this[VIEW] = new ContextView(this)
}
}
它内部最终会把调用转发给 ContextView 实例上的 render 方法,ContextView 是一个能够根据配置里面定义的 mapping,帮助我们找到对应渲染引擎的类。
3. 加载插件
3.1 在应用或框架的 config/plugin.js
中声明插件配置
3.2 插件自定义配置
开启插件后,就可以使用插件提供的功能了,如果插件包含需要用户自定义的配置,可以在 config.{env}.js
中进行指定。
4. 错误处理
全局监听拦截,无论是koa还是egg实例,都继承了http.Emiter,所以,每个地方处理出问题,捕获到之后都可以this.emit()
向外发布具体的错误信息。
所以,在全局可以在app实例上直接监听error
事件,再做进一步处理,该报错报错,该记日志记日志。
一个实际case展示:
1)全局监听error事件
2)发布错误
就是触发this.emit()即可,但是真正的业务实践,我们需要权衡发布的最佳时机。
2.1)定义中间件(最先
执行中间件)
2.2)启用中间件(必须注意顺序
)
2.3)全局封装报错方法
Tips: 一个关键问题,是上下游规范,报错规范统一
注意 tryCatch
以及 throw new Error()
5. 生命周期
在 egg 启动的过程中,提供了下面几个生命周期钩子方便大家调用:
- 配置文件即将加载,这是最后动态修改配置的时机(
configWillLoad
) - 配置文件加载完成(
configDidLoad
) - 文件加载完成(
didLoad
) - 插件启动完毕(
willReady
) - worker 准备就绪(
didReady
) - 应用启动完成(
serverDidReady
) - 应用即将关闭(
beforeClose
)
只要在项目根目录中创建 app.js
,添加并导出一个类即可:
class AppBootHook {
constructor(app) {
this.app = app
}
configWillLoad() {
// config 文件已经被读取并合并,但是还并未生效,这是应用层修改配置的最后时机
// 注意:此函数只支持同步调用
}
configDidLoad() {
// 所有的配置已经加载完毕,可以用来加载应用自定义的文件,启动自定义的服务
}
async didLoad() {
// 所有的配置已经加载完毕,可以用来加载应用自定义的文件,启动自定义的服务
}
async willReady() {
// 所有的插件都已启动完毕,但是应用整体还未 ready
// 可以做一些数据初始化等操作,这些操作成功才会启动应用
}
async didReady() {
// 应用已经启动完毕
}
async serverDidReady() {
// http / https server 已启动,开始接受外部请求
// 此时可以从 app.server 拿到 server 的实例
}
async beforeClose() {
// 应用即将关闭
}
}
module.exports = AppBootHook
6.框架扩展
egg 框架提供了下面几个扩展点:
- Application: Koa 的全局应用对象(应用级别),全局只有一个,在应用启动时被创建
- Context:Koa 的请求上下文对象(请求级别),每次请求生成一个 Context 实例
- Request:Koa 的 Request 对象(请求级别),提供请求相关的属性和方法
- Response:Koa 的 Response 对象(请求级别),提供响应相关的属性和方法
- Helper:用来提供一些实用的 utility 函数 也就是说,开发者可以对上述框架内置对象进行任意扩展。扩展的写法为:
const BAR = Symbol('bar')
module.exports = {
foo(param) {}, // 扩展方法
get bar() { // 扩展属性
if (!this[BAR]) {
this[BAR] = this.get('x-bar')
}
return this[BAR]
},
}
扩展点方法里面的 this
就指代扩展点对象自身,扩展的本质就是将用户自定义的对象合并到 Koa 扩展点对象的原型上面,即:
- 扩展 Application 就是把
app/extend/application.js
中定义的对象与 Koa Application 的 prototype 对象进行合并,在应用启动时会基于扩展后的 prototype 生成app
对象,可通过ctx.app.xxx
来进行访问: - 扩展 Context 就是把
app/extend/context.js
中定义的对象与 Koa Context 的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 ctx 对象。 - 扩展 Request/Response 就是把
app/extend/<request|response>.js
中定义的对象与内置request
或response
的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成request
或response
对象。 - 扩展 Helper 就是把
app/extend/helper.js
中定义的对象与内置helper
的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成helper
对象。
7.定制框架
egg 最为强大的功能就是允许团队自定义框架,也就是说可以基于 egg 来封装上层框架,只需要扩展两个类:
- Application:App Worker 启动时会实例化 Application,单例
- Agent:Agent Worker 启动的时候会实例化 Agent,单例
定制框架步骤:
npm init egg --type=framework --registry=china
# 或者
yarn create egg --type=framework --registry=china
生成如下目录结构:
├── app
│ ├── extend
│ │ ├── application.js
│ │ └── context.js
│ └── service
│ └── test.js
├── config
│ ├── config.default.js
│ └── plugin.js
├── index.js
├── lib
│ └── framework.js
├── package.json
可以看到,除了多了一个 lib 目录之外,其他的结构跟普通的 egg 项目并没有任何区别,我们看一下 lib/framework.js
中的代码:
const path = require('path')
const egg = require('egg')
const EGG_PATH = Symbol.for('egg#eggPath')
class Application extends egg.Application {
get [EGG_PATH]() {
return path.dirname(__dirname)
}
}
class Agent extends egg.Agent {
get [EGG_PATH]() {
return path.dirname(__dirname)
}
}
module.exports = Object.assign(egg, {
Application,
Agent,
})
可以看到,只是自定义了 Application 和 Agent 两个类,然后挂载到 egg 对象上面而已。而这两个自定义的类里面将访问器属性 Symbol.for('egg#eggPath')
赋值为 path.dirname(__dirname)
,也就是框架的根目录。为了能够在本地测试自定义框架,我们首先去框架项目(假设叫 my-framework)下运行:
npm link # 或者 yarn link
然后到 egg 项目下运行:
npm link my-framework
最后在 egg 项目的 package.json 中添加下面的代码即可:
"egg": {
"framework": "my-framework"
},
自定义框架的实现原理是基于类的继承,每一层框架都必须继承上一层框架并且指定 eggPath,然后遍历原型链就能获取每一层的框架路径,原型链下面的框架优先级更高,例如:部门框架(department)> 企业框架(enterprise)> Egg
const Application = require('egg').Application
// 继承 egg 的 Application
class Enterprise extends Application {
get [EGG_PATH]() {
return '/mypath/to/enterprise'
}
}
const Application = require('enterprise').Application
// 继承 enterprise 的 Application
class Department extends Application {
get [EGG_PATH]() {
return '/mypath/to/department'
}
}
定制框架的好处就是层层递进的业务逻辑复用,不同部门框架直接用公司框架里面的写好的业务逻辑,然后补充自己的业务逻辑。虽然插件也能达到代码复用的效果,但是业务逻辑不好封装成插件,封装成框架会更好一些。
DiDi的NodeX(DEgg)就是这层封装。
除了使用 Symbol.for('egg#eggPath')
来指定当前框架的路径实现继承之外,还可以自定义加载器,只需要提供 Symbol.for('egg#loader')
访问器属性并自定义 AppWorkerLoader
const path = require('path')
const egg = require('egg')
const EGG_PATH = Symbol.for('egg#eggPath')
const EGG_LOADER = Symbol.for('egg#loader')
class MyAppWorkerLoader extends egg.AppWorkerLoader {
// 自定义的 AppWorkerLoader
}
class Application extends egg.Application {
get [EGG_PATH]() {
return path.dirname(__dirname)
}
get [EGG_LOADER]() {
return MyAppWorkerLoader
}
}
AppWorkerLoader 继承自 egg-core
的 EggLoader
,它是一个基类,根据文件加载的规则提供了一些内置的方法,它本身并不会去调用这些方法,而是由继承类调用。
也就是说我们自定义的 AppWorkerLoader 中可以重写这些方法。 看下代码(下面的代码,忘了从哪篇文章复制的笔记了,这个作者特别用心):
const {AppWorkerLoader} = require('egg')
const {EggLoader} = require('egg-core')
// 如果需要改变加载顺序,则需要继承 EggLoader,否则可以继承 AppWorkerLoader
class MyAppWorkerLoader extends AppWorkerLoader {
constructor(options) {
super(options)
}
load() {
super.load()
console.log('自定义load逻辑')
}
loadPlugin() {
super.loadPlugin()
console.log('自定义plugin加载逻辑')
}
loadConfig() {
super.loadConfig()
console.log('自定义config加载逻辑')
}
loadAgentExtend() {
super.loadAgentExtend()
console.log('自定义agent extend加载逻辑')
}
loadApplicationExtend() {
super.loadApplicationExtend()
console.log('自定义application extend加载逻辑')
}
loadRequestExtend() {
super.loadRequestExtend()
console.log('自定义request extend加载逻辑')
}
loadResponseExtend() {
super.loadResponseExtend()
console.log('自定义response extend加载逻辑')
}
loadContextExtend() {
super.loadContextExtend()
console.log('自定义context extend加载逻辑')
}
loadHelperExtend() {
super.loadHelperExtend()
console.log('自定义helper extend加载逻辑')
}
loadCustomAgent() {
super.loadCustomAgent()
console.log('自定义custom agent加载逻辑')
}
loadCustomApp() {
super.loadCustomApp()
console.log('自定义custom app加载逻辑')
}
loadService() {
super.loadService()
console.log('自定义service加载逻辑')
}
loadMiddleware() {
super.loadMiddleware()
console.log('自定义middleware加载逻辑')
}
loadController() {
super.loadController()
console.log('自定义controller加载逻辑')
}
loadRouter() {
super.loadRouter()
console.log('自定义router加载逻辑')
}
}
最后的输出结果为:
自定义plugin加载逻辑
自定义config加载逻辑
自定义application extend加载逻辑
自定义request extend加载逻辑
自定义response extend加载逻辑
自定义context extend加载逻辑
自定义helper extend加载逻辑
自定义custom app加载逻辑
自定义service加载逻辑
自定义middleware加载逻辑
自定义controller加载逻辑
自定义router加载逻辑
自定义load逻辑
从输出结果能够看出默认情况下的加载顺序。如此以来,框架的加载逻辑可以完全交给开发者,如何加载 Controller、Service、Router 等。