共用方式為


jQuery 外掛開發風格及其重要性

"Cowboy" Ben Alman | 2010 年 5 月 14 日

 

多數外掛作者是 Web 設計師或開發人員,他們開發了一些很棒的功能,想要分享給別人。可惜的是,許多外掛作者沒有撥出時間來檢視一下其他人的程式,看看究竟哪些運作良好,又有哪些其實不然。

外掛寫作的抉擇,最終還是由你決定,希望你能從這些建議中找到有助於你養成開發和建立外掛的風格,而這些建議都來自於實際的程式碼,而且是從一些流行的 jQuery 外掛中取用。

這篇文章劃分為下面幾個章節:

  1. 一些 "專業密技"
  2. 對別人友善
  3. 開發風格的元素
  4. 結論

1. 一些 "專業密技"

當你開始開發外掛時,你的決定不只影響程式執行效能,同時也決定了程式是否容易維護。記住,執行效能較佳的程式碼,會使網站或應用程式整體效能提升,而從另一個角度思考,撰寫出可讀性較好的程式碼,能確保日後維護或更新外掛時,不會以挫折感作收。

要能從上述兩者間,找出一個平衡點,似乎並不簡單,但只要你持續關注你的選擇,就能找出一個令人安心的中間地帶。

DRY = 不要自我重複

可惜的是,開發人員經常要硬碰硬才能學到這點。基本上,在花了夠多的時間和努力,去維護結構不良的程式碼之後,漸漸地,你就會知道不要自我重複是最簡單的方式。所以為了節省你日後的頭痛時間,我現在就來介紹它。

如果你不只在一處使用了相同的複雜表達式,那就該把它的值指派給變數,然後使用這個變數。假如你不止在一處使用相同的程式區塊,那就應該把它改成函式,在需要時透過參數傳遞來使用它。外掛中相同或相似的功能程式碼越少,它就越容易測試及維護。

此外,由於你可能並不知道正在自我重複,直到回頭看程式碼才會發現,因此要確保定期做這些事。回過頭檢視程式碼,DRY 的效果就更好一點。

// 不好:太多重複,完全不符合 DRY 的程式碼。

$('body')
  .bind( 'click', function(e){ console.log( 'click: ', e.target ); })
  .bind( 'dblclick', function(e){ console.log( 'dblclick: ', e.target ); })
  .bind( 'keydown', function(e){ console.log( 'keydown: ', e.target ); })
  .bind( 'keypress', function(e){ console.log( 'keypress: ', e.target ); })
  .bind( 'keyup', function(e){ console.log( 'keyup: ', e.target ); });


// 良好:程式較符合 DRY,已經大有改善,不過第一眼的印象並不會有
// 明顯的感覺。

function myBind( name ) {
  $('body').bind( name, function(e){ console.log( name + ': ', e.target ); })
};

myBind( 'click' );
myBind( 'dblclick' );
myBind( 'keydown' );
myBind( 'keypress' );
myBind( 'keyup' );


// 更好:事件處理器使用了 event.type 屬性,
// 而它第一眼看上去的差異就很明顯。

function myHandler( e ) {
  console.log( e.type + ': ', e.target );
};

$('body')
  .bind( 'click', myHandler )
  .bind( 'dblclick', myHandler )
  .bind( 'keydown', myHandler )
  .bind( 'keypress', myHandler )
  .bind( 'keyup', myHandler );


// 最佳:真正理解 jQuery API 如何減少程式碼的複雜性
// 並且讓它的可讀性更高。

$('body').bind( 'click dblclick keydown keypress keyup', function(e){
  console.log( e.type + ': ', e.target );
});

使用 jQuery API

如同你看到上面的例子,沒有什們好的方案可以知道內建 jQuery 方法的最佳用法。只能盡其可能熟讀 API 文件和範例,檢視 jQuery 和其他外掛原始碼。

當你熟知、熟練 jQuery 的 API,你的程式碼就會越簡潔,越具可讀性。此外,利用內建 jQuery 方法,經常能消除一定比例需要自己寫的程式碼,相對來說,需要維護的程式碼就減少了。

避免過早最佳化

最佳化相當重要的,但它比不上讓你的程式碼正常運作那麼重要,報告完畢。最佳化的最大問題在於優化之後,程式碼的可讀性和理解性通常都會變差。

最佳化的第一條規則是只優化那些需要被優化的。所如一切運行正常,沒有效能或檔案大小的問題,也許你並不需要重構程式,讓它快一點或小一點,但卻造成程式碼難以理解,也就是不易維護。多花點時間在寫可以正常運作的程式即可。

