「jQuery插件开发日记」(二)高级插件理念 - [翻译]

_Advanced Plugin Concepts_,翻译自 jQuery 官方网站。

默认设置的共有接口

对于上一篇文章最后的代码,我们可以改进,也应该改进的地方就是,为我们的插件的默认设置提供共有接口,使得用户可以直接更改默认设置。

这样做的好处就是可以让用户用最少的代码量来定制我们的插件。这次的例子是:

// Plugin definition.
$.fn.hilight = function( options ) {
 
    // Extend our default options with those provided.
    // Note that the first argument to extend is an empty
    // object – this is to keep from overriding our "defaults" object.
    var opts = $.extend( {}, $.fn.hilight.defaults, options );
 
    // Our plugin implementation code goes here.
 
};
 
// Plugin defaults – added as a property on our plugin function.
$.fn.hilight.defaults = {
    foreground: "red",
    background: "yellow"
};

现在用户可以这样更改默认设定:

// This needs only be called once and does not
// have to be called from within a "ready" block
$.fn.hilight.defaults.foreground = "blue";

这样使用我们的插件:

$( "#myDiv" ).hilight();

你可以看到,我们允许用户使用仅仅一行代码就覆盖了某个默认设定。除此之外,用户仍然可以传入参数来覆盖他们设定的默认设置。

// Override plugin default foreground color.
$.fn.hilight.defaults.foreground = "blue";
 
// ...
 
// Invoke plugin using new defaults.
$( ".hilightDiv" ).hilight();
 
// ...
 
// Override default by passing options to plugin method.
$( "#green" ).hilight({
    foreground: "green"
});

附属函数的公共接口

为某些附属函数提公共接口,能够非常好的去扩展和让别人扩展你的插件。

举个例子,我们的插件可能实现了一个叫 format 的函数, 这个函数格式化强调文本的形式。我们在 hilight 函数下面定义了 format 的默认方法。

// Plugin definition.
$.fn.hilight = function( options ) {
 
    // Iterate and reformat each matched element.
    return this.each(function() {
 
        var elem = $( this );
 
        // ...
 
        var markup = elem.html();
 
        // Call our format function.
        markup = $.fn.hilight.format( markup );
 
        elem.html( markup );
 
    });
 
};
 
// Define our format function.
$.fn.hilight.format = function( txt ) {
    return "<strong>" + txt + "</strong>";
};

我们也可以在设置中间允许一个回调函数,来覆盖默认的 format 函数, 这也是一个支持定制的非常棒的方法。
使用这些技术,其他人可以非常方便的定制你的插件,然后发布。换句话说,其他人可以为你的插件来写插件。

考虑到这篇文章里这个苍白的例子并不是非常具有说服力,一个现实的例子就是 Cycle Plugin。这个插件是一个幻灯片插件,内置非常多切换特效,像滚动、滑动、淡出等。但是实际上它不可能定义每个人想要的效果,那么插件的扩展性就非常重要了。

Cycle Plugin 对外暴露了一个 transitions 对象。 在这里用户可以自己定义他们的切换特效。

$.fn.cycle.transitions = {
 
    // ...
 
};

保持私有函数私有

对外提供一部分你的插件的公共接口确实很强大,但是你要想清楚哪些部分需要提供给公共接口,哪些不需要。当函数一旦暴露在外,任何对参数和语义(函数的功能)的更改都会摧毁向后兼容性。一般来说,如果你不确定一个方法是否应该设为公有,那么也许答案是不。

所以我们如何在不搞乱命名空间并且保持私有的情况下定义更多的函数呢?这就是闭包的事情了。

为了展示如何解决这个问题,我们在我们的插件里加了一个函数 debug。这个函数在 console 中输出选定的对象。我们将整个插件用一个函数包裹起来(上一篇文章也有提到)。

// Create closure.
(function( $ ) {
 
    // Plugin definition.
    $.fn.hilight = function( options ) {
        debug( this );
        // ...
    };
 
    // Private function for debugging.
    function debug( obj ) {
        if ( window.console && window.console.log ) {
            window.console.log( "hilight selection count: " + obj.length );
        }
    };
 
    // ...
 
// End of closure.
 
})( jQuery );

Bob & Sue(实例)

Bob 已经创建了一个新的画廊插件(叫 "superGallery")。这个插件使得一系列图片变得可导航的。Bob 同样还添加了一些动画效果使得插件变得更加有趣。他想让他的插件获得最大程度的可定制性,所以他写出了下面的代码:

