jQuery plugin design pattern (common practice?) fo

2019-01-16 10:54发布

I've been developing jQuery plugins for quite some time now, and I like to think I know how to design one well by now. One issue keeps nagging me though, and that is how to deal with private functions in a powerful yet elegant manner.

My plugins generally look something like this:

(function($) {

  $.fn.myplugin = function(...) {
    ...
    // some shared functionality, for example:
    this.css('background-color', 'green');
    ...
  };
  $.fn.mypluginAnotherPublicMethod = function(...) {
    ...
    // some shared functionality, for example:
    this.css('background-color', 'red');
    ...
  };

}(jQuery));

Now my question is: how to neatly DRY up that shared functionality? An obvious solution would be to put it in a function within the plugin's namespace:

var fill = function($obj, color) {
  $obj.css('background-color', color);
};

Although this solution is effective and nicely namespaced, I really dislike it. For one simple reason: I have to pass it the jQuery object. I.e. I have to call it like this: fill(this, 'red');, while I would like to call it like this: this.fill('red');

Of course we could achieve this result by simply putting fill into jQuery.fn. But that feels very uncomfortable. Imagine having ten plugins developed based on this approach and each plugin putting five of those 'private' functions into the jQuery function namespace. It ends up in a big mess. We could mitigate by prefixing each of these functions with the name of the plugin they belong to, but that doesn't really make it more attractive. These functions are supposed to be private to the plugin, so we do not want to expose them to the outside world at all (at least not directly).

So there's my question: does anyone of you have suggestions for how to get the best of both worlds. That is; plugin code being able to call 'private' plugin functions in a way similar to this.fill('red') (or this.myplugin.fill('red') or even this.myplugin().fill('red') etc.), while preventing jQuery function namespace pollution. And of course it should be light-weight, as these private functions might be called very frequently.


UPDATE: Thanks for your suggestions.

I especially like David's idea of defining an object type that holds the 'private' functions and wraps a jQuery object. The only problem with it is that it still disallows me from chaining 'private' and 'public' functions. Which was big reason to want a syntax like this.fill('red') to begin with.

I ended up with a solution which I consider not tremendously elegant, but appealing to the 'best of both worlds' cause:

$.fn.chain = function(func) {
    return func.apply(this, Array.prototype.slice.call(arguments, 1));
};

Which allows for constructs like:

this.
    find('.acertainclass').
    chain(fill, 'red').
    click(function() {
        alert("I'm red");
    });

I cross-posted my question in other places, which also collected some interesting responses:

4条回答
叼着烟拽天下
2楼-- · 2019-01-16 11:04

How about (within the plugin's scope):

var fill = function ()
{
    (function (color) 
    {
        this.css ('backgrorund-color', color);
        //.. your stuff here ...
    }).apply (this, arguments);
}

$.fn.myplugin = function ()
{
    fill ('green');
}

That way, fill will retain the jQuery context you're in, and is still private to your plugin


Amended: the above is incorrect w.r.t. scoping, Try the following instead:

var fill = function (color)
{
    if (!$this) return; // break if not within correct context

    $this.css ('backgrorund-color', color);
    //.. your stuff here ...
}

$.fn.myplugin = function ()
{
    var $this = $(this); // local ref to current context
    fill ('green');
}
查看更多
三岁会撩人
3楼-- · 2019-01-16 11:05

You might want to take a look at how the jQuery UI Widget Factory is implemented.

The basic approach is like this:

(function($){
    $.fn.myplugin = function(method)
    {
        if (mp[method]) // map $('foo').myplugin('bar', 'baz') to mp.bar('baz')
        {
            return mp[method].apply(this, Array.prototype.slice.call(arguments, 1));
        }
        else if (typeof method === 'object' || ! method)
        {
            return mp.init.apply(this, arguments); // if called without arguments, init
        }
        else
        {
            $.error('Method ' +  method + ' does not exist on $.myplugin');
        }
    };

    // private methods, internally accessible via this.foo, this.bar
    var foo = function() { … };
    var bar = function() { … };

    var private = { // alternative approach to private methods, accessible via this.private.foo
        foo : function() { … },
        bar : function() { … }
    }

    var mp = { // public methods, externally accessible via $.myplugin('foo', 'bar')
        init : function( options )
        {
            return this.each(function()
            {
                // do init stuff
            }
        },
        foo : function() { … },
        bar : function() { … }
    };
})(jQuery);
查看更多
We Are One
4楼-- · 2019-01-16 11:06

One thing first: if you would like to call something like this.fill('red'); where this is an instance of jQuery, you have to extend the jQuery prototype and make fill() "public". jQuery provides guidelines for extending it's prototype using so called "plugins" that can be added using $.fn.fill, which is the same as jQuery.prototype.fill.

In jQuery callbacks, this is often a reference to the HTML Element, and you can't add prototypes to those (yet). That is one of the reason why jQuery wraps elements and return jQuery instances that can be easily extended.

Using the (function(){})(); syntax, you can create and execute "private" javascript on the fly, and it all disappears when it's done. Using this technique, you can create your own jQuery-like syntax that wraps jQuery into your own private chainable object.

(function(){
    var P = function(elem) {
        return new Private(elem);
    };
    var Private = function(elem) {
        this.elem = jQuery(elem);
    }
    Private.prototype = {
        elem: null,
        fill: function(col) {
            this.elem.css('background',col);
            return this;
        },
        color: function(col) {
            this.elem.css('color', col);
            return this;
        }
    }

    $.fn.myplugin = function() {
        P(this).fill('red');
    };
    $.fn.myotherplugin = function() {
        P(this).fill('yellow').color('green');
    };

})();

$('.foo').myplugin();
$('.bar').myotherplugin();

console.log(typeof P === 'undefined') // should print 'true'

This way, the P stands for your own toolbox of "private" functions. They won't be available anywhere else in the code or in the jQuery namespace unless you attach them somewhere. You can add as many methods as you like in the Private object, and as long as you return this, you can also chain them jQuery-style as I did in the example.

查看更多
地球回转人心会变
5楼-- · 2019-01-16 11:22

Unfortunately, "private" methods (or any property for that matter) can never be called with a "this" prefix in javascript. Anything which is called like this.myFunc(myArgs) must be publicly available.

And "private" methods can only be called from within the scope in which they were defined.

Your original solution is the only one that will work. Yes, it's a pain having to pass in this, but there's no more verbosity than there would be if your impossible request was possible:

this.fill('green');
//or
fill(this,'green');

As you can see, they both take up exactly the same number of characters in your code.

Sorry to say, but you're stuck with this as a solution, unless you want to create a new namespace and make them not private - which is simply going to add to the amount of code you need to write, i.e. what you indirectly called "not directly exposed":

this.myplugin.fill('green');

...is more verbose, thus kind of defeats the purpose.

Javascript is not like other languages, there are no "private" members per-se, only members accessible within closures, which can sometimes be used in a similar way to private members, but is more of a "workaround", and not the "real-deal" private members you are looking for.

It can be difficult to come to terms with this (I often struggle), but don't try to mould javascript into what you understand from other languages, take it for what it is...

查看更多
登录 后发表回答