如何创建一个带有方法的jQuery插件?

198
我正在尝试编写一个jQuery插件,它将为调用它的对象提供其他函数/方法。我已经浏览了在线教程(已经浏览了过去2个小时),但最多只介绍如何添加选项,而不是其他函数。
这就是我想做的:
//通过调用该div的插件来将div格式化为消息容器
$("#mydiv").messagePlugin();
$("#mydiv").messagePlugin().saySomething("hello");
或类似的东西。 这就是问题的关键:我调用插件,然后调用与该插件相关联的函数。我似乎找不到方法来做到这一点,而我以前见过许多插件都可以做到这一点。
这是我目前为插件所做的事情:
jQuery.fn.messagePlugin = function() {
  return this.each(function(){
    alert(this);
  });

  //i tried to do this, but it does not seem to work
  jQuery.fn.messagePlugin.saySomething = function(message){
    $(this).html(message);
  }
};

我怎样才能实现这样的效果?

谢谢!


更新于2013年11月18日:根据Hari的评论和赞数,我已将正确答案更改为他的回答。

20个回答

320
根据 jQuery 插件创作页面 (http://docs.jquery.com/Plugins/Authoring) 的介绍,最好不要混淆 jQuery 和 jQuery.fn 命名空间。他们建议使用以下方法:
(function( $ ){

    var methods = {
        init : function(options) {

        },
        show : function( ) {    },// IS
        hide : function( ) {  },// GOOD
        update : function( content ) {  }// !!!
    };

    $.fn.tooltip = function(methodOrOptions) {
        if ( methods[methodOrOptions] ) {
            return methods[ methodOrOptions ].apply( this, Array.prototype.slice.call( arguments, 1 ));
        } else if ( typeof methodOrOptions === 'object' || ! methodOrOptions ) {
            // Default to "init"
            return methods.init.apply( this, arguments );
        } else {
            $.error( 'Method ' +  methodOrOptions + ' does not exist on jQuery.tooltip' );
        }    
    };


})( jQuery );

基本上,你将函数存储在一个数组中(作用域限定为包装函数),如果传递的参数是字符串,则检查该条目,如果参数是对象(或 null),则回退到默认方法(这里是“init”)。 然后你可以像这样调用这些方法...
$('div').tooltip(); // calls the init method
$('div').tooltip({  // calls the init method
  foo : 'bar'
});
$('div').tooltip('hide'); // calls the hide method
$('div').tooltip('update', 'This is the new tooltip content!'); // calls the update method

Javascript的"arguments"变量是一个包含所有参数的数组,因此它可以处理带有任意长度的函数参数。


2
这是我使用的方法。您还可以通过 $.fn.tooltip('methodname', params) 静态调用这些方法。 - Rake36
18
对于像我一样第一次说出“参数变量从哪里来”的人——https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments——我已经使用JS很长时间了,但从未知道这一点。你每天都在学习新东西! - streetlogics
1
假设我定义了配置默认值(可以通过在插件初始化时传递对象进行覆盖),那么我该如何访问它们的值?还是说我必须为每个属性创建方法/获取器? - Maciej Gurban
2
@DiH,我完全同意你的看法。这种方法似乎很好,但是除了“init”之外,它不会让你从任何其他地方访问全局设置。 - Stephen Collins
4
这种技术存在一个重大问题!它并没有像你想的那样为选择器中的每个元素创建新实例,而是仅创建一个附加到选择器本身的单个实例。查看我的答案以获取解决方案。 - Kevin Jurkowski
显示剩余6条评论

56

这是我用于创建带有附加方法插件的模式。你可以像这样使用它:

$('selector').myplugin( { key: 'value' } );

或者,直接调用一个方法:

$('selector').myplugin( 'mymethod1', 'argument' );

例子:

;(function($) {

    $.fn.extend({
        myplugin: function(options,arg) {
            if (options && typeof(options) == 'object') {
                options = $.extend( {}, $.myplugin.defaults, options );
            }

            // this creates a plugin for each element in
            // the selector or runs the function once per
            // selector.  To have it do so for just the
            // first element (once), return false after
            // creating the plugin to stop the each iteration 
            this.each(function() {
                new $.myplugin(this, options, arg );
            });
            return;
        }
    });

    $.myplugin = function( elem, options, arg ) {

        if (options && typeof(options) == 'string') {
           if (options == 'mymethod1') {
               myplugin_method1( arg );
           }
           else if (options == 'mymethod2') {
               myplugin_method2( arg );
           }
           return;
        }

        ...normal plugin actions...

        function myplugin_method1(arg)
        {
            ...do method1 with this and arg
        }

        function myplugin_method2(arg)
        {
            ...do method2 with this and arg
        }

    };

    $.myplugin.defaults = {
       ...
    };

})(jQuery);

9
与jQuery UI相同的模式,我不喜欢所有的魔法字符串,但还有其他方法吗! - redsquare
8
这似乎是一种非标准的做法 - 是否有比这更简单的方法,比如链接函数吗?谢谢! - Yuval Karmi
2
@yuval -- 通常 jQuery 插件返回的是 jQuery 或一个值,而不是插件本身。这就是为什么在调用插件时,方法的名称会作为参数传递给插件的原因。您可以传递任意数量的参数,但您需要调整函数和参数解析。最好将它们设置为匿名对象,就像您展示的那样。 - tvanfosson
1
你第一行代码中的 ; 是什么意思?能给我解释一下吗? :) - GusDeCooL
4
@GusDeCooL 这只是确保我们开始一个新语句,这样我们的函数定义不会被解释为其他格式不良的Javascript的参数(即初始括号不会被视为函数调用运算符)。请参见https://dev59.com/gms05IYBdhLWcg3wPfcR - tvanfosson
显示剩余16条评论

