怎样设计一个 JavaScript 插件系统

更新日期: 2020-09-15阅读: 2k标签: 插件

不管是 jqueryvue 还是 react,它们都支持插件。

插件是库和框架中很常见的功能,并且有一个充分的理由:它们允许开发人员以安全、可扩展的方式添加新的功能。这能够使你的项目具有更高的价值,而且也不会增加太多额外的维护负担。

那么该如何构建插件系统呢?在本文中我们用 JavaScript 构建一个自己的插件系统。

在这里我用的是插件(plugin)这个词,有时也被称做是扩展(extensions),附加组件(add-ons)或 模块(modules)。无论叫什么名字,概念都是一样的。

构建一个插件系统

先从一个简单的 JavaScript 计算器项目 betaCalc 开始,其他人可以为它添加新的功能。下面是基本的代码

// 计算器项目
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);
  }
};

// 使用计算器
betaCalc.setValue(3); // => 3
betaCalc.plus(3);     // => 6
betaCalc.minus(2);    // => 4

我们先把计算器定义为一种客观事物,这样可以使事情变得简单。计算器的工作原理是通过 console.log 把结果输出到控制台。

目前功能很有限。在代码中有一个 setValue 方法,能够接受一个数字并将其显示在“屏幕”上。还有 plus 和 minus 方法,它们对当前显示的值执行对应的操作。

接下来就要添加更多的功能了。首先创建一个插件系统:


世界上最小的插件系统

先创建一个 register 方法,这样其他人就可以向 BetaCalc 注册插件了。它要做的工作很简单:得到一个外部插件,获取它的 exec 函数,并将其作为新方法附加到计算器上:

// 计算器项目
const betaCalc = {
  // 计算器相关的其他代码

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

下面的例子为计算器提供了一个计算平方的 squared 函数:

// 定义插件
const squaredPlugin = {
  name: 'squared',
  exec: function() {
    this.setValue(this.currentValue * this.currentValue)
  }
};

// 注册插件
betaCalc.register(squaredPlugin);

在大多数插件系统中,插件通常被分为两个部分:

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

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

现在 BetaCalc 多了一个计算平方的 squared 函数,可以直接调用:

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

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

不过这个插件系统有一些缺陷。

通过为插件提供对 BetaCalc 的 this 的访问权限,他们可以对所有 BetaCalc 的代码进行读写访问。虽然这样对于获取和设置 currentValue 很轻松,但是也很危险。如果一个插件要重新定义一个内部函数(如 setValue),它会给 BetaCalc 和其他插件带来意想不到的后果。这违背了开放封闭原则,开放封闭原则的核心思想是软件实体是可扩展而不可修改的。

另外 squared 函数是通过产生副作用) 发挥作用的。这在 JavaScript 中很常见,但是感觉并不好,特别是当其他插件可能处在同一内部状态的情况下。我们需要一种更加实用的方法使我们的系统更安全、更可预测。


更好的插件架构

让我们换一种更好的插件架构。下面的代码更改了计算器及其插件 api

// 计算器项目
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;
  }
};
  
// 我们的插件
const squaredPlugin = { 
  name: 'squared',
  exec: function(currentValue) {
    return currentValue * currentValue;
  }
};

betaCalc.register(squaredPlugin);

// 使用计算器
betaCalc.setValue(3);      // => 3
betaCalc.press('plus', 2); // => 5
betaCalc.press('squared'); // => 25
betaCalc.press('squared'); // => 625

我们在代码中做了如下修改:

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

其次,我们实现了一个 press 方法,该方法按名称查找功能对应的函数,然后调用。现在,当我们调用插件的exec 函数时,会把计算器当前的值(currentValue)传给它,并得到新的值。

从本质上来说,新增加的 press 方法把所有的计算器功能都转换为了纯函数(pure functions),它们返回结果只依赖其参数,并且在执行过程中没有副作用。这样做有很多好处:

  • 简化了API。
  • 简化了测试(对于 BetaCalc 和插件本身)。
  • 它减少了系统的依赖性,也就是实现了松耦合。

这种新的架构与第一个例子相比受到了更多的限制,但是效果很好。实际上,我们为插件的作者设置了防护边界,限制他们只能做我们允许的事。

不过它可能过于严格了,现在我们的计算器插件只能对 currentValue 进行操作。如果插件的作者想要添加一些高级的功能,例如“暂存结果”或跟踪历史记录的功能,就无能为力了。

从另外一个角度来看,也许没什么关系。因为你赋予插件作者的力量是一种微妙的平衡。给他们开放过多的功能由可能会影响项目的稳定性,但是反过来,给他们的功能过少会也使他们很难解决自己的问题,如果这样的话你还不如没有插件。


还需要做些什么

为了改善我们系统,还需要做很多工作。

如果插件作者忘了定义名称或返回值,可以通过添加错误处理机制来通知插件作者。需要像 QA 那样思考问题,并想象在什么情况下会使我们的系统崩溃,这样才能使我们为这些情况添加容错机制并避免崩溃。

