加载器(Loader)
Egg 在 Koa 的基础上进行增强最重要的就是基于一定的约定,根据功能差异将代码放到不同的目录下管理,对整体团队的开发成本提升有着明显的效果。Loader 实现了这套约定,并抽象了很多底层 API 可以进一步扩展。
# 应用、框架和插件
Egg 是一个底层框架,应用可以直接使用,但 Egg 本身的插件比较少,应用需要自己配置插件增加各种特性,比如 MySQL。
// 应用配置 |
当应用达到一定数量,我们会发现大部分应用的配置都是类似的,这时可以基于 Egg 扩展出一个框架,应用的配置就会简化很多。
// 框架配置 |
从上面的使用场景可以看到应用、插件和框架三者之间的关系。
- 我们在应用中完成业务,需要指定一个框架才能运行起来,当需要某个特性场景的功能时可以配置插件(比如 MySQL)。
- 插件只完成特定功能,当两个独立的功能有互相依赖时,还是分开两个插件,但需要配置依赖。
- 框架是一个启动器(默认就是 Egg),必须有它才能运行起来。框架还是一个封装器,将插件的功能聚合起来统一提供,框架也可以配置插件。
- 在框架的基础上还可以扩展出新的框架,也就是说框架是可以无限级继承的,有点像类的继承。
+-----------------------------------+--------+ |
# 加载单元(loadUnit)
Egg 将应用、框架和插件都称为加载单元(loadUnit),因为在代码结构上几乎没有什么差异,下面是目录结构
loadUnit |
不过还存在着一些差异
文件 | 应用 | 框架 | 插件 |
---|---|---|---|
package.json | ✔︎ | ✔︎ | ✔︎ |
config/plugin.{env}.js | ✔︎ | ✔︎ | |
config/config.{env}.js | ✔︎ | ✔︎ | ✔︎ |
app/extend/application.js | ✔︎ | ✔︎ | ✔︎ |
app/extend/request.js | ✔︎ | ✔︎ | ✔︎ |
app/extend/response.js | ✔︎ | ✔︎ | ✔︎ |
app/extend/context.js | ✔︎ | ✔︎ | ✔︎ |
app/extend/helper.js | ✔︎ | ✔︎ | ✔︎ |
agent.js | ✔︎ | ✔︎ | ✔︎ |
app.js | ✔︎ | ✔︎ | ✔︎ |
app/service | ✔︎ | ✔︎ | ✔︎ |
app/middleware | ✔︎ | ✔︎ | ✔︎ |
app/controller | ✔︎ | ||
app/router.js | ✔︎ |
文件按表格内的顺序自上而下加载
在加载过程中,Egg 会遍历所有的 loadUnit 加载上述的文件(应用、框架、插件各有不同),加载时有一定的优先级
- 按插件 => 框架 => 应用依次加载
- 插件之间的顺序由依赖关系决定,被依赖方先加载,无依赖按 object key 配置顺序加载,具体可以查看插件章节
- 框架按继承顺序加载,越底层越先加载。
比如有这样一个应用配置了如下依赖
app |
最终的加载顺序为
=> plugin1 |
plugin1 为 framework1 依赖的插件,配置合并后 object key 的顺序会优先于 plugin2/plugin3。因为 plugin2 和 plugin3 的依赖关系,所以交换了位置。framework1 继承了 egg,顺序会晚于 egg。应用最后加载。
请查看 Loader.getLoadUnits 方法
# 文件顺序
上面已经列出了默认会加载的文件,Egg 会按如下文件顺序加载,每个文件或目录再根据 loadUnit 的顺序去加载(应用、框架、插件各有不同)。
- 加载 plugin,找到应用和框架,加载
config/plugin.js
- 加载 config,遍历 loadUnit 加载
config/config.{env}.js
- 加载 extend,遍历 loadUnit 加载
app/extend/xx.js
- 自定义初始化,遍历 loadUnit 加载
app.js
和agent.js
- 加载 service,遍历 loadUnit 加载
app/service
目录 - 加载 middleware,遍历 loadUnit 加载
app/middleware
目录 - 加载 controller,加载应用的
app/controller
目录 - 加载 router,加载应用的
app/router.js
注意:
- 加载时如果遇到同名的会覆盖,比如想要覆盖
ctx.ip
可以直接在应用的app/extend/context.js
定义 ip 就可以了。 - 应用完整启动顺序查看框架开发
# 生命周期
框架提供了这些生命周期函数供开发人员处理:
- 配置文件即将加载,这是最后动态修改配置的时机(
configWillLoad
) - 配置文件加载完成(
configDidLoad
) - 文件加载完成(
didLoad
) - 插件启动完毕(
willReady
) - worker 准备就绪(
didReady
) - 应用启动完成(
serverDidReady
) - 应用即将关闭(
beforeClose
)
定义如下:
// app.js or agent.js |
开发者使用类的方式定义 app.js
和 agent.js
之后, 框架会自动加载并实例化这个类, 并且在各个生命周期阶段调用对应的方法。
启动过程如图所示:
使用 beforeClose
的时候需要注意,在框架的进程关闭处理中是有超时时间的,如果 worker 进程在接收到进程退出信号之后,没有在所规定的时间内退出,将会被强制关闭。
如果需要调整超时时间的话,查看此处文档。
弃用的方法:
# beforeStart
beforeStart
方法在 loading 过程中调用, 所有的方法并行执行。 一般用来执行一些异步方法, 例如检查连接状态等, 比如 egg-mysql
就用 beforeStart
来检查与 mysql 的连接状态。所有的 beforeStart
任务结束后, 状态将会进入 ready
。不建议执行一些耗时较长的方法, 可能会导致应用启动超时。插件开发者应使用 didLoad
替换。应用开发者应使用 willReady
替换。
# ready
ready
方法注册的任务在 load 结束并且所有的 beforeStart
方法执行结束后顺序执行, HTTP server 监听也是在这个时候开始, 此时代表所有的插件已经加载完毕并且准备工作已经完成, 一般用来执行一些启动的后置任务。开发者应使用 didReady
替换。
# beforeClose
beforeClose
注册方法在 app/agent 实例的 close
方法被调用后, 按注册的逆序执行。一般用于资源的释放操作, 例如 egg
用来关闭 logger、删除监听方法等。开发者不应该直接使用 app.beforeClose
, 而是定义类的形式, 实现 beforeClose
方法。
这个方法不建议在生产环境使用, 可能遇到未执行完就结束进程的问题。
此外,我们可以使用 egg-development
来查看加载过程。
# 文件加载规则
框架在加载文件时会进行转换,因为文件命名风格和 API 风格存在差异。我们推荐文件使用下划线,而 API 使用驼峰。比如 app/service/user_info.js
会转换成 app.service.userInfo
。
框架也支持连字符和驼峰的方式
app/service/user-info.js
=>app.service.userInfo
app/service/userInfo.js
=>app.service.userInfo
Loader 还提供了 caseStyle 强制指定首字母大小写,比如加载 model 时 API 首字母大写,app/model/user.js
=> app.model.User
,就可以指定 caseStyle: 'upper'
。
# 扩展 Loader
Loader 是一个基类,并根据文件加载的规则提供了一些内置的方法,它本身并不会去调用这些方法,而是由继承类调用。
- loadPlugin()
- loadConfig()
- loadAgentExtend()
- loadApplicationExtend()
- loadRequestExtend()
- loadResponseExtend()
- loadContextExtend()
- loadHelperExtend()
- loadCustomAgent()
- loadCustomApp()
- loadService()
- loadMiddleware()
- loadController()
- loadRouter()
Egg 基于 Loader 实现了 AppWorkerLoader 和 AgentWorkerLoader,上层框架基于这两个类来扩展,Loader 的扩展只能在框架进行。
// 自定义 AppWorkerLoader |
通过 Loader 提供的这些 API,可以很方便的定制团队的自定义加载,如 this.model.xx
,app/extend/filter.js
等等。
以上只是说明 Loader 的写法,具体可以查看框架开发。
# 加载器函数(Loader API)
Loader 还提供一些底层的 API,在扩展时可以简化代码,点击此处查看所有相关 API。
# loadFile
用于加载一个文件,比如加载 app/xx.js
就是使用这个方法。
// app/xx.js |
如果文件 export 一个函数会被调用,并将 app 作为参数,否则直接使用这个值。
# loadToApp
用于加载一个目录下的文件到 app,比如 app/controller/home.js
会加载到 app.controller.home
。
// app.js |
一共有三个参数 loadToApp(directory, property, LoaderOptions)
- directory 可以为 String 或 Array,Loader 会从这些目录加载文件
- property 为 app 的属性
- LoaderOptions 为一些配置
# loadToContext
与 loadToApp 有一点差异,loadToContext 是加载到 ctx 上而非 app,而且是懒加载。加载时会将文件都放到一个临时对象上,在调用 ctx API 时才实例化对象。
比如 service 的加载就是使用这种模式
// 以下为示例,请使用 loadService |
文件加载后 app.serviceClasses.user
就是 UserService,当调用 ctx.service.user
时会实例化 UserService,
所以这个类只有每次请求中首次访问时才会实例化,实例化后会被缓存,同一个请求多次调用也只会实例化一次。
# LoaderOptions
# ignore [String]
ignore
可以忽略一些文件,支持 glob,默认为空
app.loader.loadToApp(directory, 'controller', { |
# initializer [Function]
对每个文件 export 出来的值进行处理,默认为空
// app/model/user.js |
# caseStyle [String]
文件的转换规则,可选为 camel
,upper
,lower
,默认为 camel
。
三者都会将文件名转换成驼峰,但是对于首字母的处理有所不同。
camel
:首字母不变。upper
:首字母大写。lower
:首字母小写。
在加载不同文件时配置不同
文件 | 配置 |
---|---|
app/controller | lower |
app/middleware | lower |
app/service | lower |
# override [Boolean]
遇到已经存在的文件时是直接覆盖还是抛出异常,默认为 false
比如同时加载应用和插件的 app/service/user.js
文件,如果为 true 应用会覆盖插件的,否则加载应用的文件时会报错。
在加载不同文件时配置不同
文件 | 配置 |
---|---|
app/controller | true |
app/middleware | false |
app/service | false |
# call [Boolean]
当 export 的对象为函数时则调用,并获取返回值,默认为 true
在加载不同文件时配置不同
文件 | 配置 |
---|---|
app/controller | true |
app/middleware | false |
app/service | true |
# CustomLoader
loadToContext
和 loadToApp
可被 customLoader
配置替代。
如使用 loadToApp
加载的代码如下
// app.js |
换成 customLoader
后变为
// config/config.default.js |