Egg 在 Koa 的基礎(chǔ)上進行增強最重要的就是基于一定的約定,根據(jù)功能差異將代碼放到不同的目錄下管理,對整體團隊的開發(fā)成本提升有著明顯的效果。Loader 實現(xiàn)了這套約定,并抽象了很多底層 API 可以進一步擴展。
應(yīng)用、框架和插件
Egg 是一個底層框架,應(yīng)用可以直接使用,但 Egg 本身的插件比較少,應(yīng)用需要自己配置插件增加各種特性,比如 MySQL。
// 應(yīng)用配置 // package.json { "dependencies": { "egg": "^2.0.0", "egg-mysql": "^3.0.0" } }
// config/plugin.js module.exports = { mysql: { enable: true, package: 'egg-mysql', }, }
|
當應(yīng)用達到一定數(shù)量,我們會發(fā)現(xiàn)大部分應(yīng)用的配置都是類似的,這時可以基于 Egg 擴展出一個框架,應(yīng)用的配置就會簡化很多。
// 框架配置 // package.json { "name": "framework1", "version": "1.0.0", "dependencies": { "egg-mysql": "^3.0.0", "egg-view-nunjucks": "^2.0.0" } }
// config/plugin.js module.exports = { mysql: { enable: false, package: 'egg-mysql', }, view: { enable: false, package: 'egg-view-nunjucks', } }
// 應(yīng)用配置 // package.json { "dependencies": { "framework1": "^1.0.0", } }
// config/plugin.js module.exports = { // 開啟插件 mysql: true, view: true, }
|
從上面的使用場景可以看到應(yīng)用、插件和框架三者之間的關(guān)系。
- 我們在應(yīng)用中完成業(yè)務(wù),需要指定一個框架才能運行起來,當需要某個特性場景的功能時可以配置插件(比如 MySQL)。
- 插件只完成特定功能,當兩個獨立的功能有互相依賴時,還是分開兩個插件,但需要配置依賴。
- 框架是一個啟動器(默認就是 Egg),必須有它才能運行起來??蚣苓€是一個封裝器,將插件的功能聚合起來統(tǒng)一提供,框架也可以配置插件。
- 在框架的基礎(chǔ)上還可以擴展出新的框架,也就是說框架是可以無限級繼承的,有點像類的繼承。
+-----------------------------------+--------+ | app1, app2, app3, app4 | | +-----+--------------+--------------+ | | | | framework3 | | + | framework1 +--------------+ plugin | | | | framework2 | | + +--------------+--------------+ | | Egg | | +-----------------------------------+--------| | Koa | +-----------------------------------+--------+
|
加載單元(loadUnit)
Egg 將應(yīng)用、框架和插件都稱為加載單元(loadUnit),因為在代碼結(jié)構(gòu)上幾乎沒有什么差異,下面是目錄結(jié)構(gòu)
loadUnit ├── package.json ├── app.js ├── agent.js ├── app │ ├── extend │ | ├── helper.js │ | ├── request.js │ | ├── response.js │ | ├── context.js │ | ├── application.js │ | └── agent.js │ ├── service │ ├── middleware │ └── router.js └── config ├── config.default.js ├── config.prod.js ├── config.test.js ├── config.local.js └── config.unittest.js
|
不過還存在著一些差異
文件 | 應(yīng)用 | 框架 | 插件 |
---|
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 | ?? | | |
文件按表格內(nèi)的順序自上而下加載
在加載過程中,Egg 會遍歷所有的 loadUnit 加載上述的文件(應(yīng)用、框架、插件各有不同),加載時有一定的優(yōu)先級
- 按插件 => 框架 => 應(yīng)用依次加載
- 插件之間的順序由依賴關(guān)系決定,被依賴方先加載,無依賴按 object key 配置順序加載,具體可以查看插件章節(jié)
- 框架按繼承順序加載,越底層越先加載。
比如有這樣一個應(yīng)用配置了如下依賴
app | ├── plugin2 (依賴 plugin3) | └── plugin3 └── framework1 | └── plugin1 └── egg
|
最終的加載順序為
=> plugin1 => plugin3 => plugin2 => egg => framework1 => app
|
plugin1 為 framework1 依賴的插件,配置合并后 object key 的順序會優(yōu)先于 plugin2/plugin3。因為 plugin2 和 plugin3 的依賴關(guān)系,所以交換了位置。framework1 繼承了 egg,順序會晚于 egg。應(yīng)用最后加載。
請查看 Loader.getLoadUnits 方法
文件順序
上面已經(jīng)列出了默認會加載的文件,Egg 會按如下文件順序加載,每個文件或目錄再根據(jù) loadUnit 的順序去加載(應(yīng)用、框架、插件各有不同)。
- 加載 plugin,找到應(yīng)用和框架,加載 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,加載應(yīng)用的 app/controller 目錄
- 加載 router,加載應(yīng)用的 app/router.js
注意:
- 加載時如果遇到同名的會覆蓋,比如想要覆蓋 ctx.ip 可以直接在應(yīng)用的 app/extend/context.js 定義 ip 就可以了。
- 應(yīng)用完整啟動順序查看框架開發(fā)
生命周期
框架提供了這些生命周期函數(shù)供開發(fā)人員處理:
- 配置文件即將加載,這是最后動態(tài)修改配置的時機(configWillLoad)
- 配置文件加載完成(configDidLoad)
- 文件加載完成(didLoad)
- 插件啟動完畢(willReady)
- worker 準備就緒(didReady)
- 應(yīng)用啟動完成(serverDidReady)
- 應(yīng)用即將關(guān)閉(beforeClose)
定義如下:
// app.js or agent.js class AppBootHook { constructor(app) { this.app = app; }
configWillLoad() { // Ready to call configDidLoad, // Config, plugin files are referred, // this is the last chance to modify the config. }
configDidLoad() { // Config, plugin files have been loaded. }
async didLoad() { // All files have loaded, start plugin here. }
async willReady() { // All plugins have started, can do some thing before app ready }
async didReady() { // Worker is ready, can do some things // don't need to block the app boot. }
async serverDidReady() { // Server is listening. }
async beforeClose() { // Do some thing before app close. } }
module.exports = AppBootHook;
|
開發(fā)者使用類的方式定義 app.js 和 agent.js 之后, 框架會自動加載并實例化這個類, 并且在各個生命周期階段調(diào)用對應(yīng)的方法。
啟動過程如圖所示:
使用 beforeClose 的時候需要注意,在框架的進程關(guān)閉處理中是有超時時間的,如果 worker 進程在接收到進程退出信號之后,沒有在所規(guī)定的時間內(nèi)退出,將會被強制關(guān)閉。
如果需要調(diào)整超時時間的話,查看此處文檔。
棄用的方法:
beforeStart
beforeStart 方法在 loading 過程中調(diào)用, 所有的方法并行執(zhí)行。 一般用來執(zhí)行一些異步方法, 例如檢查連接狀態(tài)等, 比如 egg-mysql 就用 beforeStart 來檢查與 mysql 的連接狀態(tài)。所有的 beforeStart 任務(wù)結(jié)束后, 狀態(tài)將會進入 ready 。不建議執(zhí)行一些耗時較長的方法, 可能會導致應(yīng)用啟動超時。插件開發(fā)者應(yīng)使用 didLoad 替換。應(yīng)用開發(fā)者應(yīng)使用 willReady 替換。
ready
ready 方法注冊的任務(wù)在 load 結(jié)束并且所有的 beforeStart 方法執(zhí)行結(jié)束后順序執(zhí)行, HTTP server 監(jiān)聽也是在這個時候開始, 此時代表所有的插件已經(jīng)加載完畢并且準備工作已經(jīng)完成, 一般用來執(zhí)行一些啟動的后置任務(wù)。開發(fā)者應(yīng)使用 didReady 替換。
beforeClose
beforeClose 注冊方法在 app/agent 實例的 close 方法被調(diào)用后, 按注冊的逆序執(zhí)行。一般用于資源的釋放操作, 例如 egg 用來關(guān)閉 logger、刪除監(jiān)聽方法等。開發(fā)者不應(yīng)該直接使用 app.beforeClose, 而是定義類的形式, 實現(xiàn) beforeClose 方法。
這個方法不建議在生產(chǎn)環(huán)境使用, 可能遇到未執(zhí)行完就結(jié)束進程的問題。
此外,我們可以使用 egg-development 來查看加載過程。
文件加載規(guī)則
框架在加載文件時會進行轉(zhuǎn)換,因為文件命名風格和 API 風格存在差異。我們推薦文件使用下劃線,而 API 使用駝峰。比如 app/service/user_info.js 會轉(zhuǎn)換成 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 是一個基類,并根據(jù)文件加載的規(guī)則提供了一些內(nèi)置的方法,它本身并不會去調(diào)用這些方法,而是由繼承類調(diào)用。
- loadPlugin()
- loadConfig()
- loadAgentExtend()
- loadApplicationExtend()
- loadRequestExtend()
- loadResponseExtend()
- loadContextExtend()
- loadHelperExtend()
- loadCustomAgent()
- loadCustomApp()
- loadService()
- loadMiddleware()
- loadController()
- loadRouter()
Egg 基于 Loader 實現(xiàn)了 AppWorkerLoader 和 AgentWorkerLoader,上層框架基于這兩個類來擴展,Loader 的擴展只能在框架進行。
// 自定義 AppWorkerLoader // lib/framework.js const path = require('path'); const egg = require('egg'); const EGG_PATH = Symbol.for('egg#eggPath');
class YadanAppWorkerLoader extends egg.AppWorkerLoader { constructor(opt) { super(opt); // 自定義初始化 }
loadConfig() { super.loadConfig(); // 對 config 進行處理 }
load() { super.load(); // 自定義加載其他目錄 // 或?qū)σ鸭虞d的文件進行處理 } }
class Application extends egg.Application { get [EGG_PATH]() { return path.dirname(__dirname); } // 覆蓋 Egg 的 Loader,啟動時使用這個 Loader get [EGG_LOADER]() { return YadanAppWorkerLoader; } }
module.exports = Object.assign(egg, { Application, // 自定義的 Loader 也需要 export,上層框架需要基于這個擴展 AppWorkerLoader: YadanAppWorkerLoader, });
|
通過 Loader 提供的這些 API,可以很方便的定制團隊的自定義加載,如 this.model.xx,app/extend/filter.js 等等。
以上只是說明 Loader 的寫法,具體可以查看框架開發(fā)。
加載器函數(shù)(Loader API)
Loader 還提供一些底層的 API,在擴展時可以簡化代碼,點擊此處查看所有相關(guān) API。
loadFile
用于加載一個文件,比如加載 app/xx.js 就是使用這個方法。
// app/xx.js module.exports = app => { console.log(app.config); };
// app.js // 以 app/xx.js 為例,我們可以在 app.js 加載這個文件 const path = require('path'); module.exports = app => { app.loader.loadFile(path.join(app.config.baseDir, 'app/xx.js')); };
|
如果文件 export 一個函數(shù)會被調(diào)用,并將 app 作為參數(shù),否則直接使用這個值。
loadToApp
用于加載一個目錄下的文件到 app,比如 app/controller/home.js 會加載到 app.controller.home。
// app.js // 以下只是示例,加載 controller 請用 loadController module.exports = app => { const directory = path.join(app.config.baseDir, 'app/controller'); app.loader.loadToApp(directory, 'controller'); };
|
一共有三個參數(shù) loadToApp(directory, property, LoaderOptions)
- directory 可以為 String 或 Array,Loader 會從這些目錄加載文件
- property 為 app 的屬性
- LoaderOptions 為一些配置
loadToContext
與 loadToApp 有一點差異,loadToContext 是加載到 ctx 上而非 app,而且是懶加載。加載時會將文件都放到一個臨時對象上,在調(diào)用 ctx API 時才實例化對象。
比如 service 的加載就是使用這種模式
// 以下為示例,請使用 loadService // app/service/user.js const Service = require('egg').Service; class UserService extends Service {
} module.exports = UserService;
// app.js // 獲取所有的 loadUnit const servicePaths = app.loader.getLoadUnits().map(unit => path.join(unit.path, 'app/service'));
app.loader.loadToContext(servicePaths, 'service', { // service 需要繼承 app.Service,所以要拿到 app 參數(shù) // 設(shè)置 call 在加載時會調(diào)用函數(shù)返回 UserService call: true, // 將文件加載到 app.serviceClasses fieldClass: 'serviceClasses', });
|
文件加載后 app.serviceClasses.user 就是 UserService,當調(diào)用 ctx.service.user 時會實例化 UserService, 所以這個類只有每次請求中首次訪問時才會實例化,實例化后會被緩存,同一個請求多次調(diào)用也只會實例化一次。
LoaderOptions
ignore [String]
ignore 可以忽略一些文件,支持 glob,默認為空
app.loader.loadToApp(directory, 'controller', { // 忽略 app/controller/util 下的文件 ignore: 'util/**', });
|
initializer [Function]
對每個文件 export 出來的值進行處理,默認為空
// app/model/user.js module.exports = class User { constructor(app, path) {} }
// 從 app/model 目錄加載,加載時可做一些初始化處理 const directory = path.join(app.config.baseDir, 'app/model'); app.loader.loadToApp(directory, 'model', { initializer(model, opt) { // 第一個參數(shù)為 export 的對象 // 第二個參數(shù)為一個對象,只包含當前文件的路徑 return new model(app, opt.path); }, });
|
caseStyle [String]
文件的轉(zhuǎn)換規(guī)則,可選為 camel,upper,lower,默認為 camel。
三者都會將文件名轉(zhuǎn)換成駝峰,但是對于首字母的處理有所不同。
- camel:首字母不變。
- upper:首字母大寫。
- lower:首字母小寫。
在加載不同文件時配置不同
文件 | 配置 |
---|
app/controller | lower |
app/middleware | lower |
app/service | lower |
override [Boolean]
遇到已經(jīng)存在的文件時是直接覆蓋還是拋出異常,默認為 false
比如同時加載應(yīng)用和插件的 app/service/user.js 文件,如果為 true 應(yīng)用會覆蓋插件的,否則加載應(yīng)用的文件時會報錯。
在加載不同文件時配置不同
文件 | 配置 |
---|
app/controller | true |
app/middleware | false |
app/service | false |
call [Boolean]
當 export 的對象為函數(shù)時則調(diào)用,并獲取返回值,默認為 true
在加載不同文件時配置不同
文件 | 配置 |
---|
app/controller | true |
app/middleware | false |
app/service | true |
CustomLoader
loadToContext 和 loadToApp 可被 customLoader 配置替代。
如使用 loadToApp 加載的代碼如下
// app.js module.exports = app => { const directory = path.join(app.config.baseDir, 'app/adapter'); app.loader.loadToApp(directory, 'adapter'); };;
|
換成 customLoader 后變?yōu)?/p>// config/config.default.js module.exports = { customLoader: { // 定義在 app 上的屬性名 app.adapter adapter: { // 相對于 app.config.baseDir directory: 'app/adapter', // 如果是 ctx 則使用 loadToContext inject: 'app', // 是否加載框架和插件的目錄 loadunit: false, // 還可以定義其他 LoaderOptions } }, }; |
更多建議: