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

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

相关推荐
耶啵奶膘4 分钟前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^2 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie2 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic3 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿3 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具3 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161774 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test5 小时前
js下载excel示例demo
前端·javascript·excel
Yaml45 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事5 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro