1. 程式人生 > 實用技巧 >淺析如何設計一個JavaScript外掛系統

淺析如何設計一個JavaScript外掛系統

  外掛是庫和框架的常見功能,並且有一個很好的理由:它們允許開發人員以安全,可擴充套件的方式新增功能。這使核心專案更具價值,並建立了一個社群——所有這些都不會增加額外的維護負擔。那麼如何去構建一個外掛系統呢?讓我們用 JavaScript 構建一個我們自己的外掛來回答這個問題。

一、如何構建一個簡單的外掛系統

  讓我們從一個名為 BetaCalc 的示例專案開始。BetaCalc 的目標是成為一個簡約的 JavaScript 計算器,其他開發人員可以在其中新增“按鈕”。以下是一些基本的入門程式碼:

// 計算器
const betaCalc = {
  currentValue: 0,

  setValue(newValue) {
    
this.currentValue = newValue; console.log(this.currentValue); }, plus(addend) { this.setValue(this.currentValue + addend); }, minus(subtrahend) { this.setValue(this.currentValue - subtrahend); }, }; // 使用計算器 betaCalc.setValue(3); // => 3 betaCalc.plus(3); // => 6 betaCalc.minus(2
); // => 4

  我們將計算器定義為一種客觀事物,以使事情變得簡單,計算器通過 console.log 列印結果來工作。目前功能確實很有限,我們有一個 setValue 方法,該方法接受一個數字並將其顯示在“螢幕”上。我們還有加法(plus)和減法(minus)方法,它們將對當前顯示的值執行一個運算。

  現在該新增更多功能了。首先建立一個外掛系統。

  我們將從建立一個註冊(register)方法開始,其他開發人員可以使用該方法向 BetaCalc 註冊外掛

  該方法的工作很簡單:獲取外部外掛,獲取其 exec 函式,並將其作為新方法附加到我們的計算器上:

