Egg 加載器(Loader)

2020-02-06 14:12 更新

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í)間的話,查看此處文檔。

棄用的方法:

beforeStart

beforeStart 方法在 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 替換。

ready

ready 方法注冊(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 替換。

beforeClose

beforeClose 注冊(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ò)展 Loader

Loader 是一個(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)

  1. directory 可以為 String 或 Array,Loader 會(huì)從這些目錄加載文件
  2. property 為 app 的屬性
  3. 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/controllerlower
app/middlewarelower
app/servicelower

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/controllertrue
app/middlewarefalse
app/servicefalse

call [Boolean]

當(dāng) export 的對(duì)象為函數(shù)時(shí)則調(diào)用,并獲取返回值,默認(rèn)為 true

在加載不同文件時(shí)配置不同

文件配置
app/controllertrue
app/middlewarefalse
app/servicetrue

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: {
// 相對(duì)于 app.config.baseDir
directory: 'app/adapter',
// 如果是 ctx 則使用 loadToContext
inject: 'app',
// 是否加載框架和插件的目錄
loadunit: false,
// 還可以定義其他 LoaderOptions
}
},
};


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)