1. 程式人生 > 實用技巧 >《JavaScript 模式》讀書筆記(8)— DOM和瀏覽器模式2

《JavaScript 模式》讀書筆記(8)— DOM和瀏覽器模式2

四、長期執行指令碼

  可能會注意到有時候瀏覽器會提示某個指令碼已經運行了很長時間,是否應該停止該指令碼。實際上無論要處理多麼複雜的任務,都不希望應用程式發生上述事情。而且,如果該指令碼的工作十分繁重,那麼瀏覽器的UI將會無法響應使用者的任何操作。這將給使用者帶來十分不好的體驗,應該儘量避免。

  在JavaScript中沒有執行緒,但是可以在瀏覽器中使用setTimeout()來模擬執行緒,在最新版本的瀏覽器中可以使用Web Workers。

setTimeout()

  這樣做的一個思想是將一個大任務分解為多個小任務,併為每一個小任務設定超時時間為1毫秒。通過為每一個小任務設定超時為1毫秒,會導致完成整個任務需要耗費更長的時間,但是通過這樣做,可以使得使用者介面保持響應,使用者體驗更好。

  注意:超時時間設定為1毫秒(或者設定為0毫秒)實際上是與瀏覽器和作業系統相關的。將超時事件設定為0並不意味著沒有超時,而是指儘可能快的處理。例如在IE中,最快的時鐘週期是15毫秒。

Web Workers

  最近的瀏覽器為長期執行的指令碼提供了另外一個解決方案:Web Workers。Web Workers為瀏覽器提供了背景執行緒支援。可以將任務比較繁重的計算放在單獨一個檔案中,例如my_web_workers.js。從主程式(網頁)中呼叫該檔案,如下所示:

var ww = new Worker('my_web_worker.js');
    ww.onmessage = function
(event) { document.body.innerHTML += "<p>message from the background thread" + event.data + "</p>"; };

  下面示例中Web Workers做了1e8(100000000)次簡單算術操作:

var end = 1e8,tmp = 1;

    postMessage('hello there');

    while (end) {
        end -= 1;
        tmp += end;
        if(end = 5e7) {
            postMessage(
'halfway there, tmp is now' + tmp); } } postMessage('all done');

  Web Workers使用postMessage()來與呼叫者通訊,並且呼叫者訂閱onmessage事件來接收更新。onmessage回撥函式接收事件物件作為引數,並且該物件包含data屬性。類似地,呼叫者可以使用ww.postMessage()將資料傳遞給Web Workers,Web Worker會使用onmessage回撥函式來訂閱這些訊息。

五、遠端指令碼

  當今Web應用程式經常食用遠端指令碼本來在無需重新載入當前頁面時與伺服器通訊。該方法可以獲取更多響應,並使得類似桌面的網頁應用程式成為可能。現在我們討論一些使用JavaScript與遠端伺服器通訊的方法。

  

XMLHttpRequest

  當今HMLHttpRequest是一個在大多數瀏覽器中都支援的特殊物件,該物件可以讓您採用JavaScript建立HTTP請求。建立一個HTTP請求分為如下三個步驟:

  1. 建立一個XMLHttpRequest物件(簡寫為XHR)。
  2. 提供一個回撥函式來告知請求物件改變狀態。
  3. 傳送請求。

  第一步十分簡單:

var xhr =  new XMLHttpRequest();

  但是在IE瀏覽器在7.0之前的版本中,XHR功能性是以ActiveX物件的方式實現的,因此對於那些版本需要做一些特殊處理。

  第二步是為readystatechange事件提供一個回撥函式。

xhr.onreadystatechange = handleResponse;

  最後一步是使用open()和send()兩個方法來啟動該請求。先使用open()方法指定HTTP請求方法(例如是GET和POST)和URL。然後使用send()方法傳遞POST的資料或者僅僅一個空白字串(在GET模式下)。open()方法的最後一個引數指定該請求是否是非同步的。非同步模式意味著瀏覽器將不會停下來以等待迴應。這當然會給使用者更佳的使用者體驗,因此除非在有特點的理由以外,其他情況都應該將非同步引數設定為true:

