前端外掛化架構的探索和實踐

OPPO網際網路技術發表於2020-09-21

babel外掛、webpack外掛、vue-cli外掛,為啥這麼多的優秀框架都是使用外掛系統?外掛化架構是什麼?帶來了什麼好處?可以應用到什麼場景呢?

1. 外掛化架構定義

外掛化架構又稱微核架構,指的是軟體的核心相對較小,主要功能和業務邏輯都通過外掛實現。外掛化架構一般有兩個核心的概念:核心和外掛。

  • 核心(pluginCore)通常只包含系統執行的最小功能;
  • 外掛(plugin)則是互相獨立的模組,一般會提供單一的功能。

核心一般會將要完成的所有業務進行抽象,抽象出最小粒度的基礎介面,供外掛方來呼叫。這樣,外掛開發的效率將會極大的提高。比方說,瀏覽器就是一個典型的外掛化架構,瀏覽器是核心,頁面是外掛,這樣通過不同的URL地址載入不同的頁面,來提供非常豐富的功能。而且,我們開發網頁時候,瀏覽器會提供很多API和能力,這些介面通過 window來掛載, 比如,DOM、BOM、Event、Location等等。

設計一個完善的外掛化架構的系統,包含三要素:

  • plugCore:外掛核心,提供外掛執行時,管理外掛的載入、執行、解除安裝等生命週期(類比瀏覽器);
  • pluginAPI:外掛執行時需要的基礎介面(類比瀏覽器例子,相當於window);
  • plugin:一系列特定功能的獨立模組(類比瀏覽器例子,相當於不同的網頁)。

2. 外掛化架構的實踐

我們將從plugCorepluginAPIplugin三要素,來解析jQuery、Babel和Vue CLI這三大優秀的開源庫其外掛化架構的實踐。

2.1 jQuery的外掛化架構

jQuery 是一個 JavaScript 庫,極大地簡化了JavaScript 程式設計,用更少的程式碼完成更多工作。早期瀏覽器的標準不統一,開發網頁需要相容不同瀏覽器的使用者使用是一件十分痛苦的事情。jQuery在適配了不同瀏覽器的差異的基礎上提供了更加完善易用API,供前端開發人員完成網頁程式設計,使用jQuery編寫的網頁,在一套程式碼下也可以在不同廠商的瀏覽器上正常執行。在 MV* 框架流行之前,jQuery是絕對的扛霸子。jQuery是可擴充套件的,其擁有完善的外掛體系,網頁開發所需要的各種外掛在其生態都可以找到。我們解析一下jQuery外掛體系。

外掛定義:

特別說明:$.fn = jQuery.protype(外掛精髓)。jQuery的外掛機制通過原型鏈來掛載。

外掛機制執行過程

demo 示例

$app便可以在原型鏈上查詢到myPlugin

從三要素來總結:

  • pluginCore:通過原型鏈賦值來擴充套件不同的外掛,再獲得jQuery例項後可以被呼叫。
  • pluginAPI:jQuery包的核心介面,(jQuery依靠其優異的Api取勝)
  • plugin:無限制,可以是JavaScript的型別,一般是實現具體功能的模組,比如,日期選擇器等。

2.2 Babel的外掛化架構

Babel 是一個工具鏈,主要用於將 ECMAScript 2015+ 版本的程式碼轉換為向後相容的 JavaScript 語法,以便能夠執行在當前和舊版本的瀏覽器或其他環境中。在程式碼轉換的過程中會涉及許多特性和語法的轉換,而且ECMAScript的提案總是不斷地更新。如何組織大量(不斷增加)的轉換規則呢?我們來看看 Babel的工作原理。

Babel轉換原始碼,分為三個步驟:

  • 解析(parse):進行詞法分析(Lexical Analysis)和語法分析(Syntactic Analysis)以生成抽象語法樹(AST);
  • 轉換 (transform):遍歷AST中每個節點並進行相應的轉換操作,該過程通過使用不同的外掛來實現各種特性和語法的轉換;
  • 生成 (generate):根據AST生成目的碼。

Babel在AST轉換的過程(即上圖的第2步),便使用外掛化架構,下面將會詳細講解這個轉換過程的外掛化架構的使用。

外掛定義:

外掛是一個函式,返回值是一個包含visitor的物件。外掛定義的部分概念說明:

  • name:外掛名
  • pluginAPI: 外掛執行時傳入的API
  • visitor: 是一個物件,物件的key是AST的每節點的型別,物件的值是一個函式,AST轉換的過程便在這裡發生的。
  • nodePath:是一個AST的節點的例項物件,詳細可以參考:@babel/parser/src/parser/node.js [1],其中, type欄位 : 該節點的型別,常見型別:VariableDeclaration(變數宣告)、VariableDeclarator(變數宣告表示式)、ArrowFunctionExpression(箭頭函式表示式)等等,詳細可以參考@babel/types [2]。

(筆者認為pluginAPI還應包括nodePath,因為,每個節點例項除了語法和詞法描述,還包含需求語法間的轉換方法)

外掛示例

箭頭函式轉換成普通函式的外掛:@babel/plugin-transform-arrow-functions [3]原始碼:

外掛的執行思路:

  • 第一步,執行該外掛,獲取到包含visitor物件;
  • 第二步,ATS遍歷節點,檢測nodePath的type === 'ArrowFunctionExpression',尋找到vistor物件的中key為 ArrowFunctionExpression的函式;
  • 第三步,將nodePath傳入該函式進行呼叫(AST在這步被修改);

單個外掛的執行思路很明確了,那麼在ATS遍歷過程,怎麼做到多個外掛一起工作呢?

Babel在轉換原始碼過程中,外掛化架構的工作流程是這樣的:

  • 第一步:通過解析babel的配置檔案(或者命令列--plugins引數),獲取Babel配置的所有外掛的描述;
  • 第二步,將外掛的require進記憶體,獲得外掛函式,並執行外掛函式,獲取到多個包含vistor欄位物件;(詳細邏輯:@babel/core/src/config/full.js [4])
  • 第三步,將多個包含vistor欄位物件整合成一個大的visitor原始碼展示(詳細邏輯:@babel/core/src/transformation/index.js [5]):

合併後的visitor物件:

visitor的物件中的值變成了 Array<function(nodePath)>

  • 第四步,AST遍歷時,每個節點根據 NodeType,來獲取 visitor[NodeType],並依次執行。

從三要素來總結:

  • pluginCore:外掛載入並整合(即vistor合併),AST遍歷期間是呼叫查詢vistor[NodeType]並依次呼叫;
  • pluginAPI:nodePath,提供不同型別節點的介面來轉換AST節點;
  • plugin:visitor[NodeType]=function(nodepath)。

2.3 vue-cli的外掛化架構

Vue CLI 是一個基於 Vue.js 進行快速開發的完整系統。CLI外掛是向你的 Vue 專案提供可選功能的 npm 包,例如 Babel/TypeScript 轉譯、ESLint 整合、單元測試和 end-to-end 測試等。Vue CLI 外掛的名字以 @vue/cli-plugin- (內建外掛) 或 vue-cli-plugin- (社群外掛) 開頭,非常容易使用。下面,我們將會解析cli外掛的定義、執行、安裝等過程。

外掛定義

外掛必須是vue-cli-plugin-命名的npm包,並且目錄結構也是要嚴格遵循檔案命名來定義。

注意:@vue/cli-service [6],會通過 專案根目錄下package.json中dependencies和devDependencies中定義的 npm包中符合外掛命名規範的 npm包作為專案的外掛。

檔案命名和內容說明:

  • generator.js:會在外掛被新增時執行,可以安裝npm包、修改專案原始碼等功能;

  • prompts.js:所有的對話邏輯都儲存在 prompts.js 檔案中。對話內部是通過 inquirer [7] 實現,在呼叫其獲得安裝選項結果,並在 generator.js呼叫時作為引數存入;

  • index.js:Service外掛的入口,@vue/cli-service [8]啟動時被執行

詳情可以去看Vue Cli 外掛開發指南 [9]

我們把Vue CLI的外掛執行分成兩種情況:

  • 第一種:外掛未安裝,外掛被新增的時候呼叫(prompts.js + generator.js
  • 第二種:外掛已安裝,外掛系統啟動時被執行(index.js

第一種 安裝流程

相比Babel的手動安裝新增外掛方式,Vue CLI的外掛系統提供命令列的安裝方式就顯得很方便了。我們看看Vue Cli外掛系統時怎麼實現一行命令新增外掛的功能。

安裝流程的執行思路如下:

  • 第一步:從命令列引數解析出外掛名,使用npm(yarn)install vue-cli-plugin-xxx 安裝外掛,原始碼位置:@vue/cli/lib/add.js [10]
  • 第二步:require('vue-cli-plugin-xxx/prompts'),並獲取使用者安裝是選項結果pluginOptions,原始碼位置:@vue/cli/lib/invoke.js [11]

  • 第三步:使用pluginName和pluginOptions作為引數構成出Generator [12]物件的例項

  • 第四步:執行generator.generate方法。這步包括了三個關鍵步驟:

    1)require(vue-cli-plugin-xxx/generator),獲得外掛的執行函式;

    2)構建GeneratorAPI(即pluginAPI);

    3)呼叫generator.js匯出函式。

  • 詳細程式碼:_@vue/cli/lib/Generator.js [113]_
  • 第五步:將外掛的引數新增到vue.config.js檔案中。