35
这个方法怎么样:
jQuery.fn.messagePlugin = function(){
    var selectedObjects = this;
    return {
             saySomething : function(message){
                              $(selectedObjects).each(function(){
                                $(this).html(message);
                              });
                              return selectedObjects; // Preserve the jQuery chainability 
                            },
             anotherAction : function(){
                               //...
                               return selectedObjects;
                             }
           };
}
// Usage:
$('p').messagePlugin().saySomething('I am a Paragraph').css('color', 'red');
选定的对象存储在messagePlugin闭包中,该函数返回一个包含与插件相关联的函数的对象,在每个函数中,您都可以对当前选定的对象执行所需的操作。 您可以在此处测试和使用代码。 编辑:更新了代码以保留jQuery链式操作的功能。

1
我有点难以理解这会是什么样子。假设我有需要在第一次运行时执行的代码,我需要在我的代码中先进行初始化 - 类似于这样:$('p').messagePlugin(); 然后在代码的后面,我想像这样调用函数saySomething:$('p').messagePlugin().saySomething('something'); 这不会重新初始化插件然后调用函数吗?使用封装和选项会是什么样子呢?非常感谢。-yuval - Yuval Karmi
1
这有点破坏了jQuery的链式编程范式。 - tvanfosson
3
每次调用messagePlugin()函数,它会创建一个新的对象并包含这两个函数,对吧? - w00t
4
这种方法的主要问题在于,如果你想让 $('p').messagePlugin() 后面可以继续链式调用,就必须要调用它返回的两个函数中的一个。 - Joshua Bambrick
@CMS,请问我还有一个问题 在Stackoverflow上,使用链式方法;但是即使使用了你的建议(返回“selectedObjects”变量),它也无法工作,可以看这里的jsfiddle - Peter Krauss
显示剩余3条评论

20

当前所选答案存在问题,因为您并没有像您认为的那样为选择器中的每个元素创建一个自定义插件的新实例...实际上您只创建了一个单一的实例,并将选择器本身作为范围传递进去。

查看这个fiddle以获取更深入的解释。

相反,您需要使用jQuery.each循环遍历选择器,并为选择器中的每个元素实例化一个自定义插件的新实例。

以下是如何实现:

(function($) {

    var CustomPlugin = function($el, options) {

        this._defaults = {
            randomizer: Math.random()
        };

        this._options = $.extend(true, {}, this._defaults, options);

        this.options = function(options) {
            return (options) ?
                $.extend(true, this._options, options) :
                this._options;
        };

        this.move = function() {
            $el.css('margin-left', this._options.randomizer * 100);
        };

    };

    $.fn.customPlugin = function(methodOrOptions) {

        var method = (typeof methodOrOptions === 'string') ? methodOrOptions : undefined;

        if (method) {
            var customPlugins = [];

            function getCustomPlugin() {
                var $el          = $(this);
                var customPlugin = $el.data('customPlugin');

                customPlugins.push(customPlugin);
            }

            this.each(getCustomPlugin);

            var args    = (arguments.length > 1) ? Array.prototype.slice.call(arguments, 1) : undefined;
            var results = [];

            function applyMethod(index) {
                var customPlugin = customPlugins[index];

                if (!customPlugin) {
                    console.warn('$.customPlugin not instantiated yet');
                    console.info(this);
                    results.push(undefined);
                    return;
                }

                if (typeof customPlugin[method] === 'function') {
                    var result = customPlugin[method].apply(customPlugin, args);
                    results.push(result);
                } else {
                    console.warn('Method \'' + method + '\' not defined in $.customPlugin');
                }
            }

            this.each(applyMethod);

            return (results.length > 1) ? results : results[0];
        } else {
            var options = (typeof methodOrOptions === 'object') ? methodOrOptions : undefined;

            function init() {
                var $el          = $(this);
                var customPlugin = new CustomPlugin($el, options);

                $el.data('customPlugin', customPlugin);
            }

            return this.each(init);
        }

    };

})(jQuery);

还有一个可运行的代码片段

你会注意到,在第一个代码片段中,所有的div元素总是向右移动了完全相同的像素。这是因为只有一个选项对象用于选择器中的所有元素。

使用上面所述的方法,你会发现在第二个代码片段中,每个div元素都没有对齐并且随机移动(除了第一个div元素,因为它的随机数生成器在第89行始终设置为1)。这是因为我们现在为选择器中的每个元素适当地实例化了一个新的自定义插件实例。每个元素都有自己的选项对象,并且不保存在选择器中,而是保存在自定义插件实例中。

这意味着你可以从新的jQuery选择器中访问在DOM中特定元素上实例化的自定义插件的方法,并且不必像在第一个代码片段中那样强制缓存它们。

例如,使用第二个代码片段中的技术将返回所有选项对象的数组。在第一个代码片段中,它将返回undefined。

$('div').customPlugin();
$('div').customPlugin('options'); // would return an array of all options objects

这是您需要在第一个fiddle中访问选项对象的方式,它仅返回单个对象,而不是对象数组:

var divs = $('div').customPlugin();
divs.customPlugin('options'); // would return a single options object

$('div').customPlugin('options');
// would return undefined, since it's not a cached selector

我建议使用上面的技巧,而不是当前选定答案中的技巧。


谢谢,这对我很有帮助,特别是向我介绍了 .data() 方法。非常方便。顺便说一句,您还可以通过使用匿名方法来简化代码。 - dalemac
jQuery链式调用在使用此方法时无法正常工作... $('.my-elements').find('.first-input').customPlugin('update', 'first value').end().find('.second-input').customPlugin('update', 'second value'); 返回 Cannot read property 'end' of undefined。https://jsfiddle.net/h8v1k2pL/ - Alex G

15

使用 jQuery UI Widget Factory

示例:

$.widget( "myNamespace.myPlugin", {

    options: {
        // Default options
    },
 
    _create: function() {
        // Initialization logic here
    },
 
    // Create a public method.
    myPublicMethod: function( argument ) {
        // ...
    },

    // Create a private method.
    _myPrivateMethod: function( argument ) {
        // ...
    }
 
});

初始化:

$('#my-element').myPlugin();
$('#my-element').myPlugin( {defaultValue:10} );

方法调用:

$('#my-element').myPlugin('myPublicMethod', 20);

(这就是 jQuery UI 库的构建方式。)


a) 那是众所周知的谬论 b) 每个更好的JS IDE都有代码补全或linting c) 谷歌一下 - daniel.sedlacek
那是纯粹的错觉,Sedlacek先生。 - zrooda
根据文档:该系统称为Widget Factory,并作为jQuery UI 1.8的一部分公开为jQuery.widget;但是,它可以独立于jQuery UI使用。如何在没有jQuery UI的情况下使用$.widget? - Airn5475

13
一个更简单的方法是使用嵌套函数,然后可以以面向对象的方式将它们链接在一起。例如:
jQuery.fn.MyPlugin = function()
{
  var _this = this;
  var a = 1;

  jQuery.fn.MyPlugin.DoSomething = function()
  {
    var b = a;
    var c = 2;

    jQuery.fn.MyPlugin.DoSomething.DoEvenMore = function()
    {
      var d = a;
      var e = c;
      var f = 3;
      return _this;
    };

    return _this;
  };

  return this;
};

以下是如何调用它的方法:

var pluginContainer = $("#divSomeContainer");
pluginContainer.MyPlugin();
pluginContainer.MyPlugin.DoSomething();
pluginContainer.MyPlugin.DoSomething.DoEvenMore();

但要小心。在嵌套函数被创建之前,您无法调用它。因此,您不能这样做:

var pluginContainer = $("#divSomeContainer");
pluginContainer.MyPlugin();
pluginContainer.MyPlugin.DoSomething.DoEvenMore();
pluginContainer.MyPlugin.DoSomething();
DoEvenMore函数不存在,因为DoSomething函数还没有运行,而创建DoEvenMore函数是需要它的。对于大多数jQuery插件,你只需要有一层嵌套函数,而不是像这里展示的两层。
在创建嵌套函数时,请确保在父函数中任何其他代码执行之前定义这些函数。
最后,请注意,“this”成员存储在名为“_this”的变量中。对于嵌套函数,如果您需要引用调用客户端中的实例,则应返回“_this”。您不能在嵌套函数中返回“this”,因为那样会返回对函数的引用,而不是对jQuery实例的引用。返回jQuery引用允许您在返回中链接内在的jQuery方法。

2
这很棒 - 我只是想知道为什么jQuery似乎更喜欢按名称调用方法,例如.plugin('method')模式? - w00t
6
无效。如果在两个不同的容器上调用插件,则内部变量会被覆盖(即_this)。 - mbrochh
失败:不允许 pluginContainer.MyPlugin.DoEvenMore().DoSomething();。 - Paul Swetz

9

我从 jQuery Plugin Boilerplate 中获取了它。

还在 jQuery Plugin Boilerplate, reprise 中描述过。

// jQuery Plugin Boilerplate
// A boilerplate for jumpstarting jQuery plugins development
// version 1.1, May 14th, 2011
// by Stefan Gabos

// remember to change every instance of "pluginName" to the name of your plugin!
(function($) {

    // here we go!
    $.pluginName = function(element, options) {

    // plugin's default options
    // this is private property and is accessible only from inside the plugin
    var defaults = {

        foo: 'bar',

        // if your plugin is event-driven, you may provide callback capabilities
        // for its events. execute these functions before or after events of your
        // plugin, so that users may customize those particular events without
        // changing the plugin's code
        onFoo: function() {}

    }

    // to avoid confusions, use "plugin" to reference the
    // current instance of the object
    var plugin = this;

    // this will hold the merged default, and user-provided options
    // plugin's properties will be available through this object like:
    // plugin.settings.propertyName from inside the plugin or
    // element.data('pluginName').settings.propertyName from outside the plugin,
    // where "element" is the element the plugin is attached to;
    plugin.settings = {}

    var $element = $(element), // reference to the jQuery version of DOM element
    element = element; // reference to the actual DOM element

    // the "constructor" method that gets called when the object is created
    plugin.init = function() {

    // the plugin's final properties are the merged default and
    // user-provided options (if any)
    plugin.settings = $.extend({}, defaults, options);

    // code goes here

   }

   // public methods
   // these methods can be called like:
   // plugin.methodName(arg1, arg2, ... argn) from inside the plugin or
   // element.data('pluginName').publicMethod(arg1, arg2, ... argn) from outside
   // the plugin, where "element" is the element the plugin is attached to;

   // a public method. for demonstration purposes only - remove it!
   plugin.foo_public_method = function() {

   // code goes here

    }

     // private methods
     // these methods can be called only from inside the plugin like:
     // methodName(arg1, arg2, ... argn)

     // a private method. for demonstration purposes only - remove it!
     var foo_private_method = function() {

        // code goes here

     }

     // fire up the plugin!
     // call the "constructor" method
     plugin.init();

     }

     // add the plugin to the jQuery.fn object
     $.fn.pluginName = function(options) {

        // iterate through the DOM elements we are attaching the plugin to
        return this.each(function() {

          // if plugin has not already been attached to the element
          if (undefined == $(this).data('pluginName')) {

              // create a new instance of the plugin
              // pass the DOM element and the user-provided options as arguments
              var plugin = new $.pluginName(this, options);

              // in the jQuery version of the element
              // store a reference to the plugin object
              // you can later access the plugin and its methods and properties like
              // element.data('pluginName').publicMethod(arg1, arg2, ... argn) or
              // element.data('pluginName').settings.propertyName
              $(this).data('pluginName', plugin);

           }

        });

    }

})(jQuery);

