前端領域的插件式設計

插件,是一個常見的概念。

例如,當我們需要把我們前端代碼中的 css 樣式提取打包,我們可以用 webpack 的 mini-css-extract-plugin,或者你如果用 rollup 的話,可以選擇 rollup-plugin-postcss。

再比如我們可以給 babel 配置 @babel/plugin-proposal-decorators 插件來支持裝飾器語法;

除了上述打包編譯相關的工具,我們使用的代碼編輯器也都支持各式各樣的插件,動態地給軟件增加各種能力,例如通過 Prettier 插件來是 VsCode 支持 Prettier 的代碼格式化,或者安裝主題插件來改變軟件樣式等。

除此之外,一些前端領域的框架也都有插件的機制,譬如 Vue,DvaJS,Eggjs 等。React 也有一些插件化開發的框架,例如 DevExtreme Reactive (以下簡稱 DR),React Pluggable。

認識插件

那什麼是插件呢?

所謂插件(Plug-in / Plugin),是一種可以把某些能力或特性添加到某個已有主體程序的程序,它通常遵循一定的編寫規範,並只能運行在特定的主體程序中。

插件的設計能帶來許多好處,例如

雖然上面提到的軟件都有支持插件,但也各有特點。

從插件的深入程度上來說

而從插件面向的開發者來說,也有幾種方式

插件化設計的改造案例

除了我們去使用一個一個的插件,我們也可以把插件化的設計引入到我們自己的系統中,下面拋磚引玉,按照筆者的理解介紹一些基本的設計思路,當然插件的設計方式並非固定的,我們也不應當公式化地套用模式,核心在於體會其設計思想。

當我們設計一個插件系統時,我們要考慮幾個問題:

設想一個這樣一個例子,我們有一個簡易的計算器程序,支持加減法:

 class Calculator {
   construct(initial) {
     this.num = initial;
   }
   add(num) {
     this.num = this.num + num;
     return this;
   }
   subtract(num) {
     this.num = this.num - num;
     return this;
   }
   result() {
     return this.num;
   }
 }

 const myCalculator = new Calculator(5);
 myCalculator.add(5).subtract(2).result(); // 8

可以很容易想到,計算器的主要抽象是運算,即當前值和一個新值的運算過程,這部分是穩定的,而支持的運算邏輯是可擴展的,適合做成插件,因此我們可以做如下的改造:

 // 程序主體,定義程序核心邏輯是增加計算器運算能力
 class Calculator {
   plugins = [];
   construct(initial) {
     this.num = initial;
   }

   use(plugin) {
     this.plugins.push(plugin);
     this[plugin.name] = plugin.calculate.bind(this);
   }

   result() {
     return this.num;
   }
 }
 // 插件聲明
 interface Plugin {
   name: string;
   calculate(num: number) => this;
 }
 // 插件實現
 class AddPlugin implements Plugin {
   name: 'add',
   calculate(num) {
     this.num = this.num + num;
     return this;
   }
 }

 class SubtractPlugin implements Plugin {
   name: 'subtract',
   calculate(num) {
     this.num = this.num - num;
     return this;
   }
 }

 const myCalculator = new Calculator(5);
 // 插件安裝
 myCalculator.use(new AddPlugin());
 myCalculator.use(new SubtractPlugin());

 myCalculator.add(5).subtract(2).result(); // 8

經過這個改造,未來如果要實現乘法,我們只需要新增一個插件實現即可,無需修改程序主體:

 class MultiplicatiPlugin implements Plugin {
   name: 'multiplicati',
   calculate(num) {
     this.num = this.num * num;
     return this;
   }
 }

再比如我們要增加一個 help 來打印支持的計算也可以快速實現:

 class Calculator {
   ...
   help() {
     return `support ${this.plugins.map(plugin => plugin.name).join(',')}`;
   }
 }

通過上面的例子,從插件的角度可以分成幾個部分:

在一些更復雜的例子裏還會有 Plugin Loader 用於加載和管理 plugin。

插件設計的案例分享

我們已經對插件設計有了一個基本的認識,我們再來看看一些開源庫的插件設計案例

Webpack

webpack 核心模塊爲 compiler 和 compilation,他們都有各自的聲明週期鉤子(Hook),插件開發者可以藉助這些 Hook 來完成各種能力,因此 webpack 核心便是定義聲明週期(或者叫事件流),並在各個聲明週期中調用插件在對應聲明週期註冊的方法。不過在同一個生命週期註冊了多個事件時,我們還需要關注幾個問題:

上述問題 webpack 封裝了一個 Hook 的核心庫 Tapable, compiler 和 compilation 都是基於 Tapable 的實現,根據上面的維度提供了不同的 Hook 類:

img

