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

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

相关推荐
卢叁6 分钟前
Flutter之全局路由事件监听器RouteListenerManager
前端
盗德9 分钟前
为什么要用Monorepo管理前端项目?(详解)
前端·架构·代码规范
五号厂房11 分钟前
ProTable 大数据渲染优化:实现高性能表格编辑
前端
右子27 分钟前
理解响应式设计—理念、实践与常见误解
前端·后端·响应式设计
KaiSonng30 分钟前
【前端利器】这款轻量级图片标注库让你的Web应用瞬间提升交互体验
前端
二十雨辰33 分钟前
vite性能优化
前端·vue.js
明月与玄武35 分钟前
浅谈 富文本编辑器
前端·javascript·vue.js
paodan36 分钟前
如何使用ORM 工具,Prisma
前端
布列瑟农的星空1 小时前
重学React——memo能防止Context的额外渲染吗
前端
FuckPatience1 小时前
Vue 与.Net Core WebApi交互时路由初探
前端·javascript·vue.js