你的方法破坏了jQuery的链式调用:$('.first-input').data('pluginName').publicMethod('new value').css('color', red); 返回 Cannot read property 'css' of undefined https://jsfiddle.net/h8v1k2pL/1/ - Alex G
@AlexG,根据这个例子,您需要添加return $element,因此在这个例子中,您需要将其更改为plugin.foo_public_method = function() {/* Your Code */ return $element;} @Salim,感谢您的帮助... https://github.com/AndreaLombardo/BootSideMenu/pull/34 - CrandellWS

6

虽然有些晚了,但也许它能帮助某个人。

我曾经面临着相同的情况,需要创建一个带有一些方法的jQuery插件,在阅读了一些文章和尝试之后,我创建了一个jQuery插件样板(https://github.com/acanimal/jQuery-Plugin-Boilerplate)。

此外,我使用它开发了一个标签管理插件(https://github.com/acanimal/tagger.js),并撰写了两篇博客文章,逐步解释如何创建一个jQuery插件(https://www.acuriousanimal.com/blog/20130115/things-i-learned-creating-a-jquery-plugin-part-i)。


可能是我遇到的关于初学者创建jQuery插件的最好的帖子 - 谢谢 ;) - Dex Dave
感谢您采用这种明智的方法。正确创建jQuery插件实在太过复杂,这确实是jQuery和JavaScript整体上的一个重大弱点,因为它具有原型奇怪性。 - user3700562

5

您可以进行以下操作:

(function($) {
  var YourPlugin = function(element, option) {
    var defaults = {
      //default value
    }

    this.option = $.extend({}, defaults, option);
    this.$element = $(element);
    this.init();
  }

  YourPlugin.prototype = {
    init: function() { },
    show: function() { },
    //another functions
  }

  $.fn.yourPlugin = function(option) {
    var arg = arguments,
        options = typeof option == 'object' && option;;
    return this.each(function() {
      var $this = $(this),
          data = $this.data('yourPlugin');

      if (!data) $this.data('yourPlugin', (data = new YourPlugin(this, options)));
      if (typeof option === 'string') {
        if (arg.length > 1) {
          data[option].apply(data, Array.prototype.slice.call(arg, 1));
        } else {
          data[option]();
        }
      }
    });
  };
});

通过这种方式,您的插件对象将作为数据值存储在元素中。

//Initialization without option
$('#myId').yourPlugin();

//Initialization with option
$('#myId').yourPlugin({
  // your option
});

// call show method
$('#myId').yourPlugin('show');

3

使用触发器怎么样?有人知道使用它们的任何缺点吗?

优点是所有内部变量都可以通过触发器访问,代码非常简单。

jsfiddle上查看。

示例用法

<div id="mydiv">This is the message container...</div>

<script>
    var mp = $("#mydiv").messagePlugin();

    // the plugin returns the element it is called on
    mp.trigger("messagePlugin.saySomething", "hello");

    // so defining the mp variable is not needed...
    $("#mydiv").trigger("messagePlugin.repeatLastMessage");
</script>

插件

jQuery.fn.messagePlugin = function() {

    return this.each(function() {

        var lastmessage,
            $this = $(this);

        $this.on('messagePlugin.saySomething', function(e, message) {
            lastmessage = message;
            saySomething(message);
        });

        $this.on('messagePlugin.repeatLastMessage', function(e) {
            repeatLastMessage();
        });

        function saySomething(message) {
            $this.html("<p>" + message + "</p>");
        }

        function repeatLastMessage() {
            $this.append('<p>Last message was: ' + lastmessage + '</p>');
        }

    });

}

1
根据您的评论,我唯一看到的问题可能是事件系统的误用。仅仅为了调用一个函数而使用事件是不典型的,似乎有些过度设计,容易出错。通常情况下,您会以发布-订阅的方式使用事件,例如,一个函数发布某个条件“A”已经发生。其他对“A”感兴趣的实体会监听“A”已经发生的消息,然后执行相应操作。您似乎将其用作推送“命令”,但假设只有一个侦听器。您需要小心,确保您的语义不会被(其他人)添加侦听器破坏。 - tvanfosson
@tvanfosson 感谢您的评论。我知道这不是常见的技术,如果有人意外添加事件监听器,可能会导致问题,但如果它以插件命名,那么这种情况非常不可能发生。我不知道是否存在任何性能相关的问题,但对我来说,代码本身似乎比其他解决方案简单得多,但我可能会错过什么。 - István Ujj-Mészáros

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接