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