我们还可以扩展插件的功能范围。现在一个 BetaCalc 插件可以添加一个功能。不过如果它还可以为某些生命周期事件注册回调,例如计算器将要显示结果值时,该怎么办?或者,如果有一个专用的位置来存储多个交互中的状态该怎么办?

我们还可以扩展插件注册。如果需要使用一些初始设置来注册插件怎么办?可以使插件更灵活吗?如果插件作者希望注册整个功能套件而不是一个功能该怎么办?为了支持这一点需要做哪些更改?


你自己的插件系统

BetaCalc 及其插件系统都非常简单。如果你的项目比较大,那就需要对其他的插件架构做一些探索。

一个很好的途径是参考现有成功项目的插件系统。对于 JavaScript 项目来说,你可以参考 jQuery、D3、CKEditor 等。

你还需要熟悉各种 JavaScript 设计模式。Addy Osmani 的《javascript设计模式》这本书就挺不错的。每种设计模式都提供了不同的接口和耦合度,你可以为自己的插件系统挑选合适的体系结构。了解这些可以帮你更好地平衡每个人的需求。

除了设计模式本身之外,还可以借鉴许多好的软件开发原则来做出这类决策。除了我在前面提到过的一些方法(例如:开闭原理和松耦合)之外,还包括一些其他的方法,例如 Demeter 法则和 dependency 注入。

看上去需要了解的知识有很多,但是你必须研究它们。让每个人都重写他们的插件是最痛苦的一件事,因为你需要改变插件的架构。这会让他人失去对你的信任,并且会阻止他们在未来为你的系统做出贡献。


总结

从零开始写一个好的插件架构是非常困难的,你必须考虑并权衡很多因素来构建满足所有人的需求。它足够简单吗?足够强大吗?可以长期工作吗?

这种努力的付出是值得的,拥有一个好的插件系统可以帮助所有人。开发人员可以自由的解决问题,最终用户可以拥有大量的选择,这样你就可以在围绕自己的项目发展生态系统和社区。这是一种双赢的局面。

来自:https://css-tricks.com/designing-a-javascript-plugin-system/


    链接: https://fly63.com/article/detial/9728

    对于前端开发,整理推荐好用的chrome插件或应用

    向web前端开发者整理提供的chrome插件或应用:比如Postman、JSON Viewer、Page Ruler 、ChromeADB 等等

    使用原生js开发插件的实现方法

    作为前端开发,我们都习惯使用一些开源的插件例如jquery工具库,那么如何使用原生js来开发封装一个自己的插件呢?接下来就看一下怎么去开发一个自己的js插件,先上代码

    typeahead.js_jquery input 搜索自动补全jQuery插件

    jquery.typeahead.js是一款高级的自动补全jQuery插件。该自动补全插件提供超过50个配置选项和回调方法,用于完成自动补全功能,能够完成绝大部分表单自动补全的需求。

    js轮播插件_轮播图js代码插件总汇

    这篇文章为大家分享图片轮播插件,最全最简单最通用的 幻灯片轮播插件,pc端和移动端都可完美使用,能满足绝大部分网站的轮播需求。js轮播插件包括Swiper、slick、owl carousel2、jssor/slider 、iSlider 等

    ios风格的时间选择插件

    在上个项目中,客户希望时间选择插件可以是ios风格的那种,但是找了很久,发现并没有用vue的ios风格时间插件,于是自己便自己造了一个轮子.插件依赖于better-scroll和vue

    前端最常用的vscode插件集

    在前端开发中,使用Visual Studio Code有哪些你常用的插件?推荐几个自己喜欢的,不带链接,自己搜索安装吧。这些都是比较实用、前端必备的插件集

    浏览器插件_常用谷歌浏览器插件推荐

    常用谷谷歌浏览器确实没有其它国产软件的内置功能丰富。但是 Google 浏览器的的优点恰恰就体现在拥有超简约的界面,以及支持众多强大好用的扩展程序,用户能够按照自己的喜好去个性化定制浏览器。今天我就给大家介绍几款自己常用的插件。

    sublime安装插件

    安装Sublime text 2插件很方便,可以直接下载安装包解压缩到Packages目录,也可以安装package control组件,然后直接在线安装

    BlockUI详细用法_Jquery中ajax加载提示插件blickUI

    BlockUI 插件是用于进行AJAX操作时模拟同步传输时锁定浏览器操作。当它被激活时,它会阻止使用者与页面(或页面的一部分)进行交互,直至它被取消。BlockUI以在DOM中添加元素的方法来实现阻止用户与浏览器交互的外观和行为

    vue项目中vscode格式化配置和eslint配置冲突

    使用vscode开发vue项目的时候,从远端拉下一个新的项目后,安装完依赖后跑起项目时,发现直接报了一堆语法错误:包括换行、空格、单双引号、分号等各种格式问题

    点击更多...

    内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!