首頁>技術>

背景

回想下,當你需要新起一個 Node.js 應用的時候,會怎麼做?

憨厚一點的就從頭開始初始化,一個個外掛的安裝,CTRL +C 一個個的配置。好一點的,就會封裝一個骨架,然後一鍵生成新專案。

那如果在應用中的一些實踐,想下沉為基礎能力,就需要修改骨架。此時,如何把舊專案升級呢?一兩個還好說,如果十幾個,甚至上百個呢?

我們的實踐是:基於 Egg 封裝一個適合特定團隊業務場景的上層業務框架。

如果你的團隊需要:

統一的技術選型,比如資料庫、模板、前端框架及各種中介軟體設施都需要選型,而框架封裝後保證應用使用一套架構。統一的預設配置,開源社群的配置可能不適用於公司,而又不希望每個應用重複配置。統一的部署方案,通過框架和平臺的雙向控制,應用只需要關注自己的程式碼。統一的程式碼風格,框架不僅僅解決程式碼重用問題,還可以對應用做一定約束,並定製適合團隊的目錄載入規範。鴨蛋炒雞蛋 = ?

下面,我們來一起基於Egg 定製一個獨屬於我們的 鴨蛋框架(yadan),它提供以下能力:

內建 nunjucks來提供服務端模板渲染能力。封裝一套請求後端介面的協議,並自動載入 app/rpc/** 為ctx.rpc.clz.method() 方法。
// 請求後端介面,查詢使用者資訊const userInfo = await ctx.rpc.user.getDetail('yadan');// 渲染首頁await this.ctx.render('home.tpl', { userInfo });

完整的示例程式碼可以參見 https://github.com/atian25/yadan,下文我們會講解關鍵細節。

初始化

通過骨架一鍵初始化 Framework 程式碼:

$ npm init egg --type=framework yadan

可以看到,Framework 的目錄結構,和一個 Egg 應用幾乎一模一樣,熟悉的 config、 app/extend 、 app/service 。

yadan├── app│   ├── extend│   └── service├── config│   ├── config.default.js│   └── plugin.js├── lib│   └── framework.js├── test│   ├── fixtures│   └── framework.test.js├── README.md├── index.js└── package.json

接下來我們逐個講解下關鍵細節。

框架定義

首先來看下入口檔案,其實就是繼承了下 Application,然後把當前目錄通過EGG_PATH 的約定,加入到 Egg 的LoadUnits中去。

骨架已經預設生成,基本上不用改,程式碼如下:

// lib/framework.jsconst path = require('path');const egg = require('egg');const EGG_PATH = Symbol.for('egg#eggPath');class Application extends egg.Application {  get [EGG_PATH]( "EGG_PATH") {    return path.dirname(__dirname);  }}class Agent extends egg.Agent {  get [EGG_PATH]( "EGG_PATH") {    return path.dirname(__dirname);  }}module.exports = Object.assign(egg, {  Application,  Agent,});
內建外掛

我們要內建模板外掛,先安裝依賴:

tnpm i --save egg-view-nunjucks

再掛載下外掛:

// config/plugin.jsexports.nunjucks = {  enable: true,  package: 'egg-view-nunjucks',};
預設配置

可以設定統一的預設配置,如把預設的模板引擎設定為 nunjucks :

// config/config.default.jsmodule.exports = () => {  const config = {};  config.view = {    defaultViewEngine: 'nunjucks',    mapping: {      '.nj': 'nunjucks',      '.tpl': 'nunjucks',    },  };  return config;};
RPC 規範

除了常規的擴充套件外,在實際業務開發中,我們往往需要為團隊定製一些新的目錄規範。

此處我們來定義一個 RPC 規範:

約定 app/rpc/** 將被掛載為 ctx.rpc.**提供 egg.RPC 基類,對後端請求進行封裝,供應用層繼承。定義 RPC 基類

直接 show me the code ,其實就是對 HTTP 協議做了一個上層封裝,統一了響應格式。

該 RPC 類在framework.js裡面會被引入到 egg 物件上。

// lib/rpc.jsclass RPC {  constructor(ctx) {    this.ctx = ctx;    this.app = ctx.app;    this.logger = ctx.logger;    this.config = ctx.app.config;  }  async api(apiName, data) {    const host = this.config.rpc.host;    try {      const targetUrl = `${host}/api${apiName}`;      this.logger.info(`[RPC] request api: ${targetUrl}`);      const res = await this.ctx.curl(targetUrl, {        dataType: 'json',        contentType: 'json',        timeout: 5000,        data,      });      return this.handlerResult(res, { apiName, data });    } catch (err) {      return this.handlerError(err, { apiName, data });    }  }  handlerResult(res) {    return {      success: true,      data: res.data,    };  }  handlerError(err, meta) {    this.logger.error(`[RPC] request ${meta.apiName} fail: ${err.message}`);    return {      success: false,      error: {        message: err.message,      },    };  }}module.exports = RPC;
RPC 載入邏輯

在 《如何為團隊量身定製 Egg 目錄掛載規範?》[1]一文中有專門介紹過。

此處我們僅需要簡單配置下:

// config/config.default.jsmodule.exports = () => {  const config = {};  // ...  // 自定義載入規範  config.customLoader = {    rpc: {      directory: 'app/rpc',      inject: 'ctx',      loadunit: true,    },  };  return config;};

然後我們如果在應用中新增app/rpc/user.js檔案:

// app/rpc/user.jsconst { RPC } = require('egg');module.exports = class TestRPC extends RPC {  async getDetail(id) {    return await this.api('/user/detail', { id });  }};

在 Controller 那邊就可以直接呼叫 ctx.rpc.user.getDetail()了。

class HomeController extends Controller {  async detail() {    const { ctx } = this;    const name = ctx.params.name;    const { data: userInfo } = await ctx.rpc.user.getDetail(name);    await ctx.render('home.tpl', userInfo);  }}
單元測試

單元測試很重要,尤其是 Framework 必須要求 100% 的測試覆蓋率。

首先需要新增 fixtures ,可以看到,就是一個標準的 Egg 應用,用來模擬我們的業務場景。

└── test    ├── fixtures    │   └── example    │       ├── app    │       │   ├── rpc    │       │   │   └── user.js    │       │   ├── controller    │       │   │   └── home.js    │       │   └── router.js    │       ├── config    │       │   └── config.default.js    │       └── package.json    └── framework.test.js

然後編寫一個個的單測:

跟 Egg 應用的單元測試幾乎沒區別,只是多了一個framework: true 的宣告。

// test/framework.test.jsconst mock = require('egg-mock');describe('test/framework.test.js', () => {  let app;  before(() => {    app = mock.app({      baseDir: 'example',      // 宣告是測試 Framework      framework: true,    });    return app.ready();  });  after(() => app && app.close());  afterEach(mock.restore);  it('should GET /', async () => {    return app.httpRequest()      .get('/')      .expect('<div>yadan</div>\\n')      .expect(200);  });});

如果你的 Framework 提供了多個功能,我們建議拆為多個 fixtures,一個特性一個特性的測試,並覆蓋完全。

通過 npm run cov 來檢視你的單元測試覆蓋率,我們內建骨架也幫你自動生成了GitHub Action的 CI 測試配置。

釋出流程

跟平時釋出 npm 沒啥區別,此處介紹下我們的一些最佳實踐。

本地驗證

如果你想在釋出前先測試,首先可以通過npm link 方式來短鏈到應用中

$ cd /path/to/demo$ npm link /path/to/framework

詳情參見你所不知道的模組除錯技巧 - npm link #17[2]

釋出 beta

接著就可以釋出測試版本了,此時可以先發0.x :

修改 package.json 為0.0.1釋出指令為 npm publish --tag=beta在應用引入時為 npm i --save @eggjs/yadan@beta

這樣的好處是,在 0.x 升級新版本的時候,應用那邊能安裝到最新的版本。

因為根據Semver規則, ^0.0.1 是安裝不到 0.1.0 等版本的。

釋出正式

當 beta 驗證通過後,應該果斷的釋出 1.x 版本,禁止停留在 0.x 版本,否則你會踩坑。

Chromium 等都版本低了,你吝嗇個啥啊,版本號又不值錢。

修改 package.json 為 1.0.0釋出指令去掉 beta,改為 npm publish在應用引入時為 npm i --save @eggjs/yadan

後續發版本,要嚴格遵循 **Semver **規則,不能有 break change,且要求應用不鎖版本,通過^1 的方式引入依賴。

如果實在無法相容,就發大版本,且最好提供 codemod 來幫舊應用自動升級。

應用層

在應用中使用你的框架很簡單,只需要在 package.json 簡單宣告下:

{  "name": "egg-showcase",  "egg": {    "framework": "@eggjs/yadan"  },  "dependencies": {    "yadan": "^1"  }}

然後正常啟動即可,會看到以下資訊:

[master] yadan started on http://127.0.0.1:7001 (1511ms)

這樣,所有依賴這個 Framework 的應用,都可以使用它提供的標準化能力和團隊規範。

框架的框架

至此,我們就已經完成了一個基於 Egg 的上層業務框架的開發,是不是覺得很簡單?

簡單就對了!Egg 本身的定位就是框架的框架,幫助團隊的技術負責人,來定製適合特定的業務場景的上層業務框架。

在阿里內部也是這麼實踐的:

實際上,框架還支援多層繼承,在我們內部的繼承關係其實是:

特定場景框架:      chair-serverless  |   midway-faas    |                          ↑                 ↑團隊業務框架:            chair       |     midway       |   nut     | ...                          ↑                 ↑               ↑         ↑阿里統一框架:           @ali/egg                          ↑                 ↑               ↑         ↑開源社群框架:             egg
框架的演進

從上面可以看到,Egg 的應用、外掛、框架的目錄結構幾乎一模一樣。

實際開發過程中,我們也有一套漸進式的演進方式,分享給大家:

實驗性的功能,可以先在應用裡面實現,作為 inline plugin 通過 path 方式來掛載。功能穩定後,就抽出來變為獨立的外掛,應用再通過 npm 依賴方式引入,只需改兩行程式碼即可。當該功能成熟後,成為團隊的統一規範時,直接把這個外掛整合到 Framework 中,所有應用只需重新安裝下依賴,即可立刻享受到。

這個過程是閉環的,是漸進式,而且升級過程幾乎不同。

詳見文件漸進式開發[3]

最後補一張之前的 Slide:

寫在最後

希望通過本文,讓大家了解到 Egg 的三個概念,也能一窺我們如此設計架構的原因。

一個人的專案怎麼樣都無所謂,但當大規模應用的時候,數千個應用分佈到數十個團隊裡面,此時的生態共建、差異化定製、應用治理能力,就變為一個很複雜的工程問題了(可以思考下這種規模下如何推動框架升級和治理)。

這也是我們為什麼做 Egg 的初心,它的定位就是框架的框架,專注於提供一套 Loader 規範和外掛框架體系,目標使用者是團隊的架構師。 它本身是不能跟市面上的框架直接對比的,基於它搭建的上層業務框架,才是一個合適的框架對比物件。

但實際上,框架只是整個鏈路中的很小的一點,Egg 也已經是我們 3 年前的實踐了。

如何讓前端同學可以在不增加額外學習成本的情況下,無感無痛地使用服務端能力,目前還有非常多急需解決的問題,需要深入到 PaaS、中介軟體基礎設施、研發平臺等等層面。我們還在路上,正致力於為螞蟻提供 輕研發、免運維 的下一代 Node.js 研發方案。

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • Dubbo 高危漏洞!原來都是反序列化惹得禍