工作中发现,我的很多时间都在为前端页面交互编写 JavaScript 代码。相比较而言,由于有 MVC 框架的各种约定,后端的 PHP 代码写起来显得比 JavaScript 要优雅很多。虽然也有 jQuery 这样的利器(得益于良好的跨浏览器兼容特性和简洁的使用接口,jQuery 几乎已经成了 JavaScript 的代名词),但我用到的只是它的选择器,以及别人写的一些插件而已。我深知要透彻地理解一门语言,就应该去用它做一个小项目,带着目标学习需用到的东西才能有的放矢。正好遇到一个交互需求找不到已有解决方案,于是摒弃以前那样简单粗暴的写 js 代码,我决定自己实现一个 jQuery 插件来搞定它。

JavaScript 是一门混乱的语言,好的特性和坏的特性混杂在一起。而不同浏览器对标准的解析不一致,使得这门语言更加混乱,在这种情况下遵循最佳实践有诸多好处,至少不会掉入坑里。所以就有了《JavaScript: The Good Parts》这类书专门教最佳实践。可惜读完后再去看别人的 js 代码,会发觉几乎没有谁做得很标准。

一、jQuery 插件的类别

在 jQuery 中要使用一个插件,一般有两种形式:

  • 类级别。例如 $.myPlugin()
  • 对象级别。例如 $('#node').myPlugin()

类级别插件可以理解为拓展 jQuery 类,即给 jQuery 添加新的全局函数,典型的例子就是 $.ajax() 这个函数。jQuery 的全局函数就是属于 jQuery 命名空间的函数。另一种是对象级别的插件,即作用于指定的 jQuery 对象,典型的例子 $('.msg').show()

二、类级别 jQuery 插件的开发

1. 原始写法

一般在项目中我们会引入一个 js 文件,里面存放了所有的 js 代码。

<script type="text/javascript" src="http://www.lovelucy.info/all.js"></script>

只要写一些函数简单地放在文件里,就算是一个模块,直接调用就行了。

   function m1(){
    //...
  }
  function m2(){
    //...
  }

这种方法缺陷很明显,就是污染了全局空间,无法保证不与其他模块发生变量名冲突,而且方法成员之间看不出直接关系。

2. 扩展写法

使用 jQuery.extend(object) 来扩展 jQuery 类本身,可以理解为 jQuery 添加静态方法。

$.extend({
  addMethod : function(a, b){return a + b;}
});
// $.addMethod(1, 2); //return 3

3. 简单写法

给 jQuery 添加一个全局函数,只需如下定义

jQuery.foo = function() {
    alert('test');
    // other code...
};

这样就能直接用了:jQuery.foo() 或者 $.foo()

4. 使用命名空间

虽然上面的 2 种写法相对于原始写法要干净很多,减少了在全局空间冲突的概率,但是在 jQuery 命名空间中,仍然不可避免某些函数或变量名可能和其他 jQuery 插件冲突。因此我们习惯再封装一层,将一些方法封装到另一个自定义的命名空间。

jQuery.myPlugin = {
    foo:function() {
        alert('test');
    },
    bar:function(param) {
        alert('test "' + param + '".');
    }
};
// $.myPlugin.foo();
// $.myPlugin.bar('hello');

采用命名空间的函数仍然是全局函数,使用独立的插件名我们可以避免命名空间内函数的冲突。

三、对象级别 jQuery 插件的开发

大部分 jQuery 插件都是对象级别的,开发一个对象级别插件会遇到闭包这个概念,简单起见先只看闭包的表现形式

(function($){  
    // your codes  
})(jQuery);

1. 扩展写法

给 jQuery 对象添加方法,就是对 jQuery.prototype 进行扩展,为 jQuery 类添加成员方法,需要用到 jQuery.fn.extend(object); 。下面是一个例子:

$.fn.extend({ 
    getInputText:function(){ 
        $(this).click(function(){ 
            alert($(this).val()); 
        }); 
    } 
});
//$("#username").getInputText();

2. 通用写法

对象级别的插件也可以这样定义 $.fn.myPlugin = function(){},同样的,和类级别相比多了一个 fn。

一个通用的对象级别插件框架——

(function($){ 
    $.fn.myPluginName = function(options){ 
        var defaults = {} //各种属性和参数
        var options = $.extend(defaults, options); 
        this.each(function(){ 
            //插件的实现代码
        }); 
    }; 
})(jQuery);

$.extend(defaults, options) 通过合并 defaults 和 options 来扩展默认参数,实现插件接受外部 options 参数的功能。于是我们就可以见到这样的用法:

$('#myDiv').hilight({
    foreground: 'blue'
});

3. 改进的通用写法

上面代码的一种改进是暴露插件的默认设置。这可以让插件的使用者更容易用较少的代码覆盖和修改插件。

(function($){ 
    $.fn.myPluginName = function(options){
        var options = $.extend({}, $.fn.myPluginName.defaults, options);
        this.each(function(){
            //插件的实现代码
        }); 
    };
    // 暴露的默认参数
    $.fn.myPluginName.defaults = {};
})(jQuery);

于是我们就可以见到这样使用的

$.fn.hilight.defaults.foreground = 'blue';
$('#myDiv').hilight();

覆盖默认的配置就只需要调用一次,而不必在每次调用插件时都传递参数。是否需要传递参数,在不同的场景下可以灵活处理,两者的使用可以结合起来。

更高级的插件写法还包括暴露一些函数给使用者,让他们可以覆盖。另一方面,也可以保持私有函数的私有性。具体的代码这里就不多赘述了。学习的最好方式就是阅读别人的插件代码。

四、总结

我之前遇到的需求是要让用户在表单的一个 input 字段中输入 json 格式的文本。但是用户纯手工输入很难写出标准的 json string,借助我写的插件,可以将 input 转换为一个 key-value 的表格,用户就很容易输入了。在表单提交时 key-value 将自动 encode 为 json 回填到原来的字段中进行提交。

插件开源在 GitHub: jQuery Key-Value Json Input Plugin

演示页面

这是我的第一个 jQuery 插件,当然肯定会有更好的设计方案,只是在实现这个插件的过程中,我慢慢对 jQuery 的闭包,链式操作,函数式的设计思想(匿名函数)等有了初步的概念。总结是一个反刍的过程,写完本文似乎理解又加深了一些。

之前我还和同事讨论过,Engineering 就是如此,和 Science 最大的不同就是需要总结、积累。Science 一开始就会给我们灌输概念、公式、定理,世界就是这个样子,然后用这些公式、定理去解释现象,计算结果。而 Engineering 则恰恰相反,是在长期的实践中发现规律和 Best Practice。如果一开始就搬理论,学生往往不知所云,反而是在累积了很多年工作经验以后顿悟:“是的,就是这样,就是这样”,我在大学本科上《软件工程》课时这种感觉尤为强烈。总结是必要的,这也是写 blog 的意义所在。

保持强烈的求知欲,做一个乐于学习和自我提升的人。

参考链接:
Javascript 模块化编程
jQuery 插件实现的方法和原理简单说明