第二種 執行流程

外掛執行流程是由@vue/cli-service [14]這個外掛系統定義的,這裡的呼叫外掛有兩種:

  • 第一種 內建外掛@vue/cli-service的命令和配置相關,將系外掛統功能拆分出多個內建外掛,在外掛系統中預設呼叫);
  • 第二種 專案外掛,package.json 中定義的npm包名符合外掛命名規範)。

外掛執行邏輯很簡單:

這兩個流程的 pluginAPI是不一樣的。

  • 安裝流程:@vue/cli/lib/GeneratorAPI [15]
  • 執行流程:@vue/cli-service/lib/PluginAPI [16]

從三要素來總結:

1)安裝流程

  • pluginCore:@vue/cli [17]通過命令列引數獲得外掛包名,然後安裝外掛的npm包,並執行prompts.js 獲得使用者安裝選項結果,然後,使用選項結果和generator.js作為引數構造出generator,並在呼叫generator.generate中執行generator.js函式;
  • pluginAPI:GeneratorAPI [18],提供了原始碼修改、npm包管理、模版檔案生成等功能;
  • plugin:由prompts.jsgenerator.js組成,解決某種能力植入專案時,要處理的依賴。

2)執行流程

  • pluginCore:@vue/cli-service [19],通過package.json中獲得專案外掛後,與系統內建外掛合併,最後依次執行;
  • pluginApI:PluginAPI [20],提供webpack配置修改和命令管理的能力;
  • pluginindex.js檔案,在不同命令下進行工作。

一個外掛系統是可以不多個外掛型別,並且外掛系統通過命令安裝外掛的實現,使用者在使用外掛系統時新增外掛也是十分方便的。

3. 外掛化架構的應用

3.1 應用場景

通過上述的例項,總結處理外掛架構的應用場景。

  • 第一種:富pluginAPI場景:程式碼在多種場景中執行,需要抹平場景中差異。(jQuery);
  • 第二種:富plugin場景,外掛系統,可預期需求會越來越多,適合通過更多的外掛來簡化系統的程式碼量(Babel)
  • 第三種:富pluginCorepluginAPI場景,外掛系統本身非常複雜,需要對開發人員要求極高,這時候,將複雜的工作放到核心和中pluginApi實現,剩下大部分的簡單的編碼工作留給外掛方實現,外掛方藉助pluginApi也可以快速完成業務開發(Vue CLI)

3.2 發展方向

通過建立一個外掛標準,將研發流程沉澱的能力進行外掛化程式設計,整個公司通過使用一套的外掛系統(中臺),這樣意味著,我們不用重複造業務輪子,團隊和企業可以持續積累自己的外掛生態,讓軟體開可以像汽車等工業製造一樣,打造一條標準化裝配的流水線。

參考連結:

  1. https://github.com/babel/babe...
  2. https://babeljs.io/docs/en/ba...
  3. https://babeljs.io/docs/en/ne...
  4. https://github.com/babel/babe...
  5. https://github.com/babel/babe...
  6. https://github.com/vuejs/vue-...
  7. https://github.com/SBoudrias/...
  8. https://github.com/vuejs/vue-...
  9. https://cli.vuejs.org/zh/dev-...
  10. https://github.com/vuejs/vue-...
  11. https://github.com/vuejs/vue-...
  12. https://github.com/vuejs/vue-...
  13. https://github.com/vuejs/vue-...
  14. https://github.com/vuejs/vue-...
  15. https://github.com/vuejs/vue-...
  16. https://github.com/vuejs/vue-...
  17. https://github.com/vuejs/vue-...
  18. https://github.com/vuejs/vue-...
  19. https://github.com/vuejs/vue-...
  20. https://github.com/vuejs/vue-...

image.png

相關文章