插件機制詳解

簡介

插件模式是一種應(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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容