Egg 框架開發(fā)

2020-02-06 14:12 更新

如果你的團(tuán)隊(duì)遇到過:

  • 維護(hù)很多個(gè)項(xiàng)目,每個(gè)項(xiàng)目都需要復(fù)制拷貝諸如 gulpfile.js / webpack.config.js 之類的文件。
  • 每個(gè)項(xiàng)目都需要使用一些相同的類庫,相同的配置。
  • 在新項(xiàng)目中對上面的配置做了一個(gè)優(yōu)化后,如何同步到其他項(xiàng)目?

如果你的團(tuán)隊(duì)需要:

  • 統(tǒng)一的技術(shù)選型,比如數(shù)據(jù)庫、模板、前端框架及各種中間件設(shè)施都需要選型,而框架封裝后保證應(yīng)用使用一套架構(gòu)。
  • 統(tǒng)一的默認(rèn)配置,開源社區(qū)的配置可能不適用于公司,而又不希望應(yīng)用去配置。
  • 統(tǒng)一的部署方案,通過框架和平臺(tái)的雙向控制,應(yīng)用只需要關(guān)注自己的代碼,具體查看應(yīng)用部署
  • 統(tǒng)一的代碼風(fēng)格,框架不僅僅解決代碼重用問題,還可以對應(yīng)用做一定約束,作為企業(yè)框架是很必要的。Egg 在 Koa 基礎(chǔ)上做了很多約定,框架可以使用 Loader 自己定義代碼規(guī)則。

為此,Egg 為團(tuán)隊(duì)架構(gòu)師和技術(shù)負(fù)責(zé)人提供 框架定制 的能力,框架是一層抽象,可以基于 Egg 去封裝上層框架,并且 Egg 支持多層繼承。

這樣,整個(gè)團(tuán)隊(duì)就可以遵循統(tǒng)一的方案,并且在項(xiàng)目中可以根據(jù)業(yè)務(wù)場景自行使用插件做差異化,當(dāng)后者驗(yàn)證為最佳實(shí)踐后,就能下沉到框架中,其他項(xiàng)目僅需簡單的升級(jí)下框架的版本即可享受到。

具體可以參見漸進(jìn)式開發(fā)

框架與多進(jìn)程

框架的擴(kuò)展是和多進(jìn)程模型有關(guān)的,我們已經(jīng)知道多進(jìn)程模型,也知道 Agent Worker 和 App Worker 的區(qū)別,所以我們需要擴(kuò)展的類也有兩個(gè) Agent 和 Application,而這兩個(gè)類的 API 不一定相同。

在 Agent Worker 啟動(dòng)的時(shí)候會(huì)實(shí)例化 Agent,而在 App Worker 啟動(dòng)時(shí)會(huì)實(shí)例化 Application,這兩個(gè)類又同時(shí)繼承 EggCore。

EggCore 可以看做 Koa Application 的升級(jí)版,默認(rèn)內(nèi)置 LoaderRouter 及應(yīng)用異步啟動(dòng)等功能,可以看做是支持 Loader 的 Koa。

      Koa Application
^
EggCore
^
┌──────┴───────┐
│ │
Egg Agent Egg Application
^ ^
agent worker app worker

如何定制一個(gè)框架

你可以直接通過 egg-boilerplate-framework 腳手架來快速上手。

$ mkdir yadan && cd yadan
$ npm init egg --type=framework
$ npm i
$ npm test

但同樣,為了讓大家了解細(xì)節(jié),接下來我們還是手把手來定制一個(gè)框架,具體代碼可以查看示例

框架 API

Egg 框架提供了一些 API,所有繼承的框架都需要提供,只增不減。這些 API 基本都有 Agent 和 Application 兩份。

egg.startCluster

Egg 的多進(jìn)程啟動(dòng)器,由這個(gè)方法來啟動(dòng) Master,主要的功能實(shí)現(xiàn)在 egg-cluster 上。所以直接使用 EggCore 還是單進(jìn)程的方式,而 Egg 實(shí)現(xiàn)了多進(jìn)程。

const startCluster = require('egg').startCluster;
startCluster({
// 應(yīng)用的代碼目錄
baseDir: '/path/to/app',
// 需要通過這個(gè)參數(shù)來指定框架目錄
framework: '/path/to/framework',
}, () => {
console.log('app started');
});

所有參數(shù)可以查看 egg-cluster

egg.Application 和 egg.Agent

進(jìn)程中的唯一單例,但 Application 和 Agent 存在一定差異。如果框架繼承于 Egg,會(huì)定制這兩個(gè)類,那 framework 應(yīng)該 export 這兩個(gè)類。

egg.AppWorkerLoader 和 egg.AgentWorkerLoader

框架也存在定制 Loader 的場景,覆蓋原方法或者新加載目錄都需要提供自己的 Loader,而且必須要繼承 Egg 的 Loader。

框架繼承

框架支持繼承關(guān)系,可以把框架比作一個(gè)類,那么基類就是 Egg 框架,如果想對 Egg 做擴(kuò)展就繼承。

首先定義一個(gè)框架需要實(shí)現(xiàn) Egg 所有的 API

// package.json
{
"name": "yadan",
"dependencies": {
"egg": "^2.0.0"
}
}

// index.js
module.exports = require('./lib/framework.js');

// 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]() {
// 返回 framework 路徑
return path.dirname(__dirname);
}
}

// 覆蓋了 Egg 的 Application
module.exports = Object.assign(egg, {
Application,
});

應(yīng)用啟動(dòng)時(shí)需要指定框架名(在 package.json 指定 egg.framework,默認(rèn)為 egg),Loader 將從 node_modules 找指定模塊作為框架,并加載其 export 的 Application。

{
"scripts": {
"dev": "egg-bin dev"
},
"egg": {
"framework": "yadan"
}
}

現(xiàn)在 yadan 框架目錄已經(jīng)是一個(gè) loadUnit,那么相應(yīng)目錄和文件(如 app 和 config)都會(huì)被加載,查看框架被加載的文件。

框架繼承原理

使用 Symbol.for('egg#eggPath') 來指定當(dāng)前框架的路徑,目的是讓 Loader 能探測到框架的路徑。為什么這樣實(shí)現(xiàn)呢?其實(shí)最簡單的方式是將框架的路徑傳遞給 Loader,但我們需要實(shí)現(xiàn)多級(jí)框架繼承,每一層框架都要提供自己的當(dāng)前路徑,并且需要繼承存在先后順序。

現(xiàn)在的實(shí)現(xiàn)方案是基于類繼承的,每一層框架都必須繼承上一層框架并且指定 eggPath,然后遍歷原型鏈就能獲取每一層的框架路徑了。

比如有三層框架:部門框架(department)> 企業(yè)框架(enterprise)> Egg

// enterprise
const Application = require('egg').Application;
class Enterprise extends Application {
get [EGG_PATH]() {
return '/path/to/enterprise';
}
}
// 自定義模塊 Application
exports.Application = Enterprise;

// department
const Application = require('enterprise').Application;
// 繼承 enterprise 的 Application
class department extends Application {
get [EGG_PATH]() {
return '/path/to/department';
}
}

// 啟動(dòng)需要傳入 department 的框架路徑才能獲取 Application
const Application = require('department').Application;
const app = new Application();
app.ready();

以上均是偽代碼,為了詳細(xì)說明框架路徑的加載過程,不過 Egg 已經(jīng)在本地開發(fā)應(yīng)用部署提供了很好的工具,不需要自己實(shí)現(xiàn)。

自定義 Agent

上面的例子自定義了 Application,因?yàn)?Egg 是多進(jìn)程模型,所以還需要定義 Agent,原理是一樣的。

// 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]() {
// 返回 framework 路徑
return path.dirname(__dirname);
}
}

class Agent extends egg.Agent {
get [EGG_PATH]() {
return path.dirname(__dirname);
}
}

// 覆蓋了 Egg 的 Application
module.exports = Object.assign(egg, {
Application,
Agent,
});

但因?yàn)?Agent 和 Application 是兩個(gè)實(shí)例,所以 API 有可能不一致。

自定義 Loader

Loader 應(yīng)用啟動(dòng)的核心,使用它還能規(guī)范應(yīng)用代碼,我們可以基于這個(gè)類擴(kuò)展更多功能,比如加載數(shù)據(jù)代碼。擴(kuò)展 Loader 還能覆蓋默認(rèn)的實(shí)現(xiàn),或調(diào)整現(xiàn)有的加載順序等。

自定義 Loader 也是用 Symbol.for('egg#loader') 的方式,主要的原因還是使用原型鏈,上層框架可覆蓋底層 Loader,在上面例子的基礎(chǔ)上

// lib/framework.js
const path = require('path');
const egg = require('egg');
const EGG_PATH = Symbol.for('egg#eggPath');

class YadanAppWorkerLoader extends egg.AppWorkerLoader {
load() {
super.load();
// 自己擴(kuò)展
}
}

class Application extends egg.Application {
get [EGG_PATH]() {
// 返回 framework 路徑
return path.dirname(__dirname);
}
// 覆蓋 Egg 的 Loader,啟動(dòng)時(shí)使用這個(gè) Loader
get [EGG_LOADER]() {
return YadanAppWorkerLoader;
}
}

// 覆蓋了 Egg 的 Application
module.exports = Object.assign(egg, {
Application,
// 自定義的 Loader 也需要 export,上層框架需要基于這個(gè)擴(kuò)展
AppWorkerLoader: YadanAppWorkerLoader,
});

AgentWorkerLoader 擴(kuò)展也類似,這里不再舉例。AgentWorkerLoader 加載的文件可以于 AppWorkerLoader 不同,比如:默認(rèn)加載時(shí),Egg 的 AppWorkerLoader 會(huì)加載 app.js 而 AgentWorkerLoader 加載的是 agent.js。

框架啟動(dòng)原理

框架啟動(dòng)在多進(jìn)程模型、Loader插件中或多或少都提過,這里系統(tǒng)的梳理下啟動(dòng)順序。

  • startCluster 啟動(dòng)傳入 baseDir 和 framework,Master 進(jìn)程啟動(dòng)
  • Master 先 fork Agent Worker根據(jù) framework 找到框架目錄,實(shí)例化該框架的 Agent 類Agent 找到定義的 AgentWorkerLoader,開始進(jìn)行加載AgentWorkerLoader,開始進(jìn)行加載 整個(gè)加載過程是同步的,按 plugin > config > extend > agent.js > 其他文件順序加載agent.js 可自定義初始化,支持異步啟動(dòng),如果定義了 beforeStart 會(huì)等待執(zhí)行完成之后通知 Master 啟動(dòng)完成。
  • Master 得到 Agent Worker 啟動(dòng)成功的消息,使用 cluster fork App WorkerApp Worker 有多個(gè)進(jìn)程,所以這幾個(gè)進(jìn)程是并行啟動(dòng)的,但執(zhí)行邏輯是一致的單個(gè) App Worker 和 Agent 類似,通過 framework 找到框架目錄,實(shí)例化該框架的 Application 類Application 找到 AppWorkerLoader,開始進(jìn)行加載,順序也是類似的,會(huì)異步等待,完成后通知 Master 啟動(dòng)完成
  • Master 等待多個(gè) App Worker 的成功消息后啟動(dòng)完成,能對外提供服務(wù)。

框架測試

在看下文之前請先查看單元測試章節(jié),框架測試的大部分使用場景和應(yīng)用類似。

初始化

框架的初始化方式有一定差異

const mock = require('egg-mock');
describe('test/index.test.js', () => {
let app;
before(() => {
app = mock.app({
// 轉(zhuǎn)換成 test/fixtures/apps/example
baseDir: 'apps/example',
// 重要:配置 framework
framework: true,
});
return app.ready();
});

after(() => app.close());
afterEach(mock.restore);

it('should success', () => {
return app.httpRequest()
.get('/')
.expect(200);
});
});
  • 框架和應(yīng)用不同,應(yīng)用測試當(dāng)前代碼,而框架是測試框架代碼,所以會(huì)頻繁更換 baseDir 達(dá)到測試各種應(yīng)用的目的。
  • baseDir 有潛規(guī)則,我們一般會(huì)把測試的應(yīng)用代碼放到 test/fixtures 下,所以自動(dòng)補(bǔ)全,也可以傳入絕對路徑。
  • 必須指定 framework: true,告知當(dāng)前路徑為框架路徑,也可以傳入絕對路徑。
  • app 應(yīng)用需要在 before 等待 ready,不然在 testcase 里無法獲取部分 API
  • 框架在測試完畢后需要使用 app.close() 關(guān)閉,不然會(huì)有遺留問題,比如日志寫文件未關(guān)閉導(dǎo)致 fd 不夠。

緩存

在測試多環(huán)境場景需要使用到 cache 參數(shù),因?yàn)?nbsp;mm.app 默認(rèn)有緩存,當(dāng)?shù)谝淮渭虞d過后再次加載會(huì)直接讀取緩存,那么設(shè)置的環(huán)境也不會(huì)生效。

const mock = require('egg-mock');
describe('/test/index.test.js', () => {
let app;
afterEach(() => app.close());

it('should test on local', () => {
mock.env('local');
app = mock.app({
baseDir: 'apps/example',
framework: true,
cache: false,
});
return app.ready();
});
it('should test on prod', () => {
mock.env('prod');
app = mock.app({
baseDir: 'apps/example',
framework: true,
cache: false,
});
return app.ready();
});
});

多進(jìn)程測試

很少場景會(huì)使用多進(jìn)程測試,因?yàn)槎噙M(jìn)程無法進(jìn)行 API 級(jí)別的 mock 導(dǎo)致測試成本很高,而進(jìn)程在有覆蓋率的場景啟動(dòng)很慢,測試會(huì)超時(shí)。但多進(jìn)程測試是驗(yàn)證多進(jìn)程模型最好的方式,還可以測試 stdout 和 stderr。

多進(jìn)程測試和 mm.app 參數(shù)一致,但 app 的 API 完全不同,不過 SuperTest 依然可用。

const mock = require('egg-mock');
describe('/test/index.test.js', () => {
let app;
before(() => {
app = mock.cluster({
baseDir: 'apps/example',
framework: true,
});
return app.ready();
});
after(() => app.close());
afterEach(mock.restore);
it('should success', () => {
return app.httpRequest()
.get('/')
.expect(200);
});
});

多進(jìn)程測試還可以測試 stdout/stderr,因?yàn)?nbsp;mm.cluster 是基于 coffee 擴(kuò)展的,可進(jìn)行進(jìn)程測試。

const mock = require('egg-mock');
describe('/test/index.test.js', () => {
let app;
before(() => {
app = mock.cluster({
baseDir: 'apps/example',
framework: true,
});
return app.ready();
});
after(() => app.close());
it('should get `started`', () => {
// 判斷終端輸出
app.expect('stdout', /started/);
});
});


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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)