// 計算器
const
betaCalc = { // ...其他計算器程式碼在這裡 register(plugin) { const { name, exec } = plugin; this[name] = exec; }, };

  下面這是一個示例外掛,為我們的計算器提供了一個“平方(squared)”按鈕:

// 定義外掛
const squaredPlugin = {
  name: "squared",
  exec: function () {
    this.setValue(this.currentValue * this.currentValue);
  },
};

// 註冊外掛
betaCalc.register(squaredPlugin);

  使用定義的 register() 方法註冊外掛,將外掛提供的方法新增到計算器上。

  在許多外掛系統中,外掛通常分為兩個部分:

(1)要執行的程式碼

(2)元資料(例如名稱,描述,版本號,依賴項等)

  在我們的外掛中,exec 函式是我們要執行的程式碼,name 是我們的元資料。

  註冊外掛後,exec 函式將作為一種方法直接附加到我們的 betaCalc 物件,從而可以訪問 BetaCalc 的 this。現在,BetaCalc 有一個新的“平方”按鈕,可以直接呼叫:

betaCalc.setValue(3); // => 3
betaCalc.plus(2); // => 5
betaCalc.squared(); // => 25
betaCalc.squared(); // => 625

  這個系統有很多優點。該外掛是一種簡單的物件字面量,可以傳遞給我們的函式。這意味著外掛可以通過 npm 下載並作為 ES6 模組匯入。易於分發是超級重要的。但是我們的系統有一些缺陷。

  通過為外掛提供訪問 BetaCalc 的 this 許可權,他們可以對所有 BetaCalc 的程式碼進行讀/寫訪問。雖然這對於獲取和設定 currentValue 很有用,但也很危險。如果外掛要重新定義內部函式(如 setValue),則它可能會為 BetaCalc 和其他外掛產生意外的結果。這違反了開放-封閉原則即一個軟體實體應該是開放的擴充套件,但封閉修改。

  另外,“squared”函式通過產生副作用發揮作用。這在 JavaScript 中並不少見,但感覺並不好——特別是當其他外掛可能處在同一內部狀態的情況下。一種更實用的方法將大大有助於使我們的系統更安全、更可預測。

二、更好的外掛架構

  讓我們再來看看一個更好的外掛架構。下一個例子同時改變了計算器和它的外掛 API:

// 計算器
const betaCalc = {
  currentValue: 0,

  setValue(value) {
    this.currentValue = value;
    console.log(this.currentValue);
  },

  core: {
    plus: (currentVal, addend) => currentVal + addend,
    minus: (currentVal, subtrahend) => currentVal - subtrahend,
  },

  plugins: {},

  press(buttonName, newVal) {
    const func = this.core[buttonName] || this.plugins[buttonName];
    this.setValue(func(this.currentValue, newVal));
  },

  register(plugin) {
    const { name, exec } = plugin;
    this.plugins[name] = exec;
  },
};

// 我們得外掛,平方外掛
const squaredPlugin = {
  name: "squared",
  exec: function (currentValue) {
    return currentValue * currentValue;
  },
};

betaCalc.register(squaredPlugin);

// 使用計算器
betaCalc.setValue(3); // => 3
betaCalc.press("plus", 2); // => 5
betaCalc.press("squared"); // => 25
betaCalc.press("squared"); // => 625

  我們在這裡做了一些值得注意的更改。

  首先,我們將外掛與“核心(core)”計算器方法(如 plus 和 minus)分開,方法是將其放入自己的外掛物件中。將我們的外掛儲存在plugins 物件中可使我們的系統更安全。現在,訪問此 plugins 的外掛將看不到 BetaCalc 屬性,而只能看到 betaCalc.plugins 的屬性。

  其次,我們實現了一個 press 方法,該方法按名稱查詢按鈕的功能,然後呼叫它。現在,當我們呼叫外掛的 exec 函式時,我們將當前的計算器值(currentValue )傳遞給該函式,並期望它返回新的計算器值。

  本質上,這個新的 press 方法將我們所有的計算器按鈕轉換為純函式。他們獲取一個值,執行一個操作,然後返回結果。這有很多好處:

  • 它簡化了 API。
  • 它使測試更加容易(對於 BetaCalc 和外掛本身)。
  • 它減少了我們系統的依賴性,使其更鬆散地耦合在一起。

  這種新架構比第一個示例受到更多限制,但效果很好。我們基本上為外掛作者設定了護欄,限制他們只能做我們希望他們做的改動。實際上,它可能太嚴格了!現在,我們的計算器外掛只能對 currentValue 進行操作。如果外掛作者想要新增高階功能(例如“記憶”按鈕或跟蹤歷史記錄的方式),那麼他們將無能為力。

  也許這就是好的。你給外掛作者的能力是一種微妙的平衡。給他們太多的權力可能會影響你專案的穩定性。但給它們的權力太小,它們就很難解決自己的問題——在這種情況下,你還不如不要外掛。

三、改善優化

  我們還有很多工作可以改善我們的系統。

  如果外掛作者忘記定義名稱或返回值,我們可以新增錯誤處理以通知外掛作者。像 QA 開發人員一樣思考並想象一下我們的系統如何崩潰,以便我們能夠主動處理這些情況,這是很好的。

  我們可以擴充套件外掛的功能範圍。當前,一個 BetaCalc 外掛可以新增一個按鈕。但是,如果它還可以註冊某些生命週期事件的回撥(例如當計算器將要顯示值時)怎麼辦?或者說,如果有一個專門的地方讓它在多個互動中儲存一段狀態呢?這會不會開闢一些新的用例?

  我們還可以擴充套件外掛註冊的功能。如果一個外掛可以通過一些初始設定來註冊呢?這是否能使外掛更加靈活?如果一個外掛作者想註冊一整套按鈕,而不是一個單一的按鈕——比如“BetaCalc 統計包”?需要做哪些改動來支援呢?

  BetaCalc 及其外掛系統都非常簡單。如果你的專案較大,則需要探索其他一些外掛架構。

  一個很好的起點就是檢視現有專案,以獲取成功的外掛系統的示例。比如:jQuery,D3,CKEditor 或其他。你還需要熟悉各種 JavaScript 設計模式,每種模式都提供了不同的介面和耦合程度,這給你提供了很多好的外掛架構選擇。瞭解這些選項有助於你更好地平衡使用你的專案的每個人的需求。

  除了模式本身之外,你還可以借鑑許多好的軟體開發原則來做出此類決策。我已經提到了一些方法(例如開閉原則和鬆散耦合),但是其他一些相關的方法包括 Demeter 定律和依賴注入。

  我知道這聽起來很多,但你必須進行研究。沒有什麼比讓每個人都重寫他們的外掛更痛苦的了,因為你需要更改外掛架構。這是一種快速失去信任的方式,讓人們失去對未來貢獻的信心。

  從頭開始編寫好的外掛架構是困難的!你必須平衡很多考慮因素,才能建立一個滿足大家需求的系統。它是否足夠簡單?功能夠強大嗎?它是否能長期工作?不過這也是值得的,有一個好的外掛系統對大家都有幫助,開發者可以自由地解決他們的問題。終端使用者可以從中選擇大量可選擇的特性。你可以圍繞你的專案建立一個生態系統和社群。這是一個三贏的局面。