【译】Designone个JavaScript插件系统-【FAENWSI】



本文翻译自:https://css-tricks.com/designing-a-javascript-plugin-system/

原文标题:Designing a JavaScript Plugin System

WordPress 有插件。jQuery , Gatsby,   Vue都有插件系统。

插件是库和框架的一个常见特性,有一个很好的理由:它们允许开发人员以安全、可伸缩的方式添加功能。这使得核心项目更有价值,并且它建立了一个社区-所有这些都不会造成额外的维护负担。

那么,如何构建一个插件系统呢?让我们用JavaScript来构建一个吧。

构建一个插件系统

让我们从一个名为betaCalc的示例项目开始。BetaCalc的目标是成为一个极简的JavaScript计算器,其他开发人员可以添加“按钮”。下面是代码片段:

….….

// The Calculatorconst 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 calculatorbetaCalc.setValue(3); // => 3betaCalc.plus(3);     // => 6betaCalc.minus(2);    // => 4

我们将betaCalc定义成一个对象,这样比较简单。我们通过console.log来打印betaCalc的结果。目前功能确实有限。我们有一个setValue方法,它接受一个数字并在“屏幕”上显示它。我们还有加号和减号方法,它将对当currentValue执行操作。

接下来增加插件系统,来添加更多的内容。

最小的插件系统

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

// The Calculatorconst betaCalc = {  // ...other calculator code up here  register(plugin) {    const { name, exec } = plugin;    this[name] = exec;  }};

下面是一个”squared”插件的例子:

// Define the pluginconst squaredPlugin = {  name: 'squared',  exec: function() {    this.setValue(this.currentValue * this.currentValue)  }};// Register the pluginbetaCalc.register(squaredPlugin);

在许多插件系统中,插件通常包含两个部分:

  • 可执行代码

  • Metadata(比如名称,描述,版本号,依赖等)

在我们的插件中,exec函数包含我们的代码,名称就是我们的metadata。当插件注册后,exec函数作为一个方法直接附加到我们的betaCalc对象,让它访问betaCalc的this。

现在,我们有一个新的squared”按钮”,可以直接调用了:

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

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

但我们的插件系统有一些缺陷。

通过让插件访问BetaCalc的this,它们可以读/写BetaCalc的所有代码。虽然这对于获取和设置currentValue很有用,但也很危险。如果一个插件要重新定义一个内部函数(比如setValue),它可能会为BetaCalc和其他插件产生意外的结果。这违反了open-closed原则,该原则规定软件实体应该对扩展开放,但是对修改应该关闭。

另外,“square”函数的作用是产生副作用。这在JavaScript中并不少见,但感觉并不太好——尤其是当其他插件可能在那里扰乱相同的内部状态时。一个更加实用的方法将大大有助于使我们的系统更安全、更可预测。

更好的插件架构

让我们再来看看更好的插件架构。下一个示例将更改我们的计算器及其插件API。

// The Calculatorconst 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 Pluginconst squaredPlugin = {   name: 'squared',  exec: function(currentValue) {    return currentValue * currentValue;  }};betaCalc.register(squaredPlugin);// Using the calculatorbetaCalc.setValue(3);      // => 3betaCalc.press('plus', 2); // => 5betaCalc.press('squared'); // => 25betaCalc.press('squared'); // => 625

我们做了一些显著的改变。

首先,我们将插件与“核心”计算器方法(如加号和减号)分开,将它们放在自己的plugins对象中。将插件存储在plugin对象中可以使系统更安全。现在访问这个的插件看不到BetaCalc属性。它们只能看到betaCalc.plugins.

其次,我们实现了一个press方法,它按名称查找按钮的函数,然后调用它。当我们调用一个插件时,我们会传入currentValue,并且返回最新的计算值。

从本质上讲,把我们所有的函数都转换成了新的计算方法。它们获取一个值,执行一个操作,然后返回结果。这有很多好处:

  • 简化了API。

  • 使测试变得更容易(对于BetaCalc和插件本身)。

  • 减少了系统的依赖性,使其更松散耦合。

这个新的架构比第一个例子的限制更过,但是在一个好的方面。我们基本上为插件作者设置了防护栏,限制他们只做我们希望他们做的更改。

事实上,这可能限制太多了!现在我们的计算器插件只能对currentValue进行操作。如果一个插件作者想要添加高级功能,比如“内存”按钮或者跟踪历史的方法,他们就不能这样做了。

也许没关系。作者给你的插件是一种微妙的平衡。给他们太多的权力可能会影响项目的稳定性。但是,给他们太少的权力使他们很难解决他们的问题。在这种情况下,你最好不要插件。

还能做什么?

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

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

我们可以扩展插件的功能范围。目前,BetaCalc插件可以添加一个按钮。但是如果它还可以注册某些生命周期事件的回调,比如计算器将要显示一个值时,该怎么办?或者,如果有一个专门的地方来存储跨多个交互的状态片段呢?这会带来一些新的用例吗?

我们还可以扩展插件注册。如果一个插件可以注册一些初始设置呢?这能让插件更加灵活吗?如果一个插件作者想要注册一整套按钮而不是一个按钮,比如“BetaCalc统计包”呢?需要做些什么样的改变来支持这一点?

你的插件系统

BetaCalc和它的插件系统都是十分简单的。如果你的项目比较大,那么你会想探索其他一些插件架构。

一个好的开始是看看现有的项目中成功的插件系统的例子。比如jQuery、Gatsby、D3、CKEditor等等。

你可能还需要熟悉各种JavaScript设计模式。每个模式都提供了不同的接口和耦合度,这给了你很多很好的插件架构选择。了解这些选项可以帮助你更好地平衡每个使用你项目的人的需求。

除了模式本身,还有很多好的软件开发原则可以用来做这些决策。我已经提到了一些(比如开闭原理和松耦合),但是其他一些相关的包括Demeter定律(最少知识原则)和依赖注入。

我知道听起来很多,但你必须做你的研究。没有什么比让每个人重写他们的插件更痛苦的了,因为你需要改变插件的架构。这是一个快速失去信任和阻止人们在未来作出贡献的方法。

结论

从头开始写一个好的插件架构是很困难的!为了构建一个能满足每个人需求的系统,你必须平衡很多考虑因素。它够简单吗?足够强大吗?它能长期工作吗?

尽管如此,这还是值得的。拥有一个好的插件系统可以帮助每个人。开发人员可以自由地解决他们的问题。最终用户可以从大量的选择加入功能中进行选择。你可以在你的项目周围建立一个生态系统和社区。这是一个三赢的局面。

特别声明:以上文章内容仅代表作者本人观点,不代表FAENWSI观点或立场。如有关于作品内容、版权或其它问题请于作品发表后的30日内与FAENWSI联系。
Hank
版权声明:本站原创文章,由 Hank2022-06-10发表,共计4652字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)