jQuery.fn.superGallery = function( options ) {
 
    // Bob's default settings:
    var defaults = {
        textColor: "#000",
        backgroundColor: "#fff",
        fontSize: "1em",
        delay: "quite long",
        getTextFromTitle: true,
        getTextFromRel: false,
        getTextFromAlt: false,
        animateWidth: true,
        animateOpacity: true,
        animateHeight: true,
        animationDuration: 500,
        clickImgToGoToNext: true,
        clickImgToGoToLast: false,
        nextButtonText: "next",
        previousButtonText: "previous",
        nextButtonTextColor: "red",
        previousButtonTextColor: "red"
    };
 
    var settings = $.extend( {}, defaults, options );
 
    return this.each(function() {
        // Plugin code would go here...
    });
 
};

你想到的第一件事情可能就是,这个插件该有多大才能实现这么多可定制功能。这个插件的体积可能完全没必要这么大。

Bob 对他的插件非常满意。他觉得他的插件会在不同的情景之下会是一个通用的解决方案。

Sue 决定去用一用这个新插件。她设置了所有的选项,但是她意识到,如果图片的宽度以低速变化的话,会获得更加好的效果。她赶紧去搜索了 Bob 的文档,但是并没有找到 animateWidthDuration 类似的选项。

你看到问题了吗?

问题的关键是并不是你的插件有多少个选项,而是有什么选项。

Bob 做的有点儿过了。他提供的定制性看起来很高,其实非常低。特别是考虑到一个人可能想要在这个插件中控制的所有特性。Bob 犯了个错误,它提供了非常多荒诞的选项,殊不知让他的插件变得更难定制了。

一个更好的模型

所以现在非常明显了,Bob 需要一个新的定制模型。一个并没有放弃必须的控制和抽象细节的定制模型。

这里有一些建议,可以让你更好的创建一个可定制化插件。

不要创建自己的语法

使用你的插件的开发者不应该要去学一门新的语言或新的技术,只要搞定他们的工作。

Bob 他为 delay 这个选项提供了最大的定制性。他设置了4个不同的延迟:

var delayDuration = 0;
 
switch ( settings.delay ) {
 
    case "very short":
        delayDuration = 100;
        break;
 
    case "quite short":
        delayDuration = 200;
        break;
 
    case "quite long":
        delayDuration = 300;
        break;
 
    case "very long":
        delayDuration = 400;
        break;
 
    default:
        delayDuration = 200;
 
}

这导致不仅仅用户所能控制的延迟水平变少了,还花费了比较多的空间。20行代码仅仅就为了定义一个延迟时间,有点儿多了。

一个更加好的方式就是让用户自己传入延迟的时间。

元素的完全控制权

如果你的插件在DOM中创建了一些元素。那么你最好让用户有方法去控制它。有些时候这意味着提供给用户传入ID或者类名的方法。但是注意你的插件不应该全局依赖这些。

一个不好的实现:

// Plugin code
$( "<div class='gallery-wrapper' />" ).appendTo( "body" );

一个好一点儿的实现:

// Retain an internal reference:
var wrapper = $( "<div />" )
    .attr( settings.wrapperAttrs )
    .appendTo( settings.container );
 
// Easy to reference later...
wrapper.append( "..." );

注意我们使用了 .attr() 来增加特定的属性。所以我们的设置应该像这样:

var defaults = {
    wrapperAttrs : {
        class: "gallery-wrapper"
    },
    // ... rest of settings ...
};
 
// We can use the extend method to merge options/settings as usual:
// But with the added first parameter of TRUE to signify a DEEP COPY:
var settings = $.extend( true, {}, defaults, options );

对于CSS 也可以使用同样的方法来实现:

var defaults = {
    wrapperCSS: {},
    // ... rest of settings ...
};
 
// Later on in the plugin where we define the wrapper:
var wrapper = $( "<div />" )
    .attr( settings.wrapperAttrs )
    .css( settings.wrapperCSS ) // ** Set CSS!
    .appendTo( settings.container );

提供回调函数

如果你的插件是事件驱动的话,最好为每个事件提供回调函数。

var defaults = {
 
    // We define an empty anonymous function so that
    // we don't need to check its existence before calling it.
    onImageShow : function() {},
 
    // ... rest of settings ...
 
};
 
// Later on in the plugin:
 
nextButton.on( "click", showNextImage );
 
function showNextImage() {
 
    // Returns reference to the next image node
    var image = getNextImage();
 
    // Stuff to show the image here...
 
    // Here's the callback:
    settings.onImageShow.call( image );
}

记住,这是一个权衡问题

你的插件不可能在所有的情况下都能工作。同样的,当你提供很少的控制的方法的时候,它也有可能没啥用。所以,权衡是非常重要的事情。以下有三点:

  • 灵活性:你的插件要处理多少种情况?

  • 大小:你的插件大小和它的功能相匹配吗?

  • 性能:你的插件无论什么情况都会处理设置选项吗?影响速度吗?值得吗?

阅读 2.5k

推荐阅读
Jasonの前端地界
用户专栏

作者在前端学习路上的知识总结。

43 人关注
14 篇文章
专栏主页