设计一个 JavaScript 插件系统

Avatar of Bryan Braun
Bryan Braun

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费积分!

WordPress 有 插件。 jQuery 有 插件GatsbyEleventyVue 也有。

插件是库和框架的常见功能,而且有充分的理由:它们允许开发人员以安全、可扩展的方式添加功能。 这使得核心项目更有价值,并构建了一个社区——所有这些都无需增加额外的维护负担。 真是太划算了!

那么,如何构建插件系统呢? 让我们通过构建自己的 JavaScript 插件系统来回答这个问题。

我使用的是“插件”这个词,但这些东西有时也被称为其他名称,例如“扩展”、“附加组件”或“模块”。 无论您称它们为什么,概念(和好处)都是相同的。

让我们构建一个插件系统

让我们从一个名为 BetaCalc 的示例项目开始。 BetaCalc 的目标是成为一个极简主义的 JavaScript 计算器,其他开发人员可以向其中添加“按钮”。 以下是一些基本的代码来帮助我们入门

// The Calculator
const betaCalc = {
  currentValue: 0,
  
  setValue(newValue) {
    this.currentValue = newValue;
    console.log(this.currentValue);
  },
  
  plus(addend) {
    this.setValue(this.currentValue + addend);
  },
  
  minus(subtrahend) {
    this.setValue(this.currentValue - subtrahend);
  }
};


// Using the calculator
betaCalc.setValue(3); // => 3
betaCalc.plus(3);     // => 6
betaCalc.minus(2);    // => 4

为了简单起见,我们将计算器定义为对象字面量。 计算器通过console.log打印其结果来工作。

目前的功能非常有限。 我们有一个setValue方法,它接受一个数字并在“屏幕”上显示它。 我们还有plusminus方法,它们将对当前显示的值执行操作。

是时候添加更多功能了。 让我们从创建一个插件系统开始。

世界上最小的插件系统

我们将从创建一个register方法开始,其他开发人员可以使用它将插件注册到 BetaCalc。 此方法的工作很简单:获取外部插件,获取其exec函数,并将其作为新方法附加到我们的计算器上

// The Calculator
const betaCalc = {
  // ...other calculator code up here


  register(plugin) {
    const { name, exec } = plugin;
    this[name] = exec;
  }
};

这是一个示例插件,它为我们的计算器提供了一个“平方”按钮

// Define the plugin
const squaredPlugin = {
  name: 'squared',
  exec: function() {
    this.setValue(this.currentValue * this.currentValue)
  }
};


// Register the plugin
betaCalc.register(squaredPlugin);

在许多插件系统中,插件通常有两个部分

  1. 要执行的代码
  2. 元数据(如名称、描述、版本号、依赖项等)

在我们的插件中,exec函数包含我们的代码,name是我们的元数据。 注册插件时,exec函数将直接作为方法附加到我们的betaCalc对象上,从而使其可以访问 BetaCalc 的this

因此,现在 BetaCalc 有了一个新的“平方”按钮,可以直接调用它

betaCalc.setValue(3); // => 3
betaCalc.plus(2);     // => 5
betaCalc.squared();   // => 25
betaCalc.squared();   // => 625

这个系统有很多优点。 插件是一个简单的对象字面量,可以传递给我们的函数。 这意味着插件可以通过 npm 下载并作为 ES6 模块导入。 易于分发非常重要!

但我们的系统也有一些缺点。

通过允许插件访问 BetaCalc 的this,它们可以读写访问 BetaCalc 的所有代码。 虽然这对于获取和设置currentValue很有用,但也存在危险。 如果插件重新定义内部函数(如setValue),则可能会导致 BetaCalc 和其他插件产生意外结果。 这违反了开闭原则,该原则指出软件实体应该对扩展开放,对修改关闭。

此外,“平方”函数通过产生副作用来工作。 这在 JavaScript 中并不少见,但感觉不太好——尤其是在其他插件可能在那里修改相同内部状态的情况下。 更函数式的方法将极大地有助于使我们的系统更安全、更可预测。

更好的插件架构

让我们再次尝试构建一个更好的插件架构。 下一个示例更改了计算器及其插件 API

// The Calculator
const betaCalc = {
  currentValue: 0,
  
  setValue(value) {
    this.currentValue = value;
    console.log(this.currentValue);
  },
 
  core: {
    'plus': (currentVal, addend) => currentVal + addend,
    'minus': (currentVal, subtrahend) => currentVal - subtrahend
  },


  plugins: {},    


  press(buttonName, newVal) {
    const func = this.core[buttonName] || this.plugins[buttonName];
    this.setValue(func(this.currentValue, newVal));
  },


  register(plugin) {
    const { name, exec } = plugin;
    this.plugins[name] = exec;
  }
};
  
