从无到有--手把手搭建一个通用的jQuery插件模板

众所周知,微信自从提出开发小程序后,就成为了一个可以比肩Goggle、Apple的具有平台性质的超级app。同样的,jQuery最成功的地方莫过于:它的可扩展性吸引了众多开发者为其开发插件,从而建立起了一个生态系统。

学会使用jQuery并不难,因为它简单易学,我们在使用jQuery时肯定使用或熟悉了不少其插件。如果我们希望提升自己的编程水平,编写一个属于自己的jQuery插件是个不错的选择。

jQuery插件的开发模式

软件开发过程中是需要一定的设计模式来指导开发的,有了模式,我们就能更好地组织我们的代码,并且从这些前人总结出来的模式中学到很多好的实践。

根据《jQuery高级编程》的描述,jQuery插件开发方式主要有三种:

1.通过$.extend()来扩展jQuery

2.通过$.fn 向jQuery添加新的方法

3.通过$.widget()应用jQuery UI的部件工厂方式创建

第三种方式是用来开发更高级jQuery部件的,该模式开发出来的部件带有很多jQuery内建的特性,比如插件的状态信息自动保存,各种关于插件的常用方法等,本篇文章暂不涉及该开发模式。

再来比较第一种和第二种开发模式:

很显然,第一种开发模式是直接在关键字$、jQuery上挂载相应的方法,使jQuery具有静态方法,也可以理解为使用$.extend()在jQuery的命名空间里直接扩展对应的全局方法。这种方法的声明和使用都比较简单,示例代码如下所示:

$.extend({

    Alert: function(message) {

        alert('app:' + message);

    }

});

$.Alert('hello world!');

效果如下:


$.Alert('hello world!')

上述代码标识:声明一个自定义Alert方法,通过$.Alert('hello world!')调用,弹出"app:hello world"。

这种方式一般适用于一些辅助方法,比如上述重写alert方法,或者在console.log()时加入打印的时间等。

但这种方式无法利用jQuery强大的选择器带来的便利,要处理DOM元素以及将插件更好地运用于所选择的元素身上,还是需要使用第二种开发方式。你所见到或使用的插件也大多是通过此种方式开发。

插件开发

基本方法

声明:

$.fn.myPlugin= function() {

    //to do list

}

在$.fn上挂载一个方法,方法名就是我们的插件名,然后具体逻辑在这个方法内展开。

调用

$('#app a').myPlugin();

例如:做一个插件,将页面上所有的a标签的字体颜色改为'red',那么代码如下:

//html

<div id="app">

        <a href="javascript:void(0);">谷歌</a>

        <a>href="javascript:void(0);">百度</a>

        <a>href="javascript:void(0);">网易</a>

</div>

//javascript(需要引入jQuery)

$.fn.myPlugin = function() {

    //this的解释及注意事项见下文

    this.css('color', 'red');

}

$('#app a').myPlugin();

注意:this代表的是当前调用的对象,上述代码调用myPlugin的是$('#app a'),那么this代表的就是$('#app a'),所以方法体内的this.css('color', 'red')相当于$('#app a').css('color', 'red'),同理,如果使用$('#app')那么this代表的就是$('#app')。那么就有同学问:为什么方法体内部不直接使用$('#app a')呢?

答:书写插件的目的就是为了最大限度的复用代码,我们不仅希望可以将id="app"内的a标签字体颜色全部改为'red',还希望可以将id="store"内的a标签字体颜色全部改为'red',如果我们在方法体内将this改为$('#app a'),那么id="stroe"的方法是不是也要改为$('#store a').css('color', 'red')?这不就违背我们开发jQuery插件的初衷了吗?

那么有人还会问,为什么直接使用this而不是使用$(this)?

答:在插件名字定义的这个函数内部,this指代的是我们在调用该插件时,用jQuery选择器选中的元素,一般是一个jQuery类型的集合。比如$('a')返回的是页面上所有a标签的集合,且这个集合已经是jQuery包装类型了,也就是说,在对其进行操作的时候可以直接调用jQuery的其他方法而不需要再用美元符号来包装一下。

效果如下:

$('#app a').myPlugin();

支持链式调用

在jQuery官方文档中,有以下类似的例子:

$('#d1').css('color', 'red').addClass('classA');

这段代码表示:将id="d1"的元素的字体改为'red',同时添加一个'classA'的类。这是一个典型的"链式调用"的例子。那么jQuery在开发插件时,我们也可以实现链式调用。具体方法如下:

$.fn.myPlugin = function() {

    //this的解释及注意事项见下文

    return this.css('color', 'red');

}

$('#app a').myPlugin().addClass('classA');


