簡介
插件模式是一種應(yīng)用非常廣泛的模式者蠕。我們用的很多軟件都擁有自身的插件機制檩奠,通過插件可以拓展軟件的功能翁锡。另外蔓挖,插件模式也廣泛應(yīng)用于 web 方面。例如 Webpack盗誊、 Vue CLI时甚、UMI、Babel等哈踱。
那么插件系統(tǒng)是如何實現(xiàn)的呢荒适?
如上圖所示,插件應(yīng)用的流程很簡單:
- 應(yīng)用啟動开镣,執(zhí)行初始化
- 查找和加載插件
- 調(diào)用插件
- 運行主應(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病瞳,我們就能看到如下輸出:
與我們預(yù)計的效果是相同的。