【译】设计一个JavaScript插件系统

简介: 【译】设计一个JavaScript插件系统

640.jpg本文翻译自: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定律(最少知识原则)和依赖注入。

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


结论


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

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

相关文章
|
28天前
|
JavaScript 前端开发
用html+javascript打造公文一键排版系统12:删除附件说明中“附件:”里的空格
用html+javascript打造公文一键排版系统12:删除附件说明中“附件:”里的空格
|
28天前
|
前端开发
用html+javascript打造公文一键排版系统3:获取参数设置、公文标题排版
用html+javascript打造公文一键排版系统3:获取参数设置、公文标题排版
|
28天前
用html+javascript打造公文一键排版系统1:设计界面
用html+javascript打造公文一键排版系统1:设计界面
|
2月前
|
JavaScript 前端开发 开发者
Vue.js 框架大揭秘:响应式系统、组件化与路由管理,震撼你的前端世界!
【8月更文挑战第27天】Vue.js是一款备受欢迎的前端JavaScript框架,以简洁、灵活和高效著称。本文将从三个方面深入探讨Vue.js:响应式系统、组件化及路由管理。响应式系统为Vue.js的核心特性,能自动追踪数据变动并更新视图。例如,通过简单示例代码展示其响应式特性:`{{ message }}`,当`message`值改变,页面随之自动更新。此外,Vue.js支持组件化设计,允许将复杂界面拆分为独立且可复用的组件,提高代码可维护性和扩展性。如创建一个包含标题与内容的简单组件,并在其他页面中重复利用。
68 3
|
13天前
|
缓存 JSON JavaScript
Node.js模块系统
10月更文挑战第4天
32 2
|
19天前
|
Web App开发 JavaScript API
构建高效后端系统:Node.js与Express框架的实践之路
【9月更文挑战第37天】在数字化时代的浪潮中,后端开发作为技术架构的核心,承载着数据处理和业务逻辑的重要职责。本文将深入探讨如何利用Node.js及其强大的Express框架来搭建一个高效、可扩展的后端系统。我们将从基础概念讲起,逐步引导读者理解并实践如何设计、开发和维护一个高性能的后端服务。通过实际代码示例和清晰的步骤说明,本文旨在为初学者和有经验的开发者提供一个全面的指南,帮助他们在后端开发的旅途上走得更远。
37 3
|
5天前
|
存储 JSON JavaScript
Vue.js开发中基于localStorage与sessionStorage的本地存储利器:Vue-ls插件使用详解
Vue.js开发中基于localStorage与sessionStorage的本地存储利器:Vue-ls插件使用详解
15 0
|
6天前
|
JavaScript 前端开发
前端js,vue系统使用iframe嵌入第三方系统的父子系统的通信
前端js,vue系统使用iframe嵌入第三方系统的父子系统的通信
|
2月前
|
机器学习/深度学习 人工智能 前端开发
【人工智能】利用TensorFlow.js在浏览器中实现一个基本的情感分析系统
使用TensorFlow.js在浏览器中进行情感分析是一个非常实用的应用场景。TensorFlow.js 是一个用于在JavaScript环境中训练和部署机器学习模型的库,使得开发者能够在客户端直接运行复杂的机器学习任务。对于情感分析,我们可以使用预先训练好的模型来识别文本中的积极、消极或中性情感。
83 4
【人工智能】利用TensorFlow.js在浏览器中实现一个基本的情感分析系统
|
28天前
用html+javascript打造公文一键排版系统14:为半角和全角字符相互转换功能增加英文字母、阿拉伯数字、标点符号、空格选项
用html+javascript打造公文一键排版系统14:为半角和全角字符相互转换功能增加英文字母、阿拉伯数字、标点符号、空格选项