注册

简单实现一个插件系统(不引入任何库),学会插件化思维

插件系统被广泛应用在各个系统中,比如浏览器、各个前端库如vue、webpack、babel,它们都可以让用户自行添加插件。插件系统的好处是允许开发人员在保证内部核心逻辑不变的情况下,以安全、可扩展的方式添加功能。


本文参考了webpack的插件,不引入任何库,写一个简单的插件系统,帮助大家理解插件化思维。


下面我们先看看插件有哪些概念和设计插件的流程。


准备


三个概念



  • 核心系统(Core):有着系统的基本功能,这些功能不依赖于任何插件。
  • 核心和插件之间的联系(Core <--> plugin):即插件和核心系统之间的交互协议,比如插件注册方式、插件对核心系统提供的api的使用方式。
  • 插件(plugin):相互独立的模块,提供了单一的功能。


插件系统的设计和执行流程


那么对着上面三个概念,设计插件的流程:



  • 首先要有一个核心系统。
  • 然后确定核心系统的生命周期和暴露的 API。
  • 最后设计插件的结构。

    • 插件的注册 -- 安装加载插件到核心系统中。
    • 插件的实现 -- 利用核心系统的生命周期钩子和暴露的 API。



最后代码执行的流程是:



  • 注册插件 -- 绑定插件内的处理函数到生命周期
  • 调用插件 -- 触发钩子,执行对应的处理函数

直接看代码或许更容易理解⬇️


代码实现


准备一个核心系统


一个简单的 JavaScript 计算器,可以做加、减操作。


class Calculator {
constructor(options = {}) {
const { initialValue = 0 } = options
this.currentValue = initialValue;
}
getCurrentValue() {
return this.currentValue;
}
setValue(value) {
this.currentValue = value;
}
plus(addend) {
this.setValue(this.currentValue + addend);
}
minus(subtrahend) {
this.setValue(this.currentValue - subtrahend);
}
}

// test
const calculator = new Calculator()
calculator.plus(10);
calculator.getCurrentValue() // 10
calculator.minus(5);
calculator.getCurrentValue() // 5

确定核心系统的生命周期


实现Hooks


核心系统想要对外提供生命周期钩子,就需要一个事件机制。不妨叫Hooks。(日常开发可以考虑使用webpack的核心库 Tapable


class Hooks {
constructor() {
this.listeners = {};
}
on(eventName, handler) {
let listeners = this.listeners[eventName];
if (!listeners) {
this.listeners[eventName] = listeners = [];
}
listeners.push(handler);
}
off(eventName, handler) {
const listeners = this.listeners[eventName];
if (listeners) {
this.listeners[eventName] = listeners.filter((l) => l !== handler);
}
}
trigger(eventName, ...args) {
const listeners = this.listeners[eventName];
const results = [];
if (listeners) {
for (const listener of listeners) {
const result = listener.call(null, ...args);
results.push(result);
}
}
return results;
}
destroy() {
this.listeners = {};
}
}

暴露生命周期(通过Hooks)


然后将hooks运用在核心系统中 -- JavaScript 计算器


每个钩子对应的事件:



  • pressedPlus 做加法操作
  • pressedMinus 做减法操作
  • valueWillChanged 即将赋值currentValue,如果执行此钩子后返回值为false,则中断赋值。
  • valueChanged 已经赋值currentValue

class Calculator {
constructor(options = {}) {
this.hooks = new Hooks();
const { initialValue = 0 } = options
this.currentValue = initialValue;
}
getCurrentValue() {
return this.currentValue;
}
setValue(value) {
const result = this.hooks.trigger('valueWillChanged', value);
if (result.length !== 0 && result.some( _ => ! _ )) {
} else {
this.currentValue = value;
}
this.hooks.trigger('valueChanged', this.currentValue);
}
plus(addend) {
this.hooks.trigger('pressedPlus', this.currentValue, addend);
this.setValue(this.currentValue + addend);
}
minus(subtrahend) {
this.hooks.trigger('pressedMinus', this.currentValue, subtrahend);
this.setValue(this.currentValue - subtrahend);
}
}

设计插件的结构


插件注册


class Calculator {
constructor(options = {}) {
this.hooks = new Hooks();
const { initialValue = 0, plugins = [] } = options
this.currentValue = initialValue;
// 在options中取出plugins
// 通过plugin执行apply来注册插件 -- apply执行后会绑定(插件内的)处理函数到生命周期
plugins.forEach(plugin => plugin.apply(this.hooks));
}
...
}

插件实现


插件一定要实现apply方法。在Calculator的constructor调用时,才能确保插件“apply执行后会绑定(插件内的)处理函数到生命周期”。


apply的入参是this.hooks,通过this.hooks来监听生命周期并添加处理器。


下面实现一个日志插件和限制最大值插件:


// 日志插件:用console.log模拟下日志
class LogPlugins {
apply(hooks) {
hooks.on('pressedPlus',
(currentVal, addend) => console.log(`${currentVal} + ${addend}`));
hooks.on('pressedMinus',
(currentVal, subtrahend) => console.log(`${currentVal} - ${subtrahend}`));
hooks.on('valueChanged',
(currentVal) => console.log(`结果: ${currentVal}`));
}
}

// 限制最大值的插件:当计算结果大于100时,禁止赋值
class LimitPlugins {
apply(hooks) {
hooks.on('valueWillChanged', (newVal) => {
if (100 < newVal) {
console.log('result is too large')
return false;
}
return true
});
}
}

全部代码


class Hooks {
constructor() {
this.listener = {};
}

on(eventName, handler) {
if (!this.listener[eventName]) {
this.listener[eventName] = [];
}
this.listener[eventName].push(handler);
}

trigger(eventName, ...args) {
const handlers = this.listener[eventName];
const results = [];
if (handlers) {
for (const handler of handlers) {
const result = handler(...args);
results.push(result);
}
}
return results;
}

off(eventName, handler) {
const handlers = this.listener[eventName];
if (handlers) {
this.listener[eventName] = handlers.filter((cb) => cb !== handler);
}
}

destroy() {
this.listener = {};
}
}

class Calculator {
constructor(options = {}) {
this.hooks = new Hooks();
const { initialValue = 0, plugins = [] } = options;
this.currentValue = initialValue;
plugins.forEach((plugin) => plugin.apply(this.hooks));
}
getCurrentValue() {
return this.currentValue;
}
setValue(value) {
const result = this.hooks.trigger("valueWillChanged", value);
if (result.length !== 0 && result.some((_) => !_)) {
} else {
this.currentValue = value;
}
this.hooks.trigger("valueChanged", this.currentValue);
}
plus(addend) {
this.hooks.trigger("pressedPlus", this.currentValue, addend);
this.setValue(this.currentValue + addend);
}
minus(subtrahend) {
this.hooks.trigger("pressedMinus", this.currentValue, subtrahend);
this.setValue(this.currentValue - subtrahend);
}
}

class LogPlugins {
apply(hooks) {
hooks.on("pressedPlus", (currentVal, addend) =>
console.log(`${currentVal} + ${addend}`)
);
hooks.on("pressedMinus", (currentVal, subtrahend) =>
console.log(`${currentVal} - ${subtrahend}`)
);
hooks.on("valueChanged", (currentVal) =>
console.log(`结果: ${currentVal}`)
);
}
}

class LimitPlugins {
apply(hooks) {
hooks.on("valueWillChanged", (newVal) => {
if (100 < newVal) {
console.log("result is too large");
return false;
}
return true;
});
}
}

// run test
const calculator = new Calculator({
plugins: [new LogPlugins(), new LimitPlugins()],
});
calculator.plus(10);
calculator.minus(5);
calculator.plus(1000);

脚本的执行结果如下,大家也可以自行验证一下



看完代码可以回顾一下“插件系统的设计和执行流程”哈。


更多实现


假如要给Calculator设计一个扩展运算方式的插件,支持求平方、乘法、除法等操作,这时候怎么写?


实际上目前核心系统Calculator是不支持的,因为它并没有支持的钩子。那这下只能改造Calculator。


可以自行尝试一下怎么改造。也可以直接看答案:github.com/coder-xuwen…


最后


插件化的好处


在上文代码实现的过程中,可以感受到插件让Calculator变得更具扩展性。



  • 核心系统(Core)只包含系统运行的最小功能,大大降低了核心代码的包体积。
  • 插件(plugin)则是互相独立的模块,提供单一的功能,提高了内聚性,降低了系统内部耦合度。
  • 每个插件可以单独开发,也支持了团队的并行开发。
  • 另外,每个插件的功能不一样,也给用户提供了选择功能的能力。

本文的局限性


另外,本文的代码实现很简单,仅供大家理解,大家还可以继续完善:



  • 增加ts类型,比如给把所有钩子的类型用emun记录起来
  • 支持动态加载插件
  • 提供异常拦截机制 -- 处理注册插件插件的情况
  • 暴露接口、处理钩子返回的结构时要注意代码安全

参考


Designing a JavaScript Plugin System | CSS-Tricks


当我们说插件系统的时候,我们在说什么 - 掘金


干货!撸一个webpack插件(内含tapable详解+webpack流程) - 掘金


精读《插件化思维》


【干货】React 组件插件化的简洁实现


作者:xuwentao
来源:juejin.cn/post/7344670957405126695

0 个评论

要回复文章请先登录注册