插件化系统的原理:面向接口编程 遵守开放封闭原则(OCP),系统对扩展开放,对修改封闭
一个插件系统的核心是制定一套消息通信机制,同时将系统运行时的上下文进行封装,按照不同的场景需求暴露给插件。通过消息通信机制将系统和插件隔离,保证插件不会侵入原系统,通过暴露封装后的上下文内容,安全可靠的将系统资源提供给插件调用
使用插件的优点:
- 允许第三方开发者扩展应用功能(webpack的大量插件)
- 拔插式的设计方便开发者添加、删减插件
-
实现一个插件系统
控制插件的加载
- 对插件暴露合适范围的上下文,并对不同场景的上下文做隔离
- 有一套可拔插的消息通信机制,订阅&监听
Hooks 声明
- Hooks 相当于插件的管理中心,每一个生命周期钩子基本对应框架的不同场景
通过不同的 Hook 来对应框架不同的事件、生命周期节点,以便在需要时调用相应的 Hook
// hooks.js,通过这个hook建立一个hash map,相当于一个插件注册中心。每个key代表一个类型的钩子
class Hooks {
constructor() {
this.hooks = new Map();
}
add(name, fn) {
const hooks = this.get(name);
hooks.add(fn);
this.hooks.set(name, hooks);
}
get(name) {
return this.hooks.get(name) || new Set();
}
invoke(name, ...args) {
for (const hook of this.get(name)) {
hook(...args);
}
}
async invokePromise(name, ...args) {
for (const hook of this.get(name)) {
await hook(...args);
}
}
}
module.exports = new Hooks();
Hook 的调用、注册
针对不同场景的具体插件,会注册特定的钩子(大于等于一个钩子)。插件组合使用不同特性的钩子,往其中插入具体业务代码。框架加载插件后,即会自动注册钩子。当运行到指定钩子的时候,相应的也会执行具体插件钩入的业务代码
// index.js
#!/usr/bin/env node
const fs = require('fs');
const hookBus = require('./hooks');
function onCreate() {
console.log('onCreate');
hookBus.invoke('onCreate',{a: 1,b: 2}); // 这里增加了主生命周期钩子的注册,可以将主流程中的上下文变量传过去
}
async function onStart() {
console.log('onStart');
await hookBus.invokePromise('onStart', {a: 3, b: 4}); // 这里是一个主生命周期异步钩子的注册
}
// 这个方法传给plugin,提供给插件来调用钩子
function hook(name, fn) {
hookBus.add(name, fn);
}
function loadPlugin() {
fs.readdirSync(__dirname)
.filter(item => /^plugin/.test(item))
.forEach(file =>
require(require.resolve(`${__dirname}/${file}`)).apply(hook) // 这里统一向钩子暴露了apply方法,作为插件主入口
);
}
function main() {
loadPlugin();
onCreate();
onStart();
}
main();
// plugin-1.js
console.log('plugin-1 loaded');
function apply(hook) {
hook('onCreate', function(ctx) {
console.log('plugin-1 onCreate');
console.log(ctx);
});
hook('onStart', function(ctx) {
console.log('plugin-1 onStart');
console.log(ctx);
});
}
module.exports = {
apply
};
// plugin-2.js
console.log('plugin-2 loaded');
function apply(hook) {
hook('onCreate', function(ctx) {
console.log('plugin-2 onCreate');
console.log(ctx);
});
}
module.exports = {
apply
};