這裡有些程式碼,是我過去積極地為了檔案大小最佳化,最後雖然縮到很小,但它可說幾乎無法理解。它的維護性如何?並不好。不管怎樣,在程式完全正常運作前,我不會去考慮最佳化。

最佳化的第二條規則是「別太早最佳化」。如果你的 API 還在發展中,不要把事情弄得更複雜,不要把原本容易讀但效率稍差的邏輯,改寫成連你自己也許都不確定原本的用意是什麼。將這工作留到最後,在寫完你的單元測試之後。然後你就可以藉由重構程式碼來反覆檢查。

過猶不及的避免過度最佳化

使用上面提供「不要過早最佳化」作為藉口來寫出壞的程式碼是不可接受的,因此我們需要討論一下。你的程式碼應該要保持可讀性,但不是因此就取消最佳化。

舉例來說,暫存 jQuery 物件和串接方法時,你可以看到效能顯著地改善。有許多人在討論這個話題,而且你也應該意識到有效能反模式的存在,我會簡單地說明這個例子,然後留給你自己做更多效能最佳實踐的研究。

// 不良:非常慢,而且不符 DRY。

$('#foo').appendTo( 'body' );
$('#foo').addClass( 'test' );
$('#foo').show();

// 良好: jQuery 物件參照被暫存在元素中

var elem = $('#foo')

elem.appendTo( 'body' );
elem.addClass( 'test' );
elem.show();

// 更好: jQuery 方法串接起來。

$('#foo')
  .appendTo( 'body' )
  .addClass( 'test' )
  .show();

// 你甚至可以暫存串接,在許多情況中
// 它會特別有用。

var elem = $('#foo').appendTo( 'body' );

if ( some_condition ) {
  elem.addClass( 'test' );
} else {
  elem.show();
}

2. 對別人友善

如果你希望人們使用你的外掛,不僅要提供他們想要的功能,還要和他們正在使用的程式完美地共存。假如他們嘗試使用你的外掛,卻把其他的程式弄亂,他們必然會停止使用你的外掛。

人們希望使用規規矩矩的外掛,如果你的不規矩,他們就會找另外一個代替。

不要修改不屬於你的物件

我不會在這個議題上著墨太深,因為 Nicholas Zakas 已有寫過一篇文章,所以讀他的這篇「not modifying objects you don't own」,文章裡頭說「假如他們嘗試使用你的外掛,卻把其他的程式弄亂,他們再也不會使用你的外掛。」

宣告你的變數

永遠在使用之前先宣告你的變數,並且使用 var 這個關鍵字。沒有將變數宣告成區域變數,就像你想的,它們就會變成全域變數。這個「隱性全域」有可能和其他的程式發生衝突,而這是不好的,看一下上面那點。

此外,當你要維護的程式碼包含隱性全域時,它通常很難知道在哪些地方還會被使用,造成日後維護工作更困難也更花時間。

使用 closure

最終而言,將你的程式碼放進一個 closure(也就是一個函式中),你的程式外掛就能有私有的屬性,而你的方法就不會闖入全域的名稱空間。此外,藉由匿名函式並立即執行它,這個函式也不會因為留存參照而干擾全域名稱空間。

利用 (...)(); 來包住你的 closure 函式,然後立即執行它,你可以傳遞任何你想要的變數。在下面的範例中,第一層的外掛函式會立即執行,傳遞一個 jQuery 的參考,在內部將會使用 $. 來代替 jQuery. 這樣做的好處是,即使 jQuery 在 noConflict mode中執行,你仍然可以在程式中使用 $,讓你的程式碼保持可讀性。

// 最基本的 jQuery 外掛模式。
(function($){
  
  var myPrivateProperty = 1;
  
  // 呼叫這個公用方法像是 $.myMethod();
  $.myMethod = function(){
    // 在此放置非特定元素使用的 jQuery 方法。
  };
  
  // 呼叫公用方法像是 $(elem).myMethod();
  $.fn.myMethod = function(){
    return this.each(function(){
      // 可串接「jQuery 物件」方法的程式碼寫在這裡。
    });
  };
  
  function myPrivateMethod(){
    // 更多的程式。
  };
  
})(jQuery);

綁定事件處理器時使用名稱空間

使用 bindunbind 事件處理器,使用名稱空間或函式參照可以建立簡單可靠的解除綁定,而不用擔心和其他程式碼發生衝突。

// 不良:這個方法會解除綁定所有「body」的 click 事件處理器!
$('body').bind( 'click', handler ); // 綁定
$('body').unbind( 'click' );        // 綁定

// 良好:只有和你的名稱空間綁定的「body」click 事件處理器
// 名稱空間被解除綁定
$('body').bind( 'click.yourNamespace', handler ); // 綁定
$('body').unbind( 'click.yourNamespace' );        // 綁定