效果如下:


链式调用

如果插件内不使用 return this,那么链式调用的时候虽然a标签的字体变成了red,但是在addClass的时候会报错:


非链式调用

接收参数

在使用jQuery系列插件的时候,往往会有一个该插件的官网文档,在这个文档中会介绍相应的参数配置,那么我们接下来学习书写插件时如何传递参数,以使我们插件更加健壮。

现在有一个需求:在调用插件时,可以自定义字体颜色,如果在初始化的时候设定了自定义字体颜色,则设置为该颜色;如果未在初始化时自定义,则默认显示为红色。

通常,我们声明一个函数,在使用函数的参数时是这样的:

function sub(a, b, step) {

    return a + b + (typeof step === 'undefined' ? 0 : step);

}

这段代码表示:在调用sub时,如果参数step存在那么返回结果为a+b+step;反之,则返回a+b。

同样,我们在插件内也是可以这样去声明的:

$.fn.myPlugin(color) {

    return this.css('color', typeof color === 'undefined'  ? 'red' : color);

}

但是这仅仅是一个配置项,如果我们的插件足够大,配置项足够多,那么这种在参数会罗列成color、href、width、 height、 bgImg等等,这样非常不便于维护和升级。

那么为了避免参数越来越长,我们采用一个传递一个对象的方式:

$.fn.myPlugin(opts) {

    this.css('color', typeof opts.color === 'undefined'  ? 'red' : opts.color);

    this.attr('href',  typeof opts.href=== 'undefined'  ? '#' : opts.href );

    return this;

}

这样看起来好多了,看似解决了我们参数过多的问题。

但是,这样写有一个弊端:每逢用到一个参数我们都要通过一个三目运算(当然if else也可以)来判断这个参数是不是在传了进来,这样代码显得臃肿不堪。好在jQuery提供了一个$.extend()的方法(不清楚这个方法是干什么用的的同学请出门左拐,百度一下),将插件内的默认项defaults与自定义参数opts进行合并。

$.fn.myPlugin = function(opts) {

    var defaults = {

        color: 'red',

        href: '#'

    };

    var settings = $.extend(defaults, opts);

    this.css('color', settings.color);

    this.attr('href', settings.href);

    return this;

}

$('#app a').myPlugin({ color: 'green' }).addClass('classA');

效果如下:


jQuery-plugin

我们发现:字体颜色被换成"green",href属性仍然是使用默认值"#"。

到此,插件可以接收和处理参数后,就可以编写出更健壮而灵活的插件了。若要编写一个复杂的插件,代码量会很大,如何组织代码就成了一个需要面临的问题,没有一个好的方式来组织这些代码,整体感觉会杂乱无章,同时也不好维护,所以将插件的所有方法属性包装到一个对象上,用面向对象的思维来进行开发,无疑会使工作轻松很多。

面向对象的插件开发

为什么要通过面向对象进行jQuery插件开发?核心原因还是为了便于维护。

例如:我们在一个项目中,会开发不止一个jQuery插件,那么如果采用$.fn.XXX的方法,就会出现N多个挂载方法,很容易出现命名冲突的情况。另外一个原因是,如果我们的所有属性、方法都放在$.fn.XXX这个作用域里,那么我们这个插件会随着功能的增加变得越来越臃肿,属性、方法之间的耦合度会越来越大,那么后期的维护工作量之大可想而知;如若把属性、方法放在插件作用域之外,那简直就是一个灾难----如何把控全局变量名和方法名?

面向对象的jQuery插件开发思想简单来讲,就是在函数的作用域里将属性、方法私有化,与外界进行"隔离",使函数内部的属性、方法在"自己的圈子里"专注于"干自己的事情"。

以下代码,我们通过一个具体的业务进行开发navsilde,插件实现目的:

(1)配置导航data,插件自动渲染html结构。

(2)鼠标放在某个导航选项上,下方的activeLine(一条蓝色的线条)会向左或向右地滑动到该选项下,鼠标离开后会回复到active选项上。

(3)点击某个选项,那么会将当前选项设置为active选项。

(4)插件提供一个方法,用来获取配置的data。

下面我们由浅入深的讲解面向对象的开发方法,最终实现这个插件。

首先,我们新建一个函数,命名为Navslide,将Navslide绑定到$.fn上,代码如下:

var  Navslide = function(element, options) {

    this.default = {

        data: [],

        active: null

    };

    this.element =  element;

    this.settings = $.extend({}, this.default,  options);

    this.init();

}

//插件入口函数

Navslide.prototype.init = function() {

    this.$nav = this.initNav();

}

//根据data渲染html

