本文翻译自: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定律(最少知识原则)和依赖注入。
我知道听起来很多,但你必须做你的研究。没有什么比让每个人重写他们的插件更痛苦的了,因为你需要改变插件的架构。这是一个快速失去信任和阻止人们在未来作出贡献的方法。
结论
从头开始写一个好的插件架构是很困难的!为了构建一个能满足每个人需求的系统,你必须平衡很多考虑因素。它够简单吗?足够强大吗?它能长期工作吗?
尽管如此,这还是值得的。拥有一个好的插件系统可以帮助每个人。开发人员可以自由地解决他们的问题。最终用户可以从大量的选择加入功能中进行选择。你可以在你的项目周围建立一个生态系统和社区。这是一个三赢的局面。