Skip to content

Latest commit

 

History

History
601 lines (427 loc) · 36.2 KB

File metadata and controls

601 lines (427 loc) · 36.2 KB

十、插件和小部件开发模式

本章重点介绍在实现 jQuery 插件时使用的设计模式和最佳实践。我们将在这里学习如何将应用程序的各个部分抽象为单独的 jQuery 插件,从而促进关注点分离原则和代码重用性。

我们将首先分析 jQuery 插件可以实现的最简单的方法,学习 jQuery 插件开发的各种约定以及每个插件为了遵循 jQuery 原则应该满足的基本特性。然后,我们将继续介绍最广泛使用的设计模式,并分析每种模式的特点和优点。到本章结束时,我们将能够使用最适合每个用例的开发模式实现可扩展 jQuery 插件。

在本章中,我们将:

  • 介绍 jQuery 插件 API 及其约定
  • 分析优秀插件的特点
  • 了解如何通过扩展$.fn对象来创建插件
  • 了解如何实现可扩展的通用插件,以使它们在更多用例中可重用
  • 了解如何为插件提供选项和方法
  • 介绍 jQuery 插件开发中最常见的设计模式,并分析它们各自帮助解决的常见实现问题

引入 jQuery 插件

jQuery 插件的关键概念在于扩展 jQuery API,使其功能作为 jQuery复合集合对象上的方法进行访问。jQuery 插件只是在$.fn对象上定义为新方法的函数,该对象是每个 jQuery 集合对象继承的原型对象

$.fn.simplePlugin101 = function(arg1, arg2/*, ...*/) { 
    // Plugin's implementation... 
};

通过在$.fn对象上定义一个方法,我们实际上是在扩展核心 jQuery API 本身,因为这使该方法从此点开始在所有创建的 jQuery 集合对象上可用。因此,插件加载到网页后,其功能作为$()函数返回的每个对象的方法可用:

$('h1').simplePlugin101('test', 1);

jQuery 插件 API 的主要约定是,调用插件的 jQuery 集合对象作为其执行上下文提供给插件的方法。换句话说,我们可以在 plugin 方法中使用this标识符,如下所示:

$.fn.simplePlugin101 = function() { 
    this.slideToggle(); 
    // "this" is a jQuery object where all 
    // jQuery methods are available
};

遵循 jQuery 原则

创建插件的目标之一是让它感觉像 jQuery 本身的一部分。在阅读了前面的章节之后,您应该熟悉所有 jQuery 方法所遵循的一些原则以及使其方法具有特殊性的特性。实现一个遵循这些原则的插件会让用户对它的 API 感到更舒服,更有效率,并且实现错误更少,这会导致插件的普及和采用率的提高。

伟大的 jQuery 插件应该具备的两个最重要的特征如下:

  • 只要适用,它应该应用于调用它的 jQuery 集合对象的所有元素
  • 它应该允许进一步链接其他 jQuery 方法

现在让我们继续分析这些原则。

处理复合收集对象

jQuery 方法最重要的特性之一是,它们应用于调用它们的复合集合对象的每个项。例如,$.fn.addClass()方法在单独检查每个类是否已在每个元素上定义后,向集合的每个项添加一个或多个 CSS 类。

因此,我们的 jQuery 插件也应该遵循这一原则,在集合中的每个元素上进行操作,而这似乎是合乎逻辑的。如果您在插件的实现中只使用 jQuery 方法,大多数情况下,您可以免费获得这些方法。另一方面,需要注意的一个重要问题是,并非所有 jQuery 方法都对集合对象的每个元素进行操作。当用作 setter 方法时,$.fn.html()$.fn.css()$.fn.data()等方法适用于集合的所有项,但当用作 getter 方法时,仅对第一个元素进行操作。

我们来看一个插件的示例实现,该插件使用$.fn.animate()在 jQuery 对象的所有项上创建震动效果:

$.fn.vibrate = function() { 
  this.each(function(i, element) { 
    // specifically handle every element
    var $element = $(element); 
    if ($element.css('position') === 'static') { 
      $element.css({ position: 'relative' }); 
    } 
  }); 

  this.animate({ left: '+=3' }, 30) 
    .animate({ left: '-=6' }, 60) 
    .animate({ left: '+=6' }, 60) 
    .animate({ left: '-=3' }, 30); 

  return this; // allow further chaining
};

通过$('button').vibrate();调用此插件,在页面的每个匹配元素上应用抖动动画。为了实现这一点,插件使用$.fn.animate()方法更改所有匹配元素的leftCSS 属性,该方法可以方便地对每个元素进行操作。另一方面,由于当用作 getter 时,$.fn.css()方法仅适用于集合的第一个元素,因此我们必须使用$.fn.each()方法迭代所有元素,并确保每个元素都不是静态定位的,在这种情况下,leftCSS 属性不会影响其外观。

显然,仅使用 jQuery 方法并不总是足以实现插件。在大多数情况下,新插件的实现必须至少使用一个非 jqueryapi,这需要我们迭代集合中的项目,并将插件的逻辑分别应用于每个项目。当集合的每个元素必须根据其状态稍微不同地进行处理时,应使用相同的方法。

因此,插件在$.fn.each()调用中封装几乎所有的实现是非常常见的。通过认识到显式迭代所涵盖的共同需求,jQuery 团队和大多数 jQuery 插件样板现在将其作为标准实践的一部分。

允许进一步链接

一般来说,当你的插件的代码不需要返回任何东西时,你所要做的就是在最后一行添加return this;语句,就像我们在前面的例子中看到的那样。确保所有代码路径返回调用上下文(this或其他相关 jQuery 集合对象的引用,方法与$.fn.parent()$.fn.find()相同。或者,当您的所有代码都封装在另一个 jQuery 方法(如$.fn.each())中时,通常只返回该调用的结果,如下所示:

$.fn.myLogPlugin = function() { 
    return this.each(function(i, element) { 
        console.log($(element).text()); 
    }); 
};

请记住,如果您的代码操作调用它的集合对象,而不是返回this引用,那么您可能需要返回插件操作后产生的新集合。

为了允许进一步链接,您应该避免将插件的实现基于返回值。与其这样做,不如在第一次调用时初始化插件,然后提供一些重载方式来调用它,作为返回值的一种方式。

与$.noConflict()合作

改进插件实现的第一步是使其在无法访问$标识符的环境中工作。这方面的一个例子是当网页使用jQuery.noConflict()方法时,该方法阻止 jQuery 将自身分配给$全局标识符(或window.$,并使其仅在jQuery名称空间(window.jQuery上)可用。

jQuery.noConflict()方法允许我们防止 jQuery 与其他库和实现冲突,这些库和实现也恰好使用了$变量。有关的更多信息,您可以访问 jQuery 文档页面:http://api.jquery.com/jQuery.noConflict/

在这种情况下,插件定义会抛出一个**$is not defined**错误,甚至更糟;它可能会尝试使用开发人员保留在实现中使用的$变量,从而导致难以调试的错误。

幸运的是,修复此问题所需的更改很容易实现,并且不会影响插件的功能。我们需要做的就是将插件中出现的所有$标识符重命名为jQuery,如下所示:

jQuery.fn.simplePlugin101 = function(arg1, arg2/*, ...*/) { 
    var $buttons = jQuery('button');
    // ...
};

用生命包裹

接下来要遵循的最佳实践是用 IIFE 包装插件的定义和实现。这不仅使我们的插件看起来像模块模式,而且通过增加其他几个好处,使我们的实现更加健壮。

首先,IIFE 模式允许我们在插件定义的上下文中创建和使用私有变量和函数。这些变量在插件的所有实例中共享,其方式与其他编程语言中静态变量的工作方式类似,使我们能够将它们用作插件实例之间的同步点:

(function($) { 
    var callCounter = 0; 

    function utilityLogMethod(message) { 
        if (window.console && console.log) { 
            console.log(message); 
        } 
    } 

    $.fn.simplePlugin101 = function(arg1, arg2/*, ...*/) { 
        callCounter++; 
        utilityLogMethod(callCounter); 
        return this;
    }; 
})(jQuery);

否则,我们将不得不使用类似于$.simplePlugin101._callCounter$.simplePlugin101._utilityLogMethod()的东西来模拟隐私,这只是一种命名约定,并不提供任何实际的隐私。

第二个好处,如上例中所示,允许我们再次使用$标识符访问 jQuery,而不必担心冲突。为了实现这一点,我们将 jQuery 命名空间变量作为调用参数传递给 IIFE,并使用$标识符命名相应的参数。通过这种方式,我们在 IIFE 创建的上下文中将 jQuery 名称空间有效地别名为$,使我们能够在实现中使用最小的$标识符,以保持代码的精简和可读性,即使使用了jQuery.noConflict()

此外,在 iLife 顶部的上添加use strict;语句有助于消除变量泄漏到全局名称空间中的情况。例如,下面的代码将抛出一个引用错误:在调用插件的方法期间,分配给未声明的变量 x错误,使我们能够在插件的开发阶段捕获这些错误,从而帮助生成更健壮的最终实现:

(function($) { 
    'use strict'; 

    $.fn.leakingPlugin = function() { 
        x = 0;// there is no "var x" declaration, 
        // so an error is thrown when executed
    }; 
})(jQuery); 

$('div').leakingPlugin();

有关 JavaScript 严格执行模式的更多信息,请访问:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode

最后,与直接引用 jQuery namespace 变量的实现相比,与使用 IIFEs 的所有名称空间别名实践一样,这种模式在缩小插件源代码时也有助于增加收益。为了最大限度地发挥这项技术的优势,我们的插件访问的所有全局名称空间变量都需要别名,如下所示:

(function ( $, window, document, undefined ) { 
    // Plugin's implementation... 
})( jQuery, window, document );

创建可重用插件

在分析了 jQuery 插件开发的最重要的方面之后,我们现在准备分析一个实现,它不仅仅用于简单的演示。为了创建一个真正有用且可重用的插件,它的设计必须使其操作不受其原始用例需求的限制。

与最有用的 jQuery 方法一样,最流行的插件是那些提供高度功能配置的插件。创建一个可配置的插件为其实现增加了一定程度的灵活性,这使我们能够匹配由相同操作原则管理的其他几个用例的需求。

正如我们前面所说,jQuery 插件只是一个附加到$.fn对象的函数,因此,我们可以像使用我们模块的普通函数一样,使其实现更加抽象和通用。与简单函数一样,区分 jQuery 插件操作的最简单方法是使用调用参数。公开大量配置参数的插件具有很大的潜力,能够满足多个不同用例的需求。

接受配置参数

与我们实现的函数通常最多可以接受五个参数,并且仍然有一个可管理且相对干净的 API 不同,这种做法在 jQuery 插件中不太管用。为了公开一个清晰的 API 并保持高水平的可用性,不管公开的各种配置选项如何,大多数 jQuery 插件都提供了一个最小的 API,最多可以接受三个调用参数。这是通过使用具有特定格式的专用设置对象来实现的,作为封装多个选项并将其作为单个参数传递的一种方式。另一种方法是公开带有两个参数的 API,其中第一个参数是定义插件操作的常规值,第二个参数用于包装不太重要的配置选项。

这两种实践的一个很好的例子是$.ajax(settings)方法,该方法使用单个设置对象作为参数进行调用,以定义其应如何操作,但也公开了另一种使用两个参数进行调用的重载方法。通过$.ajax(url, settings)调用双参数重载,其中第一个是 HTTP 请求的目标 URL,第二个是具有 rest 配置选项的对象。适用于这两种方法的是,该方法本身包含一组合理的默认值,而不是用户尚未定义的任何配置参数。此外,第二个重载还将第二个参数定义为可选参数,如果在其调用过程中未提供该参数,则其操作将基于默认设置。

在我们的插件中采用设置对象实践不仅带来了上述所有好处,而且允许我们以更可伸缩的方式扩展实现,因为添加额外的配置参数对其 API 的其余部分几乎没有影响。作为一个例子,我们将以一种更通用的方式重新实现我们在本章前面看到的$.fn.vibrate插件,以便在其配置中使用具有默认值的设置对象:

(function($) { 

  $.fn.vibrate = function(options) { 
    var opts = $.extend({}, $.fn.vibrate.defaultOptions, options);

    this.each(function(i, element) { 
      var $element = $(element); 
      if ($element.css('position') === 'static') { 
        $element.css({ position: 'relative' }); 
      } 
    }); 

    for (var i = 0, len = opts.loops * 4; i < len; i++) { 
      var animationProperties = {}; 
      var movement = (i % 2) ? '+=': '-='; 

      movement += (i === 0 || i === len - 1) ? 
        opts.amplitude / 2: 
        opts.amplitude; 

      var t = (i === 0 || i === len - 1) ? 
        opts.period / 4: 
        opts.period / 2; 

      animationProperties[opts.direction] = movement; 
      this.animate(animationProperties, t); 
    }

    return this; 
  }; 

  $.fn.vibrate.defaultOptions = { 
    loops: 2, 
    amplitude: 8, 
    period: 100, 
    direction: 'left' 
  }; 
})(jQuery);

与最初的固定实现不同,该实现接受单个对象作为调用参数,其中包含四个不同的选项,可用于使插件的操作多样化。options 对象允许我们通过公开四个定制点来实现插件操作的多样化:

  • 抖动效果应运行的循环数
  • 动画的振幅,用于控制元素应远离其原始位置的程度
  • 每个循环的周期,作为控制运动速度的一种手段
  • 动画的方向,使用left时为水平方向,使用top时为垂直方向

通过遵循广泛接受的最佳实践,我们将配置选项的所有默认值定义为一个单独的对象。这种模式不仅允许我们收集单个对象下的所有相关值,还允许我们使用$.extend()方法作为一种有效的方式,将所有已定义的选项与未定义选项的默认值组合在一起。因此,我们可以避免显式检查每个单独属性的存在,从而降低代码的复杂性和大小。

简言之,$.extend()方法在将后续对象的属性合并到第一个对象后返回作为其第一个参数传递的对象。因此,返回的对象将包含所有默认值,但在作为调用参数传递的 options 对象中定义的值除外。

有关$.extend()助手方法的更多信息,请访问文档页面:http://api.jquery.com/jQuery.extend/

此外,我们没有使用简单的变量,而是将默认选项对象作为插件函数的属性公开,使用户能够更改它们以更好地满足他们的需要。作为一个例子,考虑一个需要特定应用程序的平滑动画的情况。通过设置$.fn.vibrate.defaultOptions.period = 250,,开发人员将完全不需要在每次调用插件时指定period选项,这将导致实现代码重复性更少。

jQuery 库本身采用这种做法来定义$.ajax()方法的默认配置参数。由于此方法的复杂性增加,jQuery 为我们提供了jQuery.ajaxSetup()方法作为为每个 AJAX 请求设置默认参数的方法。

最后,为了创建原始实现的通用变体并利用上述配置选项,我们将原始实现的$.fn.animate()方法的四个固定调用替换为使用loops选项的for循环。在for循环本身内部,我们为$.fn.animate()方法的每次调用构造参数,并在循环的每次后续执行中短暂地交替动画运动的方向,同时还确保第一个和最后一个运动具有所有其他步骤的一半时长和一半移位。

最终的实现可以根据每个特定用例的需要进行配置,以生成不同的动画,从最适合通知用户无效动作的短水平动画,到看起来像悬浮效果的垂直长动画。可以使用上述选项的任意组合调用插件,使用缺少选项的默认值,甚至不使用调用参数进行操作,如下所示:

// do the default intense animation on a button
// that appears disabled, to designate an invalid action 
$('button.disabled').on('click', function() { 
  $(this).vibrate(); 
}); 

// do a smother shake animation to catch the user's 
// attention on an important part of the page 
$('.save-button').vibrate({loops: 3, period: 250}); 

// start a long running levitation effect on the header of the page 
$('h1').vibrate({direction: 'top', loops: 1000, period: 5000});

编写有状态 jQuery 插件

到目前为止,我们所看到的插件实现是无状态的,因为在完成它们的执行后,它们会恢复对 DOM 状态的操作,并且不会将分配的对象留在浏览器内存中。因此,无状态插件的后续调用总是产生相同的结果。

正如你可能猜到的那样,这些插件的应用程序有限,因为它们不能用来创建一系列与网页用户的复杂交互。为了协调复杂的用户交互,插件需要保留一个内部状态,并在该状态下执行操作,以便适当地更改其操作模式并处理后续交互。比较有状态和无状态插件的特性可以定义为等同于将普通(静态)函数与作为对象一部分并可以对其状态进行操作的方法进行比较。

另一个流行的插件类别是操纵 DOM 树的插件家族,其中具有内部状态是必不可少的。这些插件通常创建复杂的元素结构,如富文本编辑器、日期选择器和日历,通常是通过构建用户定义的空<div>元素。

实现有状态 jQuery 插件

作为用于实现该家族插件的模式的一个例子,我们将编写一个通用的元素突变****观察者插件。这个插件将为我们提供一种方便的方法,为 DOM 树的更改添加事件监听器,这些更改源于调用这个插件的任何元素。为了实现这一点,以下实现使用了MutationObserverAPI,在撰写本文时,该 API 由所有现代浏览器实现,超过 86%的 web 用户都可以使用。

有关突变观察者的更多信息,请访问:https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver

现在让我们继续实施并分析所使用的实践:

(function($) { 
  $.fn.mutationObserver = function(action) { 
    return this.each(function(i, element) { 
      var $element = $(element); 
      var instance = $element.data('plugin_mutationObserver'); 

      if (!instance) { 
        var observer = new MutationObserver(function(mutations) { 
          mutations.forEach(function(mutation) { 
            instance.callbacks.forEach(function(callbackFn) { 
              callbackFn(mutation); 
            }); 
          }); 
        }); 

        observer.observe(element, {
          attributes: true,
          childList: true,
          characterData: true
        }); 

        instance = { 
          observer: observer, 
          callbacks: [] 
        }; 
        $element.data('plugin_mutationObserver', instance); 
      } 

      if (typeof action === 'function') { 
        instance.callbacks.push(action); 
      } 
    }); 

  }; 
})(jQuery);

首先,我们在 IIFE 中定义我们的插件,正如本章前面所建议的。在$.fn对象上声明插件之后,我们使用$.fn.each()方法作为直接方法,以确保插件的功能应用于调用它的 jQuery 集合对象的每个项。

有状态插件实现的两个主要问题是缺少一种机制来保存插件每个实例化的内部状态,以及避免在同一页面元素上多次初始化的方法。为了解决这两个问题,我们需要使用类似哈希表的东西,其中键是元素本身,值是具有插件实例状态的对象。

幸运的是,方法通过使用特定的字符串键关联 DOM 元素和 JavaScript 对象值,或多或少就是这样工作的。通过使用$.fn.data()方法和插件名称作为关联键,我们能够非常轻松地存储和检索插件的状态对象。

提示

在这个用例中使用$.fn.data()方法被认为是一种最佳实践,大多数有状态的插件实现和样板都使用它,因为它是 jQuery 的一个强大部分,使我们能够减少插件实现的大小。

如果未找到现有状态对象,那么我们可以假设插件尚未在该特定元素上初始化,并立即开始初始化。该插件的 state 对象将包含活动 MutationObserver 的实例,该实例负责跟踪观察到的 DOM 元素上发生的更改,以及一个包含所有已订阅该元素以获取更改通知的回调的数组。

创建一个新的 MutationObserver 实例后,我们将其配置为查找三种特定类型的 DOM 更改,并指示它在发生此类 DOM 更改时调用插件状态对象的所有回调。最后,我们创建 state 对象本身来保存观察器和相关的回调,并使用$.fn.data()方法作为 setter 并将其与 page 元素关联。

在确保插件在提供的元素上实例化和初始化之后,我们检查插件是否以函数作为参数调用,如果是,我们将其添加到插件回调列表中。

提示

请记住,每个元素使用一个 MutationObserver 实例,并让它通过迭代回调数组来通知 DOM 更改,这大大降低了实现的内存需求,就像使用单个委托观察者一样。

使用我们新实现的插件观察特定 DOM 元素的变化的示例如下所示:

$('.container').mutationObserver(function(mutation) { 
  console.log('Something changed on the DOM tree!'); 
});

销毁插件实例

有状态插件必须考虑的一个额外的因素是为开发人员提供了一种逆转其对页面状态所做更改的方法。实现这一点的最常见和最简单的 API 是使用destroy文本作为其第一个参数来调用插件。让我们继续进行所需的实施更改:

(function($) { 
  $.fn.mutationObserver = function(action) { 
    return this.each(function(i, element) { 
      var $element = $(element); 
      var instance = $element.data('plugin_mutationObserver'); 

      if (action === 'destroy' && instance) { 
        instance.observer.disconnect(); 
        instance.observer = null;
        $element.removeData('plugin_mutationObserver'); 
        return; 
      } 

      if (!instance) { 
        /* ... */ 
      } 
    }); 

  }; 
})(jQuery);

为了使我们的实现适应上述要求,我们所要做的就是在检索插件的 state 对象之后,检查插件是否以destroy字符串值作为其第一个参数被调用。如果我们发现插件已经在指定的元素上实例化,并且已经使用了destroy字符串值,我们可以继续停止突变观察者本身,并清除$.fn.data()使用$.fn.removeData()方法创建的关联。最后,在if语句的末尾,我们添加了一个return语句,因为在完成插件实例的销毁之后,我们不再需要执行任何其他代码。使用此实现销毁插件实例的示例如下所示:

$('.container').mutationObserver('destroy');

实现 getter 和 setter 方法

通过使用与我们之前为实现我们插件的方法而展示的相同技术,我们可以提供几种其他重载方式来调用我们的插件,这些重载方式的工作方式与普通方法类似。这种模式不仅被普通的 jQuery 插件使用,也被更复杂的插件体系结构所采用,就像 jQuery UI 一样。

另一方面,我们可能最终会得到一个插件实现,导致大量调用重载,这会使使用和记录变得困难。解决这个问题的一种方法是将 API 的 getter 和 setter 方法组合成多用途方法。这不仅减少了插件的 API 表面,因此开发人员必须记住更少的方法名称,而且还提高了生产率,因为许多 jQuery 方法(如$.fn.html()$.fn.css()$.fn.prop()$.fn.val()$.fn.data()都使用了相同的模式。

作为一个演示,让我们看看如何向 MutationObserver 插件添加一个新方法,该插件同时作为注册回调的 getter 和 setter:

(function($) { 
  $.fn.mutationObserver = function(action, callbackFn) { 
    var result = this; 

    this.each(function(i, element) { 
      var $element = $(element); 
      var instance = $element.data('plugin_mutationObserver'); 
      /* ... */ 

      if (typeof action === 'function') { 
        instance.callbacks.push(action); 
      } else if (action === 'callbacks') { 
        if (callbackFn && callbackFn.length >= 0) { 
          // used as a setter 
          instance.callbacks = callbackFn; 
        } else { 
          // used as a getter for the first element 
          result = instance.callbacks; 
          return false;// break the $.fn.each() iteration 
        } 
      }
    }); 

    return result; 
  }; 
})(jQuery); 

如上面的代码所示,我们创建了一个重载调用方法,它使用callbacks字符串值作为插件调用的第一个参数。这个 getter 和 setter 方法允许我们通过使用函数参数和destroy方法来检索或覆盖在 MutationObserver 上注册的所有回调,这些回调是在调用插件的现有方法之外工作的。

getter 和 setter 实现基于这样的假设:当尝试使用callbacks方法作为 getter 时,您不需要传递任何额外的参数,当尝试将其用作 setter 时,您将传递一个额外的数组作为调用参数。为了支持 getter 变量,它防止进一步链接,并且只对复合集合的第一个元素进行操作,我们必须声明并使用result变量,该变量初始化为this标识符的值。如果使用了callbacksgetter,我们将集合的第一个元素的callbacks分配给result变量,并通过返回false中断$.fn.each()迭代,以完成插件方法的执行。

下面是我们新实现的 getter 和 setter 方法的一个示例用例:

// retrieve the callbacks 
var oldCallbacks = $('.container').mutationObserver('callbacks'); 
// clear them 
$('.container').mutationObserver('callbacks', []); 
// add a new one 
$('.container').mutationObserver(function() { 
  console.log('Printed only once'); 
  // restore the old callbacks
  $('.container').mutationObserver('callbacks', oldCallbacks); 
});

提示

请记住,通过返回非 jQuery 对象结果来阻止进一步链接的调用重载应该得到很好的记录,因为这种技术与每个人都希望工作的链接原则相冲突。

在仪表板应用程序中使用我们的插件

在完成我们的mutationObserver插件后,现在让我们看看如何使用它来实现counter子模块的,我们在前面几章的仪表板实现中使用了该子模块:

(function() { 
    'use strict'; 
    dashboard.counter = dashboard.counter || {}; 

    var $counter; 

    dashboard.counter.init = function() { 
        $counter = $('#dashboardItemCounter'); 
        var $boxContainer = dashboard.$container
          .find('.boxContainer'); 

        $boxContainer.mutationObserver(function(mutation) { 
            dashboard.counter.setValue($boxContainer.children().length); 
        }); 
    }; 

    dashboard.counter.setValue = function (value) { 
        $counter.text(value); 
    }; 
})(); 

正如您在上面的实现中所看到的,我们的插件很好地进行了抽象,并通过提供一个通用的、灵活的和可重用的 API 替换了旧的实现。该实现不再监听页面不同按钮上的点击事件,而是使用mutationObserver插件并观察boxContainer元素对子元素的添加或删除。此外,该实施变更不会影响counter模块的功能,因为所有变更都封装在模块中,因此该模块的工作方式似乎相同。

使用 jQuery 插件样板

jQuery 样板项目,可在获取 https://github.com/jquery-boilerplate/jquery-patterns 提供了几个模板,可作为实现健壮和可扩展插件的起点。这些模板包含了许多最佳实践和设计模式,如本章前面分析的那些。每一个模板都包含了许多协同工作的最佳实践,试图提供更好地匹配各种用例的良好起点。

也许最广泛使用的模板是来自 Adam Sontag 和 Addy Osmani 的jquery.basic.plugin-boilerplate,尽管它被描述为初学者及以上的通用模板,但它成功地涵盖了 jQuery 插件开发的大部分方面。该模板的独特之处在于它所遵循的面向对象方法,它的呈现方式可以帮助您编写更好的结构化代码,而不会使在实现上引入自定义变得更加困难。让我们继续分析它的源代码:

/*! 
 * jQuery lightweight plugin boilerplate 
 * Original author: @ajpiano 
 * Further changes, comments: @addyosmani 
 * Licensed under the MIT license 
 */ 
;(function ( $, window, document, undefined ) { 
  var pluginName = "defaultPluginName", 
    defaults = { 
      propertyName: "value" 
    }; 
  function Plugin( element, options ) { 
    this.element = element; 
    this.options = $.extend( {}, defaults, options) ; 
    this._defaults = defaults; 
    this._name = pluginName; 
    this.init(); 
  } 
  Plugin.prototype = { 
    init: function() { /* Place initialization logic here  */ },
    yourOtherFunction: function(options) { /* some logic */ }
  };
  // A really lightweight plugin wrapper around the constructor, 
  // preventing against multiple instantiations 
  $.fn[pluginName] = function ( options ) { 
    return this.each(function () { 
      if (!$.data(this, "plugin_" + pluginName)) { 
        $.data(this, "plugin_" + pluginName, 
        new Plugin( this, options )); 
      } 
    }); 
  }; 

})( jQuery, window, document ); 

IIFE 之前的分号是为了避免在脚本与可能缺少结尾分号的文件连接(可能是缩小)时出现错误。在下面的右侧,样板文件使用pluginName变量作为命名插件的干巴巴的方式,并将其名称用于任何其他情况。另外一个好处是,如果我们需要重命名插件,我们所要做的就是更改这个变量的值,并相应地重命名插件的.js文件。

按照我们前面看到的最佳实践,使用一个变量来保存插件的默认选项,正如我们在后面几行看到的那样,它使用$.extend()方法与用户提供的选项合并。请记住,如果我们想公开默认选项,我们所要做的就是将其定义为插件名称空间的一部分:$.fn[pluginName].defaultOptions = defaults;

实际的插件定义可以在这个样板代码的末尾找到。按照已经讨论过的最佳实践,它使用$.fn.each()迭代集合中的项目并返回其结果,这相当于返回this。然后,它通过使用$.data()方法和前缀插件名称作为关联键,确保集合的每个项都存在一个插件状态实例。

Plugin构造函数用于创建插件的状态对象,在将 DOM 元素和最终插件选项存储为对象属性后,调用其原型的init()方法。init()方法是定义初始化代码的建议位置,例如,它可以像本章前面所做的那样实例化一个新的 MutationObserver。

为插件添加方法

默认情况下,定义为原型一部分的每个方法仅可供内部使用。另一方面,我们可以轻松地扩展上述实现,使所有用户都可以使用该方法,如下所示:

$.fn[pluginName] = function ( options, extraParam ) { 
  return this.each(function () { 
    var instance = $.data(this, "plugin_" + pluginName); 
    if (!instance) { 
      instance = new Plugin( this, options ); 
      $.data(this, "plugin_" + pluginName, instance); 
    } else if (options === 'yourOtherFunction') { 
      instance.yourOtherFunction(this, extraParam); 
    } 
  }); 
};

使用这个样板时要遵循的一条准则是通过向Plugin的原型添加额外的方法来扩展插件。此外,尽量减少对插件定义的修改,最好是单行方法调用。

为了使实现更具可伸缩性,关于如何调用插件方法,以及如果我们想为插件内部或私人使用的方法添加抽象方法,我们可以引入以下更改:

$.fn[pluginName] = function ( options ) {
  var restArgs = Array.prototype.slice.call(arguments, 1);
  return this.each(function () {
    var instance = $.data(this, "plugin_" + pluginName);
    if (!instance) {
      instance = new Plugin( this, options );
      $.data(this, "plugin_" + pluginName, instance);
    } else if (typeof options === 'string' && // method name
      options[0] !== '_' && // protect private methods
      typeof instance[options] === 'function') {
      instance[options].apply(instance, restArgs);
    }
  });
};

在上面的实现中,我们使用第一个参数来标识需要调用的方法,然后使用 rest 参数来调用它。我们还添加了一个检查,以防止调用以下划线开头的方法,根据常见约定,下划线是用于内部或私人用途的。因此,为了给插件的公共 API 添加一个额外的方法,我们只需要在前面看到的Plugin.prototype中声明它。

当您已经在应用程序中使用 jQuery UI 时,实现插件的另一个很好的方法是使用$.widget()方法,也就是众所周知的 jQuery UI 小部件工厂。它的实现抽象了我们在本章中看到的样板代码的几个部分,并帮助创建复杂而健壮的插件。欲了解更多的信息,请阅读以下文档:http://api.jqueryui.com/jQuery.widget/

选择名字

最后,在学习了创建 jQuery 插件所需的最佳实践之后,让我们谈谈命名约定以及在何处发布新的闪亮插件。

正如您可能已经看到的一样,大多数 jQuery 插件使用以下命名约定:jQuery myPluginName 用于其项目站点和存储库,并将其实现存储在名为jquery.mypluginname.js的文件中。在为你的插件确定了一些可能的名称之后,花点时间在网上搜索,以确认没有其他人具有相同的项目名称。jQuery 文档建议在 NPM 上搜索插件,并使用jquery-plugin关键字优化结果。这显然是发布插件的最佳方式,这样其他人就可以很容易地找到它。

有关 NPM 的更多信息,您可以访问:https://www.npmjs.com/

另一个流行的搜索和托管 JavaScript 库的地方是 GitHub。您可以在找到其存储库搜索页面 https://github.com/search?l=JavaScript ,它过滤搜索结果以仅包括 JavaScript 项目,并搜索现有插件和已使用的项目名称。因为在我们的例子中,我们主要关注 jQuery 插件,所以通过搜索遵循前面提到的命名约定 jQuery myPluginName 的项目名称,您将获得更好的结果。

直到最近,开发人员还可以搜索现有插件,并在官方 jQuery 插件注册中心(注册一个新插件 http://plugins.jquery.com/ 。不幸的是,它已经停产,现在只允许搜索旧插件,没有新的提交。

总结

在本章中,我们学习了如何通过实现和使用插件来扩展 jQuery。我们首先看到了一个实现 jQuery 插件的最简单方法的示例,并分析了构成一个优秀插件的特性,以及遵循 jQuery 库原则的特性。

然后介绍了开发人员社区中创建 jQuery 插件的最常见的开发模式。我们分析了它们各自解决的实现问题以及更适合它们的用例。

完成本章之后,我们现在能够将应用程序的一部分抽象为可重用和可扩展的 jQuery 插件,这些插件使用与每个用例最匹配的开发模式进行结构化。

在下一章中,我们将介绍几种可用于提高 jQuery 应用程序性能的优化技术,特别是当它们变得庞大和复杂时。我们将讨论一些简单的实践,例如使用 CDN 加载第三方库,并继续讨论更高级的主题,例如延迟加载实现的模块。**