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

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

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

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

准备

三个概念

  • 核心系统(Core):有着系统的基本功能,这些功能不依赖于任何插件。
  • 核心和插件之间的联系(Core <--> plugin):通过核心系统提供的钩子将二者关联一起。
  • 插件(plugin):相互独立的模块,提供了单一的功能。

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

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

  • 首先要有一个核心系统。
  • 然后确定核心系统的生命周期和暴露的 API。
  • 最后设计插件的结构。
    • 插件的注册 -- 安装加载插件到核心系统中。
    • 插件的实现 -- 利用核心系统的生命周期钩子和暴露的 API。

最后代码执行的流程是:

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

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

代码实现

准备一个核心系统

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

js 复制代码
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 )

js 复制代码
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
js 复制代码
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);
  }
}

设计插件的结构

插件注册

js 复制代码
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来监听生命周期并添加处理器。

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

js 复制代码
// 日志插件:用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
    });
  }
}

全部代码

js 复制代码
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 组件插件化的简洁实现

相关推荐
卸任9 小时前
Vibe coding工具使用心得
前端·ai编程
brien910 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance10 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M10 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写10 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李10 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易10 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马11 小时前
前沿视界
前端
kyriewen11 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易11 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员