工作中發現,我的很多時間都在為前端頁面交互編寫 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 插件實現的方法和原理簡單說明