插件機制詳解

簡介

插件模式是一種應(yīng)用非常廣泛的模式者蠕。我們用的很多軟件都擁有自身的插件機制檩奠,通過插件可以拓展軟件的功能翁锡。另外蔓挖,插件模式也廣泛應(yīng)用于 web 方面。例如 Webpack盗誊、 Vue CLI时甚、UMI、Babel等哈踱。

那么插件系統(tǒng)是如何實現(xiàn)的呢荒适?


image.png
image.png

如上圖所示,插件應(yīng)用的流程很簡單:

  1. 應(yīng)用啟動开镣,執(zhí)行初始化
  2. 查找和加載插件
  3. 調(diào)用插件
  4. 運行主應(yīng)用

其中刀诬,第 3 步的時候,回去調(diào)用插件邪财,調(diào)用插件時會在主應(yīng)用或者狀態(tài)庫中添加一系列的屬性和鉤子陕壹。插件調(diào)用完畢后,在主應(yīng)用中就可以使用這些被插件添加的屬性和鉤子树埠,以此來拓展應(yīng)用的功能糠馆。

關(guān)鍵地方在于插件的形式及插件接口的設(shè)計。

插件的形式多種多樣怎憋,不同的應(yīng)用有不同的設(shè)計又碌。例如 Webpack 插件是一個對象,必須對外暴露一個 apply 方法绊袋;UMI 及 VUE CLI 的插件是函數(shù)的形式毕匀。
毫無疑問,每種插件系統(tǒng)都提供了固定的插件 API 供插件開發(fā)者使用癌别,插件 API 的設(shè)計也是一個重點皂岔。

那么現(xiàn)在,我們可以根據(jù)以上的流程實現(xiàn)一個簡單的擁有插件系統(tǒng)的 Demo展姐。

簡單 Demo 實現(xiàn)

這里躁垛,我們規(guī)定我們的插件是一個函數(shù)剖毯,接收 PluginApi 實例作為參數(shù)。

  • 實現(xiàn)主應(yīng)入口

假如我們的應(yīng)用入口非常簡單教馆,實例化主應(yīng)用類速兔,執(zhí)行 run 方法,如下所示

import { Service } from './Service';

const service = new Service({});

service.run('command name');
  • 實現(xiàn)主應(yīng)用類

詳細說明見代碼注釋

import { resolve } from 'path';
import { PluginApi } from './PluginApi';
import { AsyncSeriesWaterfallHook } from 'tapable';

export interface StoreState {
  beforeMiddleWares: Function[];
  cwd: string;
  hooks: Record<string, AsyncSeriesWaterfallHook<any, any>>;
  commands: Record<string, () => any>;
}

export interface ServiceOpts {
  cwd?: string;
}

export class Service {
  private cwd: string;
  private store: StoreState;
  private plugins: { name: string; fn: Function }[];

  constructor(opts: ServiceOpts) {
    const { cwd } = opts;
    this.cwd = cwd || process.cwd();
    // 創(chuàng)建 store活玲,供PluginApi使用,用于保存插件添加的數(shù)據(jù)及鉤子
    this.store = {
      beforeMiddleWares: [],
      cwd: this.cwd,
      hooks: {}, // 用于存放鉤子
      commands: {},
    };
    this.plugins = [];
    // 執(zhí)行初始化
    this.init();
  }
  // 應(yīng)用初始化
  private init() {
    // ...
    // 其它初始化操作谍婉,例如:加載環(huán)境變量

    this.loadPlugins(); // 加載插件
  }

  // 執(zhí)行一個命令
  public run(command: string) {
    const fn = this.store.commands[command];
    if (!fn) {
      throw new Error(`Command ${command} does not exists.`)
    }
    fn();
  }

  /**
   * 加載插件
   * 說明:
   * 這里為了簡單起見舒憾,只寫了從配置文件讀取插件,
   * 實際上在umi和vue-cli中還會從package.json中根據(jù)一定規(guī)則加載插件
   */
  private loadPlugins() {
    // 讀取配置文件
    const { plugins = [] }: { plugins: string[] } = require(resolve(this.cwd, '.config.js'));
    // 將插件加載進來穗熬,并保存到一個私有變量中
    this.plugins = plugins.map(this.requirePlugin);
    // 運行插件镀迂,由于我們規(guī)定插件是一個方法,所以直接調(diào)用插件導(dǎo)出的方法唤蔗,傳入 PluginApi 實例即可
    // 實例化 PluginApi 時傳入了 store 對象探遵,是為了調(diào)用 PluginApi 的方法是可以將屬性和 hooks 添加到 store 上,方便我們調(diào)用妓柜。
    this.plugins.forEach(plugin => plugin.fn(new PluginApi(this.store)))
  }

