淺析如何設計一個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 定律和依賴注入。
我知道這聽起來很多,但你必須進行研究。沒有什麼比讓每個人都重寫他們的外掛更痛苦的了,因為你需要更改外掛架構。這是一種快速失去信任的方式,讓人們失去對未來貢獻的信心。
從頭開始編寫好的外掛架構是困難的!你必須平衡很多考慮因素,才能建立一個滿足大家需求的系統。它是否足夠簡單?功能夠強大嗎?它是否能長期工作?不過這也是值得的,有一個好的外掛系統對大家都有幫助,開發者可以自由地解決他們的問題。終端使用者可以從中選擇大量可選擇的特性。你可以圍繞你的專案建立一個生態系統和社群。這是一個三贏的局面。