網上有很多很不錯的專門介紹 Tapable 文章,這裏就不繼續展開介紹,但我們可以對其有個感性的認識,實際上可以把它看做加強版的 EventEmitter。不僅 webpack 會面臨這個問題,另一個構建工具 rollup 也能看到類似的模塊來解決這類問題,在其代碼中有一個 PluginDriver 模塊(https://github.com/rollup/rollup/blob/master/src/utils/PluginDriver.ts)就是起類似的作用。

DevExtreme Reactive

最後再來聊一聊一個比較有意思的 React 插件化框架 DevExtreme Reactive。

DevExtreme Reactive (以下簡稱 DR)是 DevExpress 公司開發的一個開源 React 組件庫,目前包含了 Grid / Chart / Scheduler 三個複雜組件,這三個組件都是基於一個插件化框架進行開發的。

一個插件的例子如下:

 import { PluginHost, Plugin, Template, Getter, Action } from '@devexpress/dx-react-core';

 export default function PluginRoot(props) {
   const { chidren } = props;
   return <PluginHost>{children}</PluginHost>;
 }

 export function FeatureA() {
   return (
     <Plugin >
       <Template >...</Template>
       <Getter  />
       <Action  />
     </Plugin>
   );
 }
 export function FeatureB() {
   return (
     <Plugin >
       <Template>...</Template>
       <Getter  />
       <Action  />
     </Plugin>
   );
 }

 // App.jsx
 import PluginRoot, { FeatrueA, FeatureB, ... FeatureN } from './MyComponnent;'

 const App = () =(
   <PluginRoot>
     <FeatureA />
     <FeatureB />
     ...
     <FeatrueN />
   </PluginRoot>
 );

 ReactDOM.render(<App />, rootNode);

其中:

插件的各個能力和註冊方式都採用組件形式引用,充分契合 React 書寫方式。

TodoApp

當然這些介紹還是不太直觀,我們來看一個具體的例子。

例如我們要做一個 Todo 應用,我們可以如下實現

 import { PluginHost, Plugin, Template, Getter, Action } from '@devexpress/dx-react-core';

 const TodoApp = ({ children }) ={
   return (
     <PluginHost>
       <TodoCore />

       {/* ========= plugins ========= */}
       <TodoHeader />
       <TodoList />
       <TodoStore />
     </PluginHost>
   )
 }

 const TodoCore = () ={
   return (
     <Plugin>
       {/* 基本的狀態管理 */}
       <TodoStore />
       <Template >
         {/* 定義佈局 */}
         <section class>
           <TemplatePlaceholder  />
           <TemplatePlaceholder  />
         </section>
       </Template>
     </Plugin>
   );
 };

 const TodoHeader = () ={...}
 const TodoList = () ={...}

 const App = () ={
   return (
     <TodoApp />
   )
 }

 ReactDOM.render(<App />, rootNode);

如果我們需要增加一個功能支持設置完成,我們可以通過編寫如下的插件來支持:

 // TodoCompletable.tsx
 const TodoCompletable = () ={
   ...
   return (
     <Plugin >
       {/* 擴展 main 的佈局,增加 footer,在 footer 中展示 complete 對應的操作 */}
       <Template >
         <TemplatePlaceholder />
         <TemplatePlaceholder  />
       </Template>

       {/* 擴展狀態和操作 */}
       <Getter  computed={getTodoWithCompletedStatus} />
       <Action  action={completeTodoAction} />
       <Action  action={activeTodoAction} />

       {/* 擴展 todoItem 展示,增加 checkbox */}
       <Template >
         {({ todo }) =(
           <TemplateConnector>
             {(getters, { completeTodo, activeTodo }) =(
               <div class>
                 <input
                   class
                   type="checkbox"
                   checked={todo.completed}
                   onChange={(e) ={
                     if (e.target.checked) {
                       completeTodo(todo.id);
                     } else {
                       activeTodo(todo.id);
                     }
                   }}
                 />
                 {/* TemplatePlaceholder 表示直接使用原來 todoItem 定義的展示 */}
                 <TemplatePlaceholder />
               </div>
             )}
           </TemplateConnector>
         )}
       </Template>


       {/* Footer 的具體實現 */}
       <TodoFooter />
     </Plugin>
   );
 };

 // 引入插件
 const TodoApp = () ={
   return (
     <PluginHost>
       {...}
       {/* completable plugin */}
       <TodoCompletable />
     </PluginHost>
   );
 };

可以看到,標籤功能完全插件化了,原來的代碼不需要做任何調整!而且相關功能完全在一個模塊中實現(如果我們正常 React 的寫法會在多個組件中感知邏輯,改動肯定是分散的,而目前的寫法我們可以輕鬆增加或移除此 feature)!詳細例子可以參考寫的例子:https://stackblitz.com/edit/dr-todo-demo?file=App.tsx

通過上面的介紹,想必大家可以感受到插件化設計的魅力了,感謝看到這裏,希望能對大家有所幫助。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/KdpLI0WBR_wyhGEbbqY6QQ