  // 加載插件的簡單實現(xiàn)箱季,也就是直接使用 require 將插件引入進來
  private requirePlugin(plugin: string): { name: string, fn: Function } {
    try {
      return {
        name: plugin,
        fn: require(plugin),
      }
    } catch (error) {
      console.log(error)
      throw new Error('Plugin not exist.')
    }
  }
}

現(xiàn)在我們的主應(yīng)用已經(jīng)實現(xiàn)。接下來實現(xiàn)我們關(guān)鍵的對外API棍掐,即 PluginApi藏雏。

  • PluginApi 實現(xiàn)

為了簡單起見,我們只實現(xiàn)一個示例方法和 hook 機制

import { AsyncSeriesWaterfallHook } from 'tapable';
import { StoreState } from './Service';

export class PluginApi {
  private store: StoreState;

  constructor(store: StoreState) {
    this.store = store;
  }

  public addBeforeMiddleWare(middleWare: Function) {
    this.store.beforeMiddleWares.push(middleWare);
  }
  // 公共方法作煌,用于獲取應(yīng)用 cwd
  public getCwd() {
    return this.store.cwd;
  }

  // 注冊一個
  public registerCommand(name: string, handler: () => any) {
    this.store.commands[name] = handler;
  }

  // 注冊一個 Hook
  public registerHook(name: string) {
    if (this.store.hooks[name]) {
      throw new Error(`Hook ${name} already exists`);
    }
    // 這里為了拿到 hook 執(zhí)行完成后的返回值掘殴,我們使用了 AsyncSeriesWaterfallHook
    this.store.hooks[name] = new AsyncSeriesWaterfallHook(['memo']);
  }

  // 為 hook 添加一個監(jiān)聽器
  public onHook(hookName: string, handler: () => any) {
    const hook = this.store.hooks[hookName]
    if (hook) {
      hook.tapPromise(hookName, async (memo: any[] = []) => {
        const item = await handler();
        return memo.concat(item)
      });
    }
  }

  // 調(diào)用一個 Hook
  public async callHook(name: string) {
    const hook = this.store.hooks[name];
    if (hook) {
      const result = hook.promise();
      return result;
    }
  }

}

至此,我們的插件化機制已經(jīng)實現(xiàn)粟誓。那么接下來奏寨,我們來依據(jù)我們的插件系統(tǒng)寫個插件

插件 Demo

  • 實現(xiàn)插件,注冊 hook 及command
// demo/plugin.js

module.exports = async (api) => {
  api.addBeforeMiddleWare(() => {
    console.log(111)
  });
  api.registerHook('onDev');
  api.onHook('onDev', async () => {
    return 'hello hook 1'
  });

  api.onHook('onDev', () => {
    return 'hello hook 2'
  });
   api.registerCommand('test', async () => {
    const result = await api.callHook('onDev');
    console.log('Command: test result: ', result)
  })
}
  • 創(chuàng)建配置文件 .config.js
module.exports = {
  plugins: [
    require.resolve('./demo/plugin.js'),
  ]
}
  • 修改主應(yīng)用
// index.ts
import { Service } from './Service';

const service = new Service({});

service.run('test')

那么鹰服,現(xiàn)在使用 ts-node 運行index.ts病瞳,我們就能看到如下輸出:


image.png
image.png

與我們預(yù)計的效果是相同的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末获诈,一起剝皮案震驚了整個濱河市仍源,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌舔涎,老刑警劉巖笼踩,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異亡嫌,居然都是意外死亡嚎于,警方通過查閱死者的電腦和手機掘而,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來于购,“玉大人袍睡,你說我怎么就攤上這事±呱” “怎么了斑胜?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長嫌吠。 經(jīng)常有香客問我止潘,道長,這世上最難降的妖魔是什么辫诅? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任凭戴,我火速辦了婚禮,結(jié)果婚禮上炕矮,老公的妹妹穿的比我還像新娘么夫。我一直安慰自己,他們只是感情好肤视,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布档痪。 她就那樣靜靜地躺著,像睡著了一般邢滑。 火紅的嫁衣襯著肌膚如雪钞它。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天殊鞭,我揣著相機與錄音遭垛,去河邊找鬼。 笑死操灿,一個胖子當著我的面吹牛锯仪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播趾盐,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼庶喜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了救鲤?” 一聲冷哼從身側(cè)響起久窟,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎本缠,沒想到半個月后斥扛,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡丹锹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年稀颁,在試婚紗的時候發(fā)現(xiàn)自己被綠了芬失。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡匾灶,死狀恐怖棱烂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情阶女,我是刑警寧澤颊糜,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站秃踩,受9級特大地震影響芭析,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜吞瞪,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望驾孔。 院中可真熱鬧芍秆,春花似錦、人聲如沸翠勉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽对碌。三九已至荆虱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間朽们,已是汗流浹背怀读。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留骑脱,地道東北人菜枷。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像叁丧,于是被迫代替她去往敵國和親啤誊。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容