// Our Plugin
const squaredPlugin = { 
  name: 'squared',
  exec: function(currentValue) {
    return currentValue * currentValue;
  }
};


betaCalc.register(squaredPlugin);


// Using the calculator
betaCalc.setValue(3);      // => 3
betaCalc.press('plus', 2); // => 5
betaCalc.press('squared'); // => 25
betaCalc.press('squared'); // => 625

我们在这里做了一些值得注意的更改。

首先,我们通过将它们放在自己的插件对象中,将插件与“核心”计算器方法(如plusminus)分离开来。 将我们的插件存储在plugin对象中使我们的系统更安全。 现在访问此对象的插件无法看到 BetaCalc 属性——它们只能看到betaCalc.plugins的属性。

其次,我们实现了一个press方法,该方法按名称查找按钮的功能,然后调用它。 现在,当我们调用插件的exec函数时,我们将当前计算器值(currentValue)传递给它,并期望它返回新的计算器值。

从本质上讲,这个新的press方法将我们所有的计算器按钮转换为纯函数。 它们接受一个值,执行一个操作,并返回结果。 这有很多好处

  • 它简化了 API。
  • 它使测试更容易(对于 BetaCalc 本身和插件本身)。
  • 它减少了我们系统的依赖性,使其更松耦合

这种新的架构比第一个示例更有限,但这是好事。 我们基本上为插件作者设置了护栏,限制他们只能进行我们希望他们进行的更改

事实上,它可能过于严格了! 现在我们的计算器插件只能对currentValue执行操作。 如果插件作者想要添加高级功能(如“内存”按钮或跟踪历史记录的方法),则他们将无法做到。

也许这没关系。 您赋予插件作者的权限是一个微妙的平衡。 赋予他们过多的权限可能会影响项目的稳定性。 但赋予他们太少的权限会使他们难以解决问题——在这种情况下,您不妨不使用插件。

我们还能做些什么?

我们可以做更多的事情来改进我们的系统。

我们可以添加错误处理,以在插件作者忘记定义名称或返回值时通知他们。 最好像 QA 开发人员一样思考,想象我们的系统如何崩溃,以便我们可以主动处理这些情况。

我们可以扩展插件可以执行的操作范围。 当前,BetaCalc 插件可以添加按钮。 但是如果它还可以为某些生命周期事件注册回调——例如,当计算器即将显示值时? 或者,如果有一个专门的地方可以存储跨多个交互的状态? 这是否会开辟一些新的用例?

我们还可以扩展插件注册。 如果插件可以注册一些初始设置呢? 这是否可以使插件更灵活? 如果插件作者想要注册一整套按钮而不是单个按钮——例如“BetaCalc 统计包”呢? 为此需要进行哪些更改?

您的插件系统

BetaCalc 及其插件系统都经过精心设计,非常简单。 如果您的项目更大,那么您可能需要探索其他一些插件架构。

一个好的起点是查看现有项目以了解成功插件系统的示例。 对于 JavaScript,这可能意味着jQueryGatsbyD3CKEditor或其他。

您可能还需要熟悉各种JavaScript 设计模式。(Addy Osmani 有一本书专门介绍这个主题。) 每个模式都提供了不同的接口和耦合程度,这为您提供了许多优秀的插件架构选项供选择。 了解这些选项有助于您更好地平衡所有使用您项目的人员的需求。

除了模式本身之外,您还可以借鉴许多优秀的软件开发原则来做出此类决策。 我已经在途中提到了几个(如开闭原则和松耦合),但一些其他相关的原则包括迪米特法则依赖注入

我知道听起来很多,但您必须进行研究。 没有什么比让每个人都重写他们的插件更痛苦的了,因为您需要更改插件架构。 这是快速失去信任并阻止人们将来参与贡献的一种方法。

结论

从零开始编写一个优秀的插件架构非常困难!你需要权衡很多因素来构建一个满足所有人需求的系统。它是否足够简单?是否足够强大?它能否长期有效?

但这绝对值得付出努力。拥有一个良好的插件系统对每个人都有帮助。开发者可以自由地解决他们的问题。最终用户可以从大量可选功能中进行选择。而你则可以围绕你的项目发展生态系统和社区。这是一个三赢的局面。