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

插件系统被广泛应用在各个系统中,比如浏览器、各个前端库如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 组件插件化的简洁实现

相关推荐
haima9515 分钟前
ubuntu安装chrome无法打开问题
前端·chrome
放逐者-保持本心,方可放逐23 分钟前
XSS 与 CSRF 记录
前端·xss·csrf·浏览器安全
徊忆羽菲23 分钟前
利用HTML5和CSS来实现一个漂亮的表格样式
前端·css·html5
不爱说话郭德纲29 分钟前
Stylus、Less 和 Sass 的使用与区别
前端·css·面试·less·sass·stylus
凄凄迷人39 分钟前
如何调试 chrome 崩溃日志(MAC)
前端·chrome·macos·crash
蒙特网站43 分钟前
网站布局编辑器前端开发:设计要点与关键考量
前端·javascript·学习·html
理想不理想v1 小时前
前端开发工程师需要学什么?
java·前端·vue.js·webpack·node.js
fhf1 小时前
感觉根本等不到35岁AI就把我裁了
前端·人工智能·程序员
hummhumm1 小时前
第 36 章 - Go语言 服务网格
java·运维·前端·后端·python·golang·java-ee
蒜蓉大猩猩1 小时前
Vue.js - 组件化编程
开发语言·前端·javascript·vue.js·前端框架·ecmascript