xhr.open("GET", "page.html", true); 
xhr.send();

  下面是一個完整的範例,展示了獲取網頁內容,並採用新的內容更新當前網頁的過程。(演示檔案在這裡http://www.jspatterns.com/book/8/xhr.html)。

var i, xhr, activeXids = [
    'MSXML2.XMLHTTP.3.0',
    'MSXML2.XMLHTTP',
    'Microsoft.XMLHTTP'
];

if (typeof XMLHttpRequest === "function") { // native XHR
    xhr =  new XMLHttpRequest();        
} else { // IE before 7
    for (i = 0; i < activeXids.length; i += 1) {
        try {
            xhr = new ActiveXObject(activeXids[i]);
            break;
        } catch (e) {}
    }
}

xhr.onreadystatechange = function () {
    if (xhr.readyState !== 4) {
        return false;
    }
    if (xhr.status !== 200) {
        alert("Error, status code: " + xhr.status);
        return false;
    }
    document.body.innerHTML += "<pre>" + xhr.responseText + "<\/pre>";
};

xhr.open("GET", "page.html", true); 
xhr.send("");

  下面是對該範例的一些註釋:

  • 對於IE來說,在IE6.0及之前的版本中新建XHR物件的過程有一些複雜。範例中依次通過一個ActiveX識別符號列表(從最新版本到更早期版本)來嘗試建立新物件來確定IE的版本,並將這部分操作封裝在try-catch塊中。
  • 回撥函式檢查xhr物件的readyState屬性。該屬性取值範圍從0~4,共5個可能的屬性值,其中屬性值為4意味著“完成”。如果xhr物件的狀態不是完整狀態,那麼繼續等待下一個readystatechange事件。
  • 回撥函式也會檢查xhr物件的status屬性。該屬性對應於HTTP的狀態碼,例如200就對應於OK,而404對應於Not found。這裡只關心狀態碼為200的情況,而將其他狀態碼都按照錯誤處理(這是為了簡便起見,否則就需要檢查其他的有效狀態)。
  • 以上列出來的程式碼將會在每次建立請求的時候,就檢查瀏覽器支援的方法來建立XHR物件。由於已經在之前的章節學習了一些模式(例如初始化分支模式),可以重寫該段程式碼,以使得只需要檢查一次瀏覽器可以支援的方法。

JSONP

  JSONP(有填充的JSON)是另外一種建立遠端請求的方法。和XHR有所不同,它不受同源策略的限制,出於從第三方網站載入資料的安全性考慮,需要小心使用。

  對應於XHR請求,JSONP的請求可以是任意型別的文件:

  • XML文件(過去常用的)。
  • HTML塊(現在常見的)。
  • JSON資料(輕量級, 並且方便)。
  • 簡單文字檔案或者其他文件。

  對於JSONP,最常見的使用函式呼叫封裝的JSON,函式名由請求來提供。

  JSONP請求的URL通常格式如下所示:

http://example.org/getdata.php?callback=myHandler

  getdata.php可以是任意型別的網頁,callback引數指定採用哪個JavaScript函式來處理該請求。

  然後像下面這樣將URL載入到動態的<script>元素:

var script = document.createElement("script");
script.src = url;
document.body.appendChild(script);

  伺服器響應一些JSONP資料,這些資料作為回撥函式的引數。最終的結果是在網頁中包含了一個新的指令碼,該指令碼碰巧是一個函式呼叫,例如:

myHandler("hello": "world");

JSONP範例“字棋遊戲(Tic-tac-toe)

  下面展示一個使用JSONP的範例,一個字棋遊戲,這裡玩家即是客戶端(瀏覽器),也是伺服器。客戶端和伺服器都會生成一個1~9的隨機數,並使用JSONP來獲取伺服器的值。可以在http://www.jspatterns.com/book/8/ttt.html這個網址檢視原始碼。

  這裡有兩個按鈕:一個新建遊戲;另外一個按鈕切換為伺服器方(在一定超時後,會自動切換為客戶方):

<button id="new">New game</button>
<button id="server">Server play</button>

  面板中包含9個小格子,分別對應於不同id屬性:

<table>
    <tr>
        <td id="cell-1">&nbsp;</td>
        <td id="cell-2">&nbsp;</td>
        <td id="cell-3">&nbsp;</td>
    </tr>
    <tr>
        <td id="cell-4">&nbsp;</td>
        <td id="cell-5">&nbsp;</td>
        <td id="cell-6">&nbsp;</td>            
    </tr>
    <tr>
        <td id="cell-7">&nbsp;</td>
        <td id="cell-8">&nbsp;</td>
        <td id="cell-9">&nbsp;</td>
    </tr>
</table>

  完整遊戲程式碼在ttt全域性物件中實現:

var ttt = {
    // cells played so far
    played: [], 
    
    // shorthand
    get: function (id) { 
        return document.getElementById(id);
    },
    
    // handle clicks
    setup: function () {
        this.get('new').onclick = this.newGame;
        this.get('server').onclick = this.remoteRequest;
    },
    
    // clean the board
    newGame: function () {
        var tds = document.getElementsByTagName("td"),
            max = tds.length,
            i;
        for (i = 0; i < max; i += 1) {
            tds[i].innerHTML = "&nbsp;";
        }
        ttt.played = [];        
    },
    
    // make a request
    remoteRequest: function () {
        var script = document.createElement("script");
        script.src = "server.php?callback=ttt.serverPlay&played=" + ttt.played.join(',');
        document.body.appendChild(script);
    },
    
    // callback, server's turn to play
    serverPlay: function (data) {
        if (data.error) {
            alert(data.error);
            return;
        }
        data = parseInt(data, 10);
        this.played.push(data);

        this.get('cell-' + data).innerHTML = '<span class="server">X<\/span>';

        setTimeout(function () {
            ttt.clientPlay();
        }, 300); // as if thinking hard

    },
    
    // client's turn to play
    clientPlay: function () {
        var data = 5;

        if (this.played.length === 9) {
            alert("Game over");
            return;
        }
        
        // keep coming up with random numbers 1-9 
        // until one not taken cell is found
        while (this.get('cell-' + data).innerHTML !== "&nbsp;") {
            data = Math.ceil(Math.random() * 9);
        }
        this.get('cell-' + data).innerHTML = 'O';
        this.played.push(data);
        
    }
    
};

  ttt物件維持一個目前為止已經選擇的空格列表,並將其發給伺服器,因此伺服器可以排除已經選擇的數字來返回一個新數字。如果有錯誤發生,伺服器將會返回類似如下資訊:

ttt.serverPlay({"error":"Error description here"});

  正如您所看到的那樣,JSONP中的回撥函式必須是一個共有的和全域性有效的函式。該回調函式可以不必是一個全域性函式,但是必須是全域性物件的一個方法。如果這裡沒有錯誤的話,伺服器將會返回如下函式呼叫:

ttt.serverPlay(3);

  這裡3意味著伺服器給出的隨機選擇第三個空格。在這種情形下,由於資料十分簡單,甚至不需要使用JSON格式,只需要使用一個數值表示就行。

框架和影象燈塔

  使用框架也是一種處理遠端指令碼的備選方案。可以使用JavaScript建立一個iframe元素,並修改其src屬性的URL。新的URL可以包含更新呼叫者(在iframe之外的父頁面)的資料和函式呼叫。

  使用遠端指令碼最簡單的場景是隻需要向伺服器傳送資料,而無需伺服器迴應的時候。在這種情形下,可以建立一個新影象,並將其src屬性設定為伺服器傷的指令碼檔案,如下所示:

new Image().src = "http://example.org/some/page.php";

  這種模式稱之為影象燈塔(image beacon),這在希望向伺服器傳送日誌資料時是非常有用的。舉例來說,該模式可以用於收集訪問者統計資訊。因為使用者並不需要使用伺服器對這些日誌資料的響應,通常的做法是伺服器用一個1x1畫素的gif圖片作為響應(這是一種不好的模式)。使用“204 Not Content”這樣的HTTP響應是更好的選擇。該HTTP響應的意思是指僅向客戶端傳送HTTP報標頭檔案,而不傳送HTTP內容體。

六、配置JavaScript

  在採用JavaScript時,還有一些效能上需要考慮的因素。這裡將在比較高的層面上討論一下這方面最重要的問題,如需要了解更多的詳細內容,可以查閱資料或其他相關書籍。

合併指令碼檔案

  構建快速載入頁面的第一條規則就是儘可能少的使用外部元件,因為HTTP請求是十分耗費資源的。對於JavaScript來說,可以通過合併外部指令碼檔案來明顯提高頁面載入速度。

  假定網頁使用了jQuery庫,這是一個js檔案。然後需要使用一些jQuery外掛,每個外掛都是一個獨立的檔案。這樣在編寫程式碼前,就擁有了4~5個檔案。將這些檔案合併為一個檔案是十分有意義的,特別是考慮到這些檔案通常都十分小(2~3kb),因而導致HTTP開銷比實際下載檔案的開銷大得多。將這些指令碼檔案合併的方法很簡單,只需要建立一個新檔案,並將這些指令碼檔案的內容複製進去就行。

  當然,應該在編寫程式碼之前合併這些檔案,而不能在開始開發的過程中合併檔案,因為那樣會導致很多除錯的開銷。

  合併檔案的做法也有一些缺點,比如:

  • 儘管在正式開始編寫程式碼前需要增加一個合併指令碼檔案的步驟,但該操作可以採用命令列自動完成,例如在Linux/Unix可以使用cat命令來合併:$ cat jquery.js jquery.quickselect.js jquery.limit.js > all.js
  • 丟失一些快取效益。當對其中某一個指令碼檔案進行修改後,該修改並不會體現到整個合併後的檔案中。這就是為什麼對於大型專案需要有釋出規劃,或者是採用兩個指令碼檔案包:一個包含那些可能會改變的檔案;另外一個包含那些不會發生修改的檔案。
  • 對於檔案包最好是使用版本號或者其他內容來命名。例如使用時間戳:all_201100326.js,也可以使用檔案內容的雜湊值來命名。

  以上這些缺點可以歸納為主要在於不方便,但是使用合併指令碼檔案的方法帶來的收益遠大於帶來的不便性。

精簡和壓縮指令碼檔案

  在第二章中已經涉及了程式碼的精簡。將程式碼精簡作為構建JavaScript指令碼的一部分是十分重要的。

  當從使用者視角考慮時,使用者沒必要下載所有的註釋語句,刪除這些註釋語句對應用程式正常執行沒有影響。

  精簡指令碼檔案大力來的收益依賴於使用的註釋語句和空格的數量,也和具體精簡工具有關。但通常來說,可以精簡大約50%的檔案大小。

  應該經常維護對指令碼檔案的壓縮,這隻需要在伺服器配置中啟用gzip壓縮支援就可以實現,這樣的配置會立即提高速度。如果使用了共享主機的服務,無法獲取足夠的自由來對伺服器進行配置,大部分服務提供商至少會允許您使用Apache的.htaccess配置檔案。因此應該在Web根目錄中,將下列程式碼新增到.htaccess檔案中:

AddOutputFilterByType DEFLATE text/html text/css text/palin text/xml application/javascript application/json

  通常這樣的壓縮配置會減少70%的檔案大小。將精簡和壓縮兩種操作相結合,最後只需要下載的檔案大小僅有未精簡、壓縮之前的檔案的15%

Expires報頭

  與通常人們的想法相反,檔案並不會在瀏覽器快取中儲存太久事件。可以通過使用expires報頭來增加重複訪問時,請求的檔案依然在快取中的概率。

  該操作也僅需要在.htaccess檔案中增加如下程式碼:

ExpiresActive On 
ExpiresByType application/x-javascript "access plus 10 years"

  這樣做的缺點在於如果希望修改檔案,就需要重新命名該檔案。但可能已經為合併後的檔案確定了一個命名約定。

使用CDN

  CDN是內容分發網路(Content Delivery Network)的縮寫。CDN提供付費的主機服務,它允許您將檔案副本放置於全球各個資料中心,以便使用者可以選擇速度最快的伺服器進行連線,而您檔案程式碼中的URL地址不需要修改。

  如果不希望使用付費CDN,也還有一些免費的選擇。

七、載入策略

  乍看之下,如何將指令碼檔案包含到網頁檔案中是一個十分簡單直白的問題。只需要使用<script>元素,要麼直接使用內聯的JavaScript程式碼,要麼在src屬性中使用到單獨檔案的連結,如下所示:

<script>
    console.log('hello world');
</script>
<script src="external.js"></script>

  但是當希望構建高效能網頁應用程式時,需要意識到還有更多的模式可以考慮。

<script>元素的位置

  指令碼元素會阻止下載網頁內容。瀏覽器可以同時下載多個元件,但一旦遇到一個外部指令碼檔案後,瀏覽器會停止進一步下載,直到這個指令碼檔案狹隘、解析並執行完畢。這會嚴重影響網頁載入的總時間,特別是在網頁載入時會發生多次這類事件。

  為了最小化阻止的影響,可以將指令碼元素放置於網頁的最後部分,剛好在</body>標籤之前。在這個位置指令碼檔案不會阻止其他任何檔案塊。網頁元件的其他部分將會被下載並執行。

  在文件抬頭使用單獨檔案是最壞的模式:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="123"></script>
    <script src="456"></script>
    <script src="789"></script>
</head>
<body>
    
</body>
</html>

  將所有檔案合併式更好的做法:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="all.js"></script>
</head>
<body>
    
</body>
</html>

  最好的做法是將合併後的指令碼放於網頁的最後部分:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
<script src="all.js"></script>
</body>
</html>

HTTP塊

  HTTP支援所謂的塊編碼,該技術允許分片傳送網頁。因此如果有一個很複雜的網頁,不需等待伺服器完成所有運算工作,就可以提前將一些靜態頁面報頭先發送給使用者。

  最簡單的策略是將<head>部分內容作為HTTP的第一個塊,而將網頁中其他部分內容作為第二個塊。換句話說,網頁的分塊類似下面的範例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<!-- end of chunk #1 -->
<body>
<script src="all.js"></script>
</body>
</html>
<!-- end of chunk #2 -->

  一個簡單的改進是將第二塊中的JavaScript程式碼移到第一塊的<head>中。這樣做使得瀏覽器可以在伺服器沒有準備好第二塊的時候,就開始下載指令碼檔案:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="all.js"></script>
</head>
<!-- end of chunk #1 -->
<body>
</body>
</html>
<!-- end of chunk #2 -->

  還有一個更好的做法就是在網頁檔案的底部建立一個僅包含指令碼檔案的第三個塊。如果在每個頁面的頂部都有一些靜態報頭,可以將這部分內容放置在第一個塊中:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="header"></div>
    <!-- end of chunk #1 -->
    <!-- full body of the page -->
    <!-- end of chunk #2 -->
    <script src="all.js"></script>
</body>

</html>
<!-- end of chunk #3 -->

  這種方法非常適合漸進增強的思想,並且不會影響到JavaScript程式碼的執行。一旦下載完HTML檔案的第二部分後,就已經擁有一個完全載入、顯示和可用的網頁了,給使用者看到的效果就好像JavaScript已經在瀏覽器中禁用了一樣。等到JavaScript程式碼下載完畢後,它會增強網頁的功能,並增加所有附加功能。

使用動態<script>元素來無阻塞地下載

  如上所述,JavaScript會阻止所有後續檔案的下載,但是有一些模式可以防範這個問題:

  • 使用XHR請求載入指令碼,並使用eval()將其轉換為字串。該方法受到同源策略的限制,並且使用了eval()這種不好的模式。不要使用!
  • 使用的defer和async屬性,但是這種方法並不能在所有的瀏覽器上有效。
  • 使用動態的<script>元素。

  最後一種方法是一種比較好的,可實現的模式。類似於JSONP中所示,需要建立一個新的指令碼元素,設定該元素的src屬性,最後將該元素新增到網頁檔案中。

  下面是一個非同步載入JavaScript檔案的範例,該過程不會阻塞網頁檔案中其他部分的下載:

var script = document.createElement('script');
script.src = 'all_20100426.js';
document.documentElement.firstChild.appendChild(script);

  該模式的缺點在於如果JavaScript指令碼依賴載入主js檔案,那麼採用該模式後不能有其他指令碼元素。主js檔案是非同步載入的,因此無法保證該檔案什麼時候能載入完畢,所以緊跟著主js檔案的指令碼可能要假設所需的物件都還是未定義的。

  為了解決該缺點,可以讓所有內斂的家考本都不要立即執行,而是將這些指令碼都收集起來放在一個數組裡面。然後當主指令碼檔案載入完畢後,就可以執行快取陣列中收集的函數了。為了實現該目的,需要三步:

  首先,建立一個數組來儲存所有的內聯程式碼,這部分程式碼應該放在頁面儘可能前面的位置:

var mynamespce = {
    inline_scripts:[]
};

  然後,需要將所有單獨的內聯指令碼封裝到一個函式中,並將每個函式增加到inline_scripts陣列中,如下所示:

// 過去是
// <script>console.log('I am inline')<\/script>

// 修改為
<script>
mynamespce.inline_scripts.push(function () {
    console.log('I am inline');
}) ;

</script>

  最後,迴圈執行快取中的所有內聯指令碼:

var i ,scripts = mynamespce.inline_scripts,max = scripts.length;

for(i = 0; i < max; max += 1) {
    scripts[i]();
}

 

增加<script>元素

  通常來說,指令碼是防止於文件的<head>區域,但是也可以將指令碼檔案放置於任何元素之內,包含body區域(和JSONP範例中類似)。在之前的範例中,我們使用documentElement來新增<head>,這是因為documentElement是指<html>,而他的第一個自元素就是<head>:

document.documentElement.firstChild.appendChild(script);

  通常也可以這樣寫:

document.getElementsByTagName('head')[0].appendChild(script);

 在能夠掌控標記的時候,這樣寫是沒問題的。但是如果是建立一個小部件或者是一個廣告,無法確定網頁的型別該如何辦呢?從技術上來說,可以在網頁中不使用<head>和<body>,儘管document.body通常能夠在沒有<body>標籤後正常運作:

document.body.appendChild(script);

  但是,實際上有一個標籤一直會在指令碼執行的網頁中存在——<script>標籤。如果沒有<script>標籤(用於內聯或者外聯檔案),那麼裡面的JavaScript程式碼就不會執行。基於上述事實,可以在網頁中使用insertBefore()來在第一個有效的元素之前插入元素:

var first_script = document.getElementsByTagName('script')[0];
first_script.parentNode.insertBefore(script,first_script);

  在這裡,first_script是指令碼元素,開發人員保證該指令碼元素位於網頁中,並且script是建立的最新指令碼。

延遲載入

  關於在頁面載入王成後,載入外部檔案的這種技術稱為延遲載入。通常將一大段程式碼切分成兩部分是十分有益的:

  • 一部分程式碼適用於初始化頁面並將事件處理器附加到UI元素上的。
  • 第二部分程式碼只是在使用者互動或者其他條件下才用得上。

  這樣做的目的是希望漸進式的載入頁面,儘可能快的提供目前需要使用的資訊,而其餘的內容可以在使用者瀏覽該頁面時在後臺載入。

  載入第二部分JavaScript程式碼的方法非常簡單,只需要再一次為head或者body新增動態指令碼元素:

window.onload = function () {
    var script = document.createElement('script');
    script.src = "all_lazy_20102012.js";
    document.documentElement.firstChild.appendChild(script);
}

  對於許多應用程式來說,延遲載入的程式碼部分遠遠大於立即載入的核心部分,因為很多有趣的“操作”(例如拖放操作、XHR和動畫等)只在使用者發出後發生。

按需載入

  之前的模式在頁面載入後,無條件的載入附加的JavaScript指令碼,假定這些程式碼極有可能用得上。但是有沒有辦法可以設法只載入那部分確實需要的程式碼呢?

  想象一下,在網頁上有一個具有多個不同標籤的側邊欄。單擊一次標籤會發出一個XHR請求來獲取內容、更新標籤內容,並且更新過程中標籤顏色還有動畫變化。假設這是意義的一個需要XHR和動畫庫的地方呢?又假設使用者從未點選該標籤呢?

  這時,請使用按需載入模式。可以建立一個require()方法,該方法包含需要按需載入的指令碼的名稱和當附加指令碼載入後需要執行的回撥函式。

  require()函式的用法如下:

require('extra.js',function () {
    functionDefinedInExtraJs();
})

  讓我們來看看該如何實現該函式。很明顯,需要請求附加指令碼,只需要按照動態<script>模式元素模式即可。根據不同的瀏覽器,計算出指令碼家在的事件需要一些小技巧:

function require(file,callback) {
    var script = document.getElementsByTagName('script')[0],
        newjs = document.createElement('script');

        // IE瀏覽器
        newjs.onreadystatechange = function () {
            if(newjs.readyState === 'loaded' || newjs.readyState === 'complete') {
                newjs.onreadystatechange = null;
                callback();
            }
        };

        // 其他
        newjs.onload = function () {
            callback();
        };

        newjs.src = file;
        script.parentNode.insertBefore(newjs,script);
}

  下面是對於上述實現的一些解釋:

  • 在IE中訂閱readystatechange事件,並尋找readyState狀態為“loaded”或“complete”的狀態。其他瀏覽器將會忽略這部分程式碼。
  • 在Firefox、Safari和Opera中,需要通過onload屬性訂閱load事件。
  • 這種方法不適用於Safari 2。如果確實需要支援該版本瀏覽器,請建立一個時間間隔來定期檢查是否指定變數(在附加檔案中定義的變數)已經定義。當該變數被定義後,就意味著新指令碼已經載入並執行了。

  可以建立一個人為的延遲指令碼(用於模擬網路延遲)來測試上述實現,為其命名為ondemand.js.php。該檔案內容如下:

<?php
header('Content-type: application/javascript');
sleep(1);
?>

function extraFunction(logthis){
    console.log('loaded and executed');
    console.log(logthis)
}

  現在測試一下require()函式:

document.getElementById('gogo').onclick = function () {
    require('ondemand.js.php', function () {
        extraFunction('loaded from the parent page');
        document.body.appendChild(document.createTextNode('done!'));
    });
};

  這段程式碼將會在控制檯列印兩條直線,並更新網頁,顯示“done!”。完整的程式碼在http://www.jspatterns.com/book/8/ondemand.html

預載入JavaScript

  在延遲載入模式和按需載入模式中,我們延遲載入當前頁面需要的指令碼。此外,還可以延遲載入當前頁面不需要,但是在後續頁面中可能需要的指令碼。如此,當用戶開啟接下來的網頁後,所需要的指令碼已經預先載入了,今兒使用者感覺速度會快了許多。

  預載入可以使用動態指令碼模式來實現。但是這意味著該指令碼將被解析和執行。解析僅僅會增加預載入的事件,而執行指令碼可能會導致JavaScript錯誤,因為這些指令碼本應該在第二個頁面執行的。例如尋找某個特定的DOM節點。

  預載入JavaScript模式是可以載入指令碼而並不解析和執行這些指令碼的。該方法對css和影象也同樣有效。

  在IE中可以使用熟悉的影象燈塔模式來發出請求:

new Image().src = "preloadme.js";

  在所有其他瀏覽器中可以使用一個<object>來代替指令碼元素,並將其data屬性指向指令碼的URL:

var obj = document.createElement('object');
obj.data = "preloadme.js";
document.body.appendChild(obj);

  為了避免顯示出該物件,可以將該物件的width和height屬性都設定為0。

  可以建立一個通用的preload()方法,並使用初始化分支模式(參考第四章)來處理瀏覽器差異:

var preload;
if(/*@cc_on!@*/false) { //使用條件註釋的IE 嗅探
    preload = function (file){
        new Image().src = file;
    };
} else {
    preload = function (file) {
        var obj = document.createElement('object'),
            body = document.body;
        
        obj.width = 0;
        obj.height = 0;
        obj.data = file;
        body.appendChild(obj);
    }
}

  這樣就可以使用新函數了:

preload('my_web_worker.js');

  這種模式的缺點在於使用了使用者代理嗅探,但是這是無法避免的。因為在這種情形下, 使用特性檢測技術無法告知關於瀏覽器行為的足夠資訊。舉例來說,在這種模式下如果typeof Image是一個函式,那麼理論上可以使用該函式來代替嗅探進行測試。然而在這裡該方法沒有作用,因為所有的瀏覽器都支援new Image();區別僅僅在於有的瀏覽器的影象有獨立的快取,這也就意味著作為影象預載入的元件不會被用作快取中的指令碼,因此下一個頁面會再次下載該影象。

  預載入模式可以用於各種型別元件,而不限於指令碼。舉例來說,這在登入頁面就十分有用。當用戶開始輸入使用者名稱時,可以使用輸入的事件來啟動預載入,因為使用者下一步極有可能進入登入後的頁面。

  注意:瀏覽器嗅探使用的分支註釋是十分有趣的。該方法比在navigator.userAgent中尋找字串要安全一些,因為那些字串很容易被使用者修改。例如:

var isIE = /*@cc_on!@*/false;

  上述語句會在除IE以外的所有瀏覽器中將isIE設定為false(因為這些瀏覽器會忽視註釋語句)。但是在IE中isIE值為true,因為在註釋語句中有一個“!”。因此,在IE中該語句為:

var isIE = !false; //true

小結

  本章主要討論了在特定客戶端瀏覽器環境下的模式:

  • 關注點分離的思想、不引入注目的JavaScript、以及與瀏覽器嗅探相對的特性檢測。
  • DOM指令碼,加速DOM訪問和處理的方式。主要包括批處理DOM操作。
  • 事件,跨瀏覽器事件處理和使用事件授權來減少事件監聽器的數量,以增強效能。
  • 兩種處理長期高運算量指令碼的模式。
  • 多種用於遠端指令碼的模式,這些遠端指令碼實現伺服器和客戶端直接的通訊,主要包括XHR、JSONP、框架和影象燈塔。
  • 在產品環境配置JavaScript。確保將指令碼合併為較少的檔案、精簡併壓縮、將內容放置在CDN中和設定Expires報頭來改善快取。
  • 如何將指令碼合理的放置在網頁中,以改進效能的模式。以及,在載入大指令碼檔案時為了提高命中率,介紹了各種模式,包括延遲載入、預載入和按需載入JavaScript等。