Navslide.prototype. initNav = function() {   

    var html = [],

        datas = this.settings.data,

        len = datas.length;

    html.push('< ul class="nav-slide-wrapper">');

    for (var i = 0; i < len; i++) {

        html.push(

            ' <li class="nav-slide-item',

                 datas[i].isActive ? ' active':'',

           '">',

                datas[i].text, '

           '</li>'

        );

    }

    html.push('<i class="active-line"></i>');

    html.push('</ul>');

    return $(html.join('')).appendTo(this.element);

}

//在插件中调用对象

$.fn.navslide = function(options) {

    return new  Navslide(this, options );

}

//调用

$('#app'). navslide({

    data: [

         {'text': '新闻', 'value': 'xinwen', 'isActive': true},

         {'text': '社会趣闻', 'value': 'shehuiquwen'},

         {'text': '视频', 'value': 'shipin'},

         {'text': '新农村', 'value': 'xinongcun'},

    ]

});

css代码在本文省略,如有需要,请根据文末提供的Github地址下载完整代码。

效果如下:


navslide

通过上面这样一改造,我们的代码变得更面向对象了,也更好维护和理解,以后要加新功能新方法,只需向对象添加新变量及方法即可,然后在插件里实例化后即可调用新添加的东西。

命名空间

不仅仅是jQuery插件的开发,我们在写任何JS代码时都应该注意的一点是不要污染全局命名空间。因为随着你代码的增多,如果有意无意在全局范围内定义一些变量的话,最后很难维护,也容易跟别人写的代码有冲突。

比如你在代码中向全局window对象添加了一个变量status用于存放状态,同时页面中引用了另一个别人写的库,也向全局添加了这样一个同名变量,最后的结果肯定不是你想要的。所以不到万不得已,一般我们不会将变量定义成全局的。

一个好的做法是始终用自调用匿名函数包裹你的代码,这样就可以完全放心,安全地将它用于任何地方了,绝对没有冲突。还有一个好处就是,自调用匿名函数里面的代码会在第一时间执行,页面准备好过后,上面的代码就将插件准备好了,以方便在后面的代码中使用插件。

;   //这里多个分号?看下边的讲解

(function() {

    var Navslide = function() {...}

    ...

    //这里的属性名、方法名不会污染全局

})();

另外需要注意一个问题,在书写javascript时,并不强制需要分号结尾,我们在使用自调用匿名函数时要注意格式问题。所以为了避免其他人的不规范代码,我们在开发插件时,第一行首先使用一个分号强制结束其他语句,这样我们的插件在初始化时就不会报错了!

命名空间传参

细心的同学在阅读一些优秀的jQuery插件时,往往会看到这样的格式:

;

(function($, window, document, undefined) {

    //code here

}(jQuery, window, document));

自调用匿名函数传了jQuery、window、documnet三个参数,这是将系统变量传入到函数内部,我们在内部调用的时候可以使用别名。

这里其实是有一个小知识点:很多有同学认为$代表的就是jQuery,其实不然,$只是jQuery的一个简写,我们不仅可以把jQuery.js起一个别名叫$,同样可以将Zepto.js起一个别用叫$。那么问题来了:当我们不把jQuery传入到自定义匿名函数中,而且我们项目组同时用到了jQuery.js和Zepto.js时,函数内部的$代表的是jQuery还是Zepto呢?当然了,如果不怕麻烦,我们不需要传入jQuery,将函数内部使用到$的地方全部改为jQuery(selector)也是可以的,但是不提倡这样做。

而至于这个undefined,比较有意思,为了得到没有被修改的undefined,我们并没有传递这个参数,但却在接收时接收了它,因为实际并没有传,所以"undefined"那个位置接收到的就是真实的"undefined"了。有一点hack的感觉,也是从其他大牛总结到的经验。

最终模板


;

(function($, window, document, undefined) {

       var pluginName = 'navslide';

       var Navslide = function(element, option) {

            //初始化参数

            ...

            //调用入口函数

            this.init();

       }

      //入口方法

     Navslide.prototype.init = function() {...}

     //业务方法

     Navslide.prototype.XXX = function() {...}

    //绑定函数

    $.fn[pluginName] = function(options) {

        return new  Navslide(this, options);

    }

}(jQuery, window, document));

至此,我们搭建了一个通用的jQuery插件模板。由于篇幅有限,这个模板只能算是比较基础的,在我们的实际业务中,还要进行插件自定义方法、多语言、多主题、插件发布等设置,我会在之后的文章继续推出。

滑动导航条完整示例代码:https://github.com/zhiyuanMain/jQuery-plugins/tree/master/how-to-init

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容