// 也不錯的範例:只有 body 的 click 事件處理器參照到「處理器」函式
// 被解除綁定(注意這個方法需要一個函式參照,
// 它將無法綁定行內的匿名函式)
// function)
$('body').bind( 'click', handler );   // 綁定
$('body').unbind( 'click', handler ); // 綁定

使用唯一的資料名稱

就像使用方法名稱和事件名稱空間,當元素儲存資料時,會使用一個唯一有效的名稱。使用太通用的名稱則會引發衝突。

// 不好:不是一個非常獨特的名稱,也許會和其他的程式的資料發生衝突。
$('#foo').data( 'text', 'hello world' );

// 良好:非常獨特的名稱,和其他程式資料
// 發生衝突的機會很小
$('#foo').data( 'yourPluginName', 'hello world' );

此外,如果你打算在元素資料中儲存很多值,除了使用許多獨立的資料名稱外,可以考慮使用一個獨立的物件,這樣可以很有效率地為你的屬性提供名稱空間。

function set_data() {
  var data = {
    text: 'hello world',
    awesome: false
  };
  
  // 一次儲存所有資料的物件
  $('#foo').data( 'yourPluginName', data );
  
  // 更新 data.xyz 屬性也將更新所有的儲存資料
  data.awesome = true;
  data.super_awesome = true;
};

function get_data() {
  var data = $('#foo').data( 'yourPluginName' );
  alert( data.super_awesome );
}

set_data();
get_data(); // 顯示結果為真

3. 開發風格的元素

這可能會有所爭議,因為開發程式碼的風格相當主觀,沒有絕對的對或錯可言。事實上,一旦其他人開始看你的程式碼,這個說法很快就會被丟出門外,因為他們必須理解你的程式碼,才能追蹤程式錯誤或是加上新功能。

當你在開發程式時,別害怕回頭檢視你正在生產的程式碼。這些程式碼是否經過良好的組織,這樣合理嗎?它具有可讀性嗎?如果現在不是,當然六個月後想加上新功能時,狀況當然也不會更好。

這裡最值得記住是,你需要保持一致的開發風格。你不必然一定要步步按照那些開發風格指南,不過如果你打算按照自己的方式來,至少你要有個好理由為什麼要這樣做。

你也可以看一下 Douglas Crockford 的文章Code Conventions for JavaScript,裡面有許多建議都相當的基本。.

一行的長度

信不信由你,有些人仍在終端機介面撰寫程式,這意謂當一行超過 80 個字元時,呈現的樣子就會很醜陋。是的,這些人可以將終端機視窗設寬一點,不過你也應該先問問自己,有沒有任何一行程式真的需要超過 80 個字元欄寬。假如沒有的話,既然你可以手動將程式斷到另一行,那一定比笨笨的文字編輯器做得更好,那就做吧!

假設行的長度限制讓你覺得有點嚴格,那讓我從另一個稍微不同的情境來談談。你知道所有使用程式範例的網站,可能都有數百行,而且會有水平捲動列,萬一你看不到水平捲動列,有可能是程式範例超過視窗高度?所以,你必須先捲動到範例程式的最下方,才能將程式碼捲到右半部,萬一這時你想讀的程式碼已經被拉到上方去,超出了可見區域,現在你又必須將它捲動回上方?

你也許已經了解我要表達的。水平捲動在文字編輯器和程式範例一樣恐怖。讓你的一行長度保持合理,就能一石二鳥,達到雙贏。

Tab vs. 空白

一個陳年的爭議:tab 或空白,哪一個比較好?它在爭議之處在於 tab 字元對內縮而言更具表意的效果,而許多文字編輯器允許改變 tab 字元的寬度,不過不是每一個都能做到。在使用瀏覽器檢視原始碼的時候,tab 會產生特別的寬度(在文章中的程式碼範例也是),而 tab 出現在程式碼行尾時,會造成行尾註解的一團亂。

從另一個方面講,2 或 4 個空白內縮,提供足夠的內縮空間來區分不同等級的程式巢狀結構,而且為實際的程式碼留下的足夠的空間。有許多人主張 4 個字元內縮,而我個人則建議 2 個字元內縮,以換取最小的水平捲動或包裹。

下例哪一個比較容易閱讀呢?

// Tab (模擬使用 8 字元內縮)
function inArray( elem, array ) {
         if ( array.indexOf ) {
                 return array.indexOf( elem );
         }
         
         for ( var i = 0, length = array.length; i < length; i++ ) {
                 if ( array[ i ] === elem ) {
                         return i;
                 }
         }
         
         return -1;
};

// 2 個空白內縮
function inArray( elem, array ) {
   if ( array.indexOf ) {
     return array.indexOf( elem );
   }
   
   for ( var i = 0, length = array.length; i < length; i++ ) {
     if ( array[ i ] === elem ) {
       return i;
     }
   }
   
   return -1;
};

無論你喜歡的是哪種 tab 設定,最重要的事是一致性的內縮,適當地對齊相似巢狀的程式碼區塊,以維持最大的可讀性。不過,萬一可以有較少的水平捲動,這段程式碼不是比較容易閱讀嗎?

擁擠的參數或程式碼區塊

有時少即是多,不過使用空白,多經常會更多。給與函式參數或程式區塊一點點「喘息空間」,經常可以讓可讀性增加。就像其他的事情一樣,空白可以用得相當極端,導致程式碼閱讀性變差。既然目標是建立更容易維護的程式碼,你需要學習的是如何視情況使用,只要記住這點:不要過分吝於使用空白!

下面哪一個比較容易閱讀?

// 擁擠
function inArray(elem,array) {
   if (array.indexOf) {
     return array.indexOf(elem);
   }
   for (var i=0,length=array.length;i<length;i++) {
     if (array[i]===elem) {
       return i;
     }
   }
   return -1;
};

// 啊.. 多了一點喘息的空間
function inArray( elem, array ) {
   if ( array.indexOf ) {
     return array.indexOf( elem );
   }
   
   for ( var i = 0, length = array.length; i < length; i++ ) {
     if ( array[ i ] === elem ) {
       return i;
     }
   }
   
   return -1;
};

註解

寫有用的註解。你不需要寫一本書,而且你應該避免註解相當明顯的程式碼,只要想想你的目標閱讀者會怎麼想,思考他們也許不熟悉你用來參照的變數或是你運用的特定模式。

閱讀你的程式碼的人,一定是經常在除錯的人,希望能擴展你的外掛,讓它更好用,或者只是想學習你是如何做到的。你應該做任何可以讓你的程式更容易理解 的事。

此外,很多時候,想要除錯或擴展外掛的人是你自己,萬一時間經過夠久,很可能你會不記得當初為什麼會這樣寫。你的註解不止幫助其他人,也會幫助你自己!

大括號

就理論上來說,有些情況可以省略區塊前後的大括號,但它會讓程式碼看起來有些模糊不清,所以不要這樣做。總是使用大括號來避免可能的誤會,你的程式碼就會更具可讀性。

另外,在 JavaScript 中,由於大括號放置的位置會改變程式碼的行為,一定要確保有使用適當的retrun敘述,作法是讓return敘述和大括號維持在同一行。

為了保持一致性(因為一致性是好的)試著將你的大括號放在同一行,讓他們跟著宣告。

// 不好:不過這個例子相對而言還算清晰
if ( a === 1 )
  b = 2;

// 壞例子:這個例子有點含糊不清
if ( a === 1 )
  b = 2;
  c = 3;

// 喔!這是上一個例子實際上做的事!
if ( a === 1 ) {
  b = 2;
}
c = 3;

// 好例子:這個例子完全清楚
if ( a === 1 ) {
  b = 2;
}

// 好例子:這個例子也很清楚
if ( a === 1 ) {
  b = 2;
  c = 3;
}

// 壞例子:看前面提到的連結文章,
// 大括號的位置如何改變程式的行為
function test()
{
  return
  {
    property: true
  };
};

// 好例子:不但函式返回了預期中的物件,
// 而且{}的組成方式有一致性
function test() {
  return {
    property: true
  };
};

4. 結論

說到底,有許多不同的外掛開發風格可以考慮,最重要的是專注於發展出自己所屬的風格,並在其中取得有效率和可讀性之間的平衡點,並具有可維護性。你的外掛不僅要運作良好,而且還需要更新及維護,因為你就是那個維護的人!

此外,確保不是只在自己的程式碼上做各種實驗,也要花點時間去檢視 jQuery 原始碼和其他的外掛。檢視越多「其他」的程式碼,你就有越多工具在手上,可以決定哪些行得通,哪些行不通,這樣最終能幫助你做出更明智的選擇。

About the Author

"Cowboy" Ben Alman has been developing for the open web for over ten years. In addition to developing websites for clients such as American Express, America Online and Cisco and contributing to open source JavaScript projects such as jQuery and Modernizr, he maintains a number of popular jQuery plugins along with a blog that promotes web development and design best practices.

When he's not creating a new plugin (or writing an article on creating plugins), Ben can be found in the official jQuery IRC channel, helping newbies learn how to $('body').append('hello world'). In addition to web development, Ben is an avid photographer and funk bass player, and can be seen taking photos and playing around the greater Boston, MA area.

Find Ben on:

Videos

 

Articles