<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                # DOM和瀏覽器中的模式 在本書的前面幾章中,我們主要關注了JavaScript核心(ECMAScript),并沒有涉及太多關于在瀏覽器中使用JavaScript的內容。在本章,我們將探索一些在瀏覽器環境中的模式,因為這是最常見的JavaScript程序環境。瀏覽器腳本編程也是大部分不喜歡JavaScript的人對這門語言的認知。這當然是可以理解,因為在瀏覽器中有非常多不一致的宿主對象和DOM實現。很明顯,任何能夠減輕客戶端腳本編程的痛楚的最佳初中都是大有益處的。 在本章中,你會看到一些零散的模式,包括DOM編程、事件處理、遠程腳本、頁面腳本的加載策略以及將JavaScript部署到生產環境的步驟。 但首先,讓我們來簡要討論一下如何做客戶端腳本編程。 ## 分離 在web應用開發中主要關注的有三種東西: - 內容 即HTML文檔 - 表現 指定文檔樣式的CSS - 行為 JavaScript,用來處理用戶交互和頁面的動態變化 盡可能地將這三者分離可以加強應用在各種用戶代理(譯注:user agent,即為用戶讀取頁面并呈現的軟件,一般指瀏覽器)的可到達性(譯注:delivery,指可被用戶代理接受并理解的程度),比如圖形瀏覽器、純文本瀏覽器、用于殘障人士的輔助技術、移動設備等等。分離常常是和漸進增強的思想一起實現的,我們從一個給最簡單的用戶代理的最基礎的體驗(純HTML)開始,當用戶代理的兼容性提升時再添加更多的可以為體驗加分的東西。如果瀏覽器支持CSS,那么用戶會看到文檔更好的呈現。如果瀏覽器支持JavaScript,那文檔會更像一個應用,提供更多的特性來增強用戶體驗。 在實踐中,分離意味者: - 在關掉CSS的情況下測試頁面,看頁面是否仍然可用,內容是否可以呈現和閱讀 - 在關掉JavaScript的情況下測試頁面,確保頁面仍然可以完成它的主要功能,所有的鏈接都可以正常工作(沒有href="#"的鏈接),表單仍然可以正常填寫和提交 - 不要使用內聯的事件處理(如onclick)或者是內聯的style屬性,因為它們不屬于內容層 - 使用語義化的HTML元素,比如頭部和列表等 JavaScript(行為)層的地位不應該很顯赫,也就是說它不應該成為頁面正常工作必須的東西,不應該使得用戶在使用不支持的瀏覽器操作時存在障礙。它只應該被用來增強頁面。 通常比較優雅的用來處理瀏覽器差異的方法是特性檢測。它的思想是你不應該使用瀏覽器類型檢測來決定代碼的邏輯,而是應該檢測在當前環境中你需要使用的某個方法或者是屬性是否存在。瀏覽器檢測一般認為是一種“反模式”(譯注:anitpattern,指不好的模式)。雖然有的情況下不可避免要使用,但它應該是最后考慮的選擇,并且應該只在特性檢測沒有辦法給出明確答案(或者造成明顯性能問題)的時候使用: // antipattern if (navigator.userAgent.indexOf('MSIE') !== ?1) { document.attachEvent('onclick', console.log); } // better if (document.attachEvent) { document.attachEvent('onclick', console.log); } // or even more specific if (typeof document.attachEvent !== "undefined") { document.attachEvent('onclick', console.log); } 分離也有助于開發、維護,減少升級一個現有應用的難度,因為當出現問題的時候,你知道去看哪一塊。當出現一個JavaScript錯誤的時候,你不需要去看HTML或者是CSS就能修復它。 ## DOM編程 操作頁面的DOM樹是在客戶端JavaScript編程中最普遍的動作。這也是導致開發者頭疼的最主要原因(這也導致了JavaScript名聲不好),因為DOM方法在不同的瀏覽器中實現得有很多差異。這也是為什么使用一個抽象了瀏覽器差異的JavaScript庫能顯著提高開發速度的原因。 我們來看一些在訪問和修改DOM樹時推薦的模式,主要考慮點是性能方面。 ### DOM訪問 DOM操作性能不好,這是影響JavaScript性能的最主要原因。性能不好是因為瀏覽器的DOM實現通常是和JavaScript引擎分離的。從瀏覽器的角度來講,這樣做是很有意義的,因為有可能一個JavaScript應用根本不需要DOM,而除了JavaScript之外的其它語言(如IE的VBScript)也可以用來操作頁面中的DOM。 一個原則就是DOM訪問的次數應該被減少到最低,這意味者: - 避免在環境中訪問DOM - 將DOM引用賦給本地變量,然后操作本地變量 - 當可能的時候使用selectors API - 遍歷HTML collections時緩存length(見第2章) 看下面例子中的第二個(better)循環,盡管它看起來更長一些,但卻要快上幾十上百倍(取決于具體瀏覽器): // antipattern for (var i = 0; i < 100; i += 1) { document.getElementById("result").innerHTML += i + ", "; } // better - update a local variable var i, content = ""; for (i = 0; i < 100; i += 1) { content += i + ","; } document.getElementById("result").innerHTML += content; 在下一個代碼片段中,第二個例子(使用了本地變量style)更好,盡管它需要多寫一行代碼,還需要多定義一個變量: // antipattern var padding = document.getElementById("result").style.padding, margin = document.getElementById("result").style.margin; // better var style = document.getElementById("result").style, padding = style.padding, margin = style.margin; 使用selectors API是指使用這個方法: document.querySelector("ul .selected"); document.querySelectorAll("#widget .class"); 這兩個方法接受一個CSS選擇器字符串,返回匹配這個選擇器的DOM列表(譯注:querySelector只返回第一個匹配的DOM)。selectors API在現代瀏覽器(以及IE8+)可用,它總是會比你使用其它DOM方法來做同樣的選擇要快。主流的JavaScript庫的最近版本都已經使用了這個API,所以你有理由去檢查你的項目,確保使用的是最新版本。 給你經常訪問的元素加上一個id屬性也是有好處的,因為document.getElementById(myid)是找到一個DOM元素最容易也是最快的方法。 ### DOM操作 除了訪問DOM元素之外,你可能經常需要改變它們、刪除其中的一些或者是添加新的元素。更新DOM會導致瀏覽器重繪(repaint)屏幕,也經常導致重排(reflow)(重新計算元素的位置),這些操作代價是很高的。 再說一次,通用的原則仍然是盡量少地更新DOM,這意味著我們可以將變化集中到一起,然后在“活動的”(live)文檔樹之外去執行這些變化。 當你需要添加一棵相對較大的子樹的時候,你應該在完成這棵樹的構建之后再放到文檔樹中。為了達到這個目的,你可以使用文檔碎片(document fragment)來包含你的節點。 不要這樣添加節點: // antipattern // appending nodes as they are created var p, t; p = document.createElement('p'); t = document.createTextNode('first paragraph'); p.appendChild(t); document.body.appendChild(p); p = document.createElement('p'); t = document.createTextNode('second paragraph'); p.appendChild(t); document.body.appendChild(p); 一個更好的版本是創建一個文檔碎片,然后“離線地”(譯注:即不在文檔樹中)更新它,當它準備好之后再將它加入文檔樹中。當你將文檔碎片添加到DOM樹中時,碎片的內容將會被添加進去,而不是碎片本身。這個特性非常好用。所以當有好幾個沒有被包裹在同一個父元素的節點時,文檔碎片是一個很好的包裹方式。 下面是使用文檔碎片的例子: var p, t, frag; frag = document.createDocumentFragment(); p = document.createElement('p'); t = document.createTextNode('first paragraph'); p.appendChild(t); frag.appendChild(p); p = document.createElement('p'); t = document.createTextNode('second paragraph'); p.appendChild(t); frag.appendChild(p); document.body.appendChild(frag); 這個例子和前面例子中每段更新一次相比,文檔樹只被更新了一下,只導致一次重排/重繪。 當你添加新的節點到文檔中時,文檔碎片很有用。當你需要更新已有的節點時,你也可以將這些變化集中。你可以將你要修改的子樹的父節點克隆一份,然后對克隆的這份做修改,完成之后再去替換原來的元素。 var oldnode = document.getElementById('result'), clone = oldnode.cloneNode(true); // work with the clone... // when you're done: oldnode.parentNode.replaceChild(clone, oldnode); ## 事件 在瀏覽器腳本編程中,另一塊充滿兼容性問題并且帶來很多不愉快的區域就是瀏覽器事件,比如click,mouseover等等。同樣的,一個JavaScript庫可以解決支持IE(9以下)和W3C標準實現的雙倍工作量。 我們來看一下一些主要的點,因為你在做一些簡單的頁面或者快速開發的時候可能不會使用已有的庫,當然,也有可能你正在寫你自己的庫。 ### 事件處理 麻煩是從給元素綁定事件開始的。假設你有一個按鈕,點擊它的時候增加計數器的值。你可以添加一個內聯的onclick屬性,這在所有的瀏覽器中都能正常工作,但是會違反分離和漸進增強的思想。所以你應該盡力在JavaScript中來做綁定,而不是在標簽中。 假設你有下面的標簽: <button id="clickme">Click me: 0</button> 你可以將一個函數賦給節點的onclick屬性,但你只能這樣做一次: // suboptimal solution var b = document.getElementById('clickme'), count = 0; b.onclick = function () { count += 1; b.innerHTML = "Click me: " + count; }; 如果你希望在按鈕點擊的時候執行好幾個函數,那么在維持松耦合的情況下就不能用這種方法來做綁定。從技術上講,你可以檢測onclick是否已經包含一個函數,如果已經包含,就將它加到你自己的函數中,然后替換onclick的值為你的新函數。但是一個更干凈的解決方案是使用addEventListener()方法。這個方法在IE8及以下版本中不存在,在這些瀏覽器需要使用attachEvent()。 當我們回頭看條件初始化模式(第4章)時,會發現一個示例實現是一個很好的解決跨瀏覽器事件監聽的套件。現在我們不討論細節,只看一下如何給我們的按鈕綁定事件: var b = document.getElementById('clickme'); if (document.addEventListener) { // W3C b.addEventListener('click', myHandler, false); } else if (document.attachEvent) { // IE b.attachEvent('onclick', myHandler); } else { // last resort b.onclick = myHandler; } 現在當按鈕被點擊時,myHandler會被執行。讓我們來讓這個函數實現增加按鈕文字“Click me: 0”中的數字的功能。為了更有趣一點,我們假設有好幾個按鈕,一個myHandler()函數來處理所有的按鈕點擊。如果我們可以從每次點擊的事件對象中獲取節點和節點對應的計數器值,那為每個按鈕保持一個引用和計數器就顯得不高效了。 我們先看一下解決方案,稍后再來做些評論: function myHandler(e) { var src, parts; // get event and source element e = e || window.event; src = e.target || e.srcElement; // actual work: update label parts = src.innerHTML.split(": "); parts[1] = parseInt(parts[1], 10) + 1; src.innerHTML = parts[0] + ": " + parts[1]; // no bubble if (typeof e.stopPropagation === "function") { e.stopPropagation(); } if (typeof e.cancelBubble !== "undefined") { e.cancelBubble = true; } // prevent default action if (typeof e.preventDefault === "function") { e.preventDefault(); } if (typeof e.returnValue !== "undefined") { e.returnValue = false; } } 一個在線的例子可以在<http://jspatterns.com/book/8/click.html>找到。 在這個事件處理函數中,有四個部分: - 首先,我們需要訪問事件對象,它包含事件的一些信息以及觸發這個事件的頁面元素。事件對象會被傳到事件處理回調函數中,但是使用onclick屬性時需要使用全局屬性window.event來獲取。 - 第二部分是真正用于更新文字的部分 - 接下來是阻止事件冒泡。在這個例子中它不是必須的,但通常情況下,如果你不阻止的話,事件會一直冒泡到文檔根元素甚至window對象。同樣的,我們也需要用兩種方法來阻止冒泡:W3C標準方式(stopPropagation())和IE的方式(使用cancelBubble) - 最后,如果需要的話,阻止默認行為。有一些事件(點擊鏈接、提交表單)有默認的行為,但你可以使用preventDefault()(IE是通過設置returnValue的值為false的方式)來阻止這些默認行為。 如你所見,這里涉及到了很多重復性的工作,所以使用第7章討論過的外觀模式創建自己的事件處理套件是很有意義的。 ### 事件委托 事件委托是通過事件冒泡來實現的,它可以減少分散到各個節點上的事件處理函數的數量。如果有10個按鈕在一個div元素中,你可以給div綁定一個事件處理函數,而不是給每個按鈕都綁定一個。 我們來的睦一個實例,三個按鈕放在一個div元素中(圖8-1)。你可以在<http://jspatterns.com/book/8/click-delegate.html>看到這個事件委托的實例。 ![圖8-1 事件委托示例:三個在點擊時增加計數器值的按鈕](./figure/chapter8/8-1.jpg) 圖8-1 事件委托示例:三個在點擊時增加計數器值的按鈕 結構是這樣的: <div id="click-wrap"> <button>Click me: 0</button> <button>Click me too: 0</button> <button>Click me three: 0</button> </div> 你可以給包裹按鈕的div綁定一個事件處理函數,而不是給每個按鈕綁定一個。然后你可以使用和前面的示例中一樣的myHandler()函數,但需要修改一個小地方:你需要將你不感興趣的點擊排除掉。在這個例子中,你只關注按鈕上的點擊,而在同一個div中產生的其它的點擊應該被忽略掉。 myHandler()的改變就是檢查事件來源的nodeName是不是“button”: // ... // get event and source element e = e || window.event; src = e.target || e.srcElement; if (src.nodeName.toLowerCase() !== "button") { return; } // ... 事件委托的壞處是篩選容器中感興趣的事件使得代碼看起來更多了,但好處是性能的提升和更干凈的代碼,這個好處明顯大于壞處,因此這是一種強烈推薦的模式。 主流的JavaScript庫通過提供方便的API的方式使得使用事件委托變得很容易。比如YUI3中有Y.delegate()方法,它允許你指定一個用來匹配包裹容器的CSS選擇器和一個用于匹配你感興趣的節點的CSS選擇器。這很方便,因為如果事件發生在你不關心的元素上時,你的事件處理回調函數不會被調用。在這種情況下,綁定一個事件處理函數很簡單: Y.delegate('click', myHandler, "#click-wrap", "button"); 感謝YUI抽象了瀏覽器的差異,已經處理好了事件的來源,使得回調函數更簡單了: function myHandler(e) { var src = e.currentTarget, parts; parts = src.get('innerHTML').split(": "); parts[1] = parseInt(parts[1], 10) + 1; src.set('innerHTML', parts[0] + ": " + parts[1]); e.halt(); } 你可以在<http://jspatterns.com/book/8/click-y-delegate.html>看到實例。 ## 長時間運行的腳本 你可能注意到過,有時候瀏覽器會提示腳本運行時間過長,詢問用戶是否要停止執行。這種情況你當然不希望發生在自己的應用中,不管它有多復雜。 同時,如果腳本運行時間太長的話,瀏覽器的UI將變得沒有響應,用戶不能點擊任何東西。這是一種很差的用戶體驗,應該盡量避免。 在JavaScript中沒有線程,但你可以在瀏覽器中使用setTimeout來模擬,或者在現代瀏覽器中使用web workers。 ### setTimeout() 它的思想是將一大堆工作分解成為一小段一小段,然后每隔1毫秒運行一段。使用1毫秒的延遲會導致整個任務完成得更慢,但是用戶界面會保持可響應狀態,用戶會覺得瀏覽器沒有失控,覺得更舒服。 > 1毫秒(甚至0毫秒)的延遲執行命令在實際運行的時候會延遲更多,這取決于瀏覽器和操作系統。設定0毫秒的延遲并不意味著馬上執行,而是指“盡快執行”。比如,在IE中,最短的延遲是15毫秒。 ### Web Workers 現代瀏覽器為長時間運行的腳本提供了另一種解決方案:web workers。web workers在瀏覽器內部提供了后臺線程支持,你可以將計算量很大的部分放到一個單獨的文件中,比如my_web_worker.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>"; }; 下面展示了一個做1億次簡單的數學運算的web worker: var end = 1e8, tmp = 1; postMessage('hello there'); while (end) { end -= 1; tmp += end; if (end === 5e7) { // 5e7 is the half of 1e8 postMessage('halfway there, `tmp` is now ' + tmp); } } postMessage('all done'); web worker使用postMessage()來和調用它的程序通訊,調用者通過onmessage事件來接受更新。onmessage事件處理函數接受一個事件對象作為參數,這個對象含有一個由web worker傳過來data屬性。類似的,調用者(在這個例子中)也可以使用ww.postMessage()來給web worker傳遞數據,web worker可以通過一個onmessage事件處理函數來接受這些數據。 上面的例子會在瀏覽器中打印出: message from the background thread: hello there message from the background thread: halfway there, `tmp` is now 3749999975000001 message from the background thread: all done ## 遠程腳本編程 現代web應用經常會使用遠程腳本編程和服務器通訊,而不刷新當前頁面。這使得web應用更靈活,更像桌面程序。我們來看一下幾種用JavaScript和服務器通訊的方法。 ### XMLHttpRequest 現在,XMLHttpRequest是一個特別的對象(構造函數),絕大多數瀏覽器都可以用,它使得我們可以從JavaScript來發送HTTP請求。發送一個請求有以下三步: 1. 初始化一個XMLHttpRequest對象(簡稱XHR) 2. 提供一個回調函數,供請求對象狀態改變時調用 3. 發送請求 第一步很簡單: var xhr = new XMLHttpRequest(); 但是在IE7之前的版本中,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://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(""); 代碼中的一些說明: - 因為IE6及以下版本中,創建XHR對象有一點復雜,所以我們通過一個數組列出ActiveX的名字,然后遍歷這個數組,使用try-catch塊來嘗試創建對象。 - 回調函數會檢查xhr對象的readyState屬性。這個屬性有0到4一共5個值,4代表“complete”(完成)。如果狀態還沒有完成,我們就繼續等待下一次readystatechange事件。 - 回調函數也會檢查xhr對象的status屬性。這個屬性和HTTP狀態碼對應,比如200(OK)或者是404(Not found)。我們只對狀態碼200感興趣,而將其它所有的都報為錯誤(為了簡化示例,否則需要檢查其它不代表出錯的狀態碼)。 - 上面的代碼會在每次創建XHR對象時檢查一遍支持情況。你可以使用前面提到過的模式(如條件初始化)來重寫上面的代碼,使得只需要做一次檢查。 ### JSONP JSONP(JSON with padding)是另一種發起遠程請求的方式。與XHR不同,它不受瀏覽器同源策略的限制,所以考慮到加載第三方站點的安全影響的問題,使用它時應該很謹慎。 一個XHR請求的返回可以是任何類型的文檔: - 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); 服務器返回一些作為參數傳遞給回調函數的JSON數據。最終的結果實際上是頁面中多了一個新的腳本,這個腳本的內容就是一個函數調用,如: myHandler({"hello": "world"}); (譯注:原文這里說得不是太明白。JSONP的返回內容如上面的代碼片段,它的工作原理是在頁面中動態插入一個腳本,這個腳本的內容是函數調用+JSON數據,其中要調用的函數是在頁面中已經定義好的,數據以參數的形式存在。一般情況下數據由服務端動態生成,而函數由頁面生成,為了使返回的腳本能調用到正確的函數,在請求的時候一般會帶上callback參數以便后臺動態返回處理函數的名字。) #### JSONP示例:井字棋 我們來看一個使用JSONP的井字棋游戲示例,玩家就是客戶端(瀏覽器)和服務器。它們兩者都會產生1到9之間的隨機數,我們使用JSONP去取服務器產生的數字(圖8-2)。 你可以在<http://jspatterns.com/book/8/ttt.html>玩這個游戲。 ![圖8-2 使用JSONP的井字棋游戲](./figure/chapter8/8-2.jpg) 圖8-2 使用JSONP的井字棋游戲 界面上有兩個按鈕:一個用于開始新游戲,一個用于取服務器下的棋(客戶端下的棋會在一定數量的延時之后自動進行): <button id="new">New game</button> <button id="server">Server play</button> 界面上包含9個單元格,每個都有對應的id,比如: <td id="cell-1">&nbsp;</td> <td id="cell-2">&nbsp;</td> <td id="cell-3">&nbsp;</td> ... 整個游戲是在一個全局對象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.played,并且將它發送給服務器,這樣服務器就可以返回一個沒有玩過的數字。如果有錯誤發生,服務器會像這樣響應: ttt.serverPlay({"error": "Error description here"}); 如你所見,JSONP中的回調函數必須是公開的并且全局可訪問的函數,它并不一定要是全局函數,也可以是一個全局對象的方法。如果沒有錯誤發生,服務器將會返回一個函數調用,像這樣: ttt.serverPlay(3); 這里的3是指3號單元格是服務器要下棋的位置。在這種情況下,數據非常簡單,甚至都不需要使用JSON格式,只需要一個簡單的值就可以了。 ### 框架(frame)和圖片信標(image beacon) 另外一種做遠程腳本編程的方式是使用框架。你可以使用JavaScript來創建框架并改變它的src URL。新的URL可以包含數據和函數調用來更新調用者,也就是框架之外的父頁面。 遠程腳本編程中最最簡單的情況是你只需要傳遞一點數據給服務器,而并不需要服務器的響應內容。在這種情況下,你可以創建一個新的圖片,然后將它的src指向服務器的腳本: new Image().src = "http://example.org/some/page.php"; 這種模式叫作圖片信標,當你想發送一些數據給服務器記錄時很有用,比如做訪問統計。因為信標的響應對你來說完全是沒有用的,所以通常的做法(不推薦)是讓服務器返回一個1x1的GIF圖片。更好的做法是讓服務器返回一個“204 No Content”HTTP響應。這意味著返回給客戶端的響應只有響應頭(header)而沒有響應體(body)。 ## 部署JavaScript 在生產環境中使用JavaScript時,有不少性能方面的考慮。我們來討論一下最重要的一些。如果需要了解所有的細節,可以參見O'Reilly出社的《高性能網站建設指南》和《高性能網站建設進階指南》。 ### 合并腳本 創建高性能網站的第一個原則就是盡量減少外部引用的組件(譯注:這里指文件),因為HTTP請求的代價是比較大的。具體就JavaScript而言,可以通過合并外部腳本來顯著提高頁面加載速度。 我們假設你的頁面正在使用jQuery庫,這是一個.js文件。然后你使用了一些jQuery插件,這些插件也是單獨的文件。這樣的話在你還一行代碼都沒有寫的時候就已經有了四五個文件了。把這些文件合并起來是很有意義的,尤其是其中的一些體積很小(2-3kb)時,這種情況下,HTTP協議中的開銷會比下載本身還大。合并腳本的意思就是簡單地創建一個新的js文件,然后把每個文件的內容粘貼進去。 當然,合并的操作應該放在代碼部署到生產環境之前,而不是在開發環境中,因為這會使調試變得困難。 合并腳本的不便之處是: - 在部署前多了一步操作,但這很容易使用命令行自動化工具來做,比如使用Linux/Unix的cat: $ cat jquery.js jquery.quickselect.js jquery.limit.js > all.js - 失去一些緩存上的便利——當你對某個文件做了一點小修改之后,會使得整個合并后的代碼緩存失效。所以比較好的方法是為大的項目設定一個發布計劃,或者是將代碼合并為兩個文件:一個包含可能會經常變更的代碼,另一個包含那些不會輕易變更的“核心”。 - 你需要處理合并后文件的命名或者是版本問題,比如使用一個時間戳all_20100426.js或者是使用文件內容的hash值。 這就是主要的不便之處,但它帶來的好處卻是遠遠大于這些麻煩的。 ### 壓縮代碼 第二章中,我們討論過代碼壓縮。部署之前進行代碼壓縮也是一個很重要的步驟。 從用戶的角度來想,完全沒有必要下載代碼中的注釋,因為這些注釋根本不影響代碼運行。 壓縮代碼帶來的好處多少取決于代碼中注釋和空白的數量,也取決于你使用的壓縮工具。平均來說,壓縮可以減少50%左右的體積。 服務端腳本壓縮也是應該要做的事情。配置啟用gzip壓縮是一個一次性的工作,能帶來立桿見影的速度提升。即使你正在使用共享的空間,供應商并沒有提供那么多服務器配置的空間,大部分的供應商也會允許使用.htaccess配置文件。所以可以將這些加入到站點根目錄的.htaccess文件中: AddOutputFilterByType DEFLATE text/html text/css text/plain text/xml application/javascript application/json 平均下來壓縮會節省70%的文件體積。將代碼壓縮和服務端壓縮合計起來,你可以期望你的用戶只下載你寫出來的未壓縮文件體積的15%。 ### 緩存頭 與流行的觀點相反,文件在瀏覽器緩存中的時間并沒有那么久。你可以盡你自己的努力,通過使用Expires頭來增加非首次訪問時命中緩存的概率: 這也是一個在.htaccess中做的一次性配置工作: ExpiresActive On ExpiresByType application/x-javascript "access plus 10 years" 它的弊端是當你想更改這個文件時,你需要給它重命名,如果你已經處理好了合并的文件命名規則,那你就已經處理好這里的命名問題了。 ### 使用CDN CDN是指“文件分發網絡”(Content Delivery Network)。這是一項收費(有時候還相當昂貴)的托管服務,它將你的文件分發到世界上各個不同的數據中心,但代碼中的URL卻都是一樣的,這樣可以使用戶更快地訪問。 即使你沒有CDN的預算,你仍然有一些可以免費使用的東西: - Google托管了很多流行的開源庫,你可以免費使用,并從它的CDN中得到速度提升(譯注:鑒于Google在國內的尷尬處境,不建議使用) - 微軟托管了jQuery和自家的Ajax庫 - 雅虎在自己的CDN上托管了YUI庫 ## 加載策略 怎樣在頁面上引入腳本,這第一眼看起來是一個簡單的問題——使用\<script\>元素,然后要么寫內聯的JavaScript代碼或者是在src屬性中指定一個獨立的文件: // option 1 <script> console.log("hello world"); </script> // option 2 <script src="external.js"></script> 但是,當你的目標是要構建一個高性能的web應用的時候,有些模式和考慮點還是應該知道的。 作為題外話,來看一些比較常見的開發者會用在\<script\>元素上的屬性: - language="JavaScript" 還有一些不同大小寫形式的“JavaScript”,有的時候還會帶上一個版本號。language屬性不應該被使用,因為默認的語言就是JavaScript。版本號也不像想象中工作得那么好,這應該是一個設計上的錯誤。 - type="text/javascript" 這個屬性是HTML4和XHTML1標準所要求的,但它不應該存在,因為瀏覽器會假設它就是JavaScript。HTML5不再要求這個屬性。除非是要強制通過難,否則沒有任何使用type的理由。 - defer (或者是HTML5中更好的async)是一種指定瀏覽器在下載外部腳本時不阻塞頁面其它部分的方法,但還沒有被廣泛支持。關于阻塞的更多內容會在后面提及。 ### \<script\>元素的位置 script元素會阻塞頁面的下載。瀏覽器會同時下載好幾個組件(文件),但遇到一個外部腳本的時候,會停止其它的下載,直到腳本文件被下載、解析、執行完畢。這會嚴重影響頁面的加載時間,尤其是當這樣的事件在頁面加載時發生多次的時候。 為了盡量減小阻塞帶來的影響,你可以將script元素放到頁面的尾部,在\</body\>之前,這樣就沒有可以被腳本阻塞的元素了。此時,頁面中的其它組件(文件)已經被下載完畢并呈現給用戶了。 最壞的“反模式”是在文檔的頭部使用獨立的文件: <!doctype html> <html> <head> <title>My App</title> <!-- ANTIPATTERN --> <script src="jquery.js"></script> <script src="jquery.quickselect.js"></script> <script src="jquery.lightbox.js"></script> <script src="myapp.js"></script> </head> <body> ... </body> </html> 一個更好的選擇是將所有的文件合并起來: <!doctype html> <html> <head> <title>My App</title> <script src="all_20100426.js"></script> </head> <body> ... </body> </html> 最好的選擇是將合并后的腳本放到頁面的尾部: <!doctype html> <html> <head> <title>My App</title> </head> <body> ... <script src="all_20100426.js"></script> </body> </html> ### HTTP分塊 HTTP協議支持“分塊編碼”。它允許將頁面分成一塊一塊發送。所以如果你有一個很復雜的頁面,你不需要將那些每個站都多多少少會有的(靜態)頭部信息也等到所有的服務端工作都完成后再開始發送。 一個簡單的策略是在組裝頁面其余部分的時候將頁面\<head\>的內容作為第一塊發送。也就是像這樣子: <!doctype html> <html> <head> <title>My App</title> </head> <!-- end of chunk #1 --> <body> ... <script src="all_20100426.js"></script> </body> </html> <!-- end of chunk #2 --> 這種情況下可以做一個簡單的發動,將JavaScript移回\<head\>,隨著第一塊一起發送。 這樣的話可以讓服務器在拿到head區內容后就開始下載腳本文件,而此時頁面的其它部分在服務端還尚未就緒: <!doctype html> <html> <head> <title>My App</title> <script src="all_20100426.js"></script> </body> </head> <!-- end of chunk #1 --> <body> ... </html> <!-- end of chunk #2 --> 一個更好的辦法是使用第三塊內容,讓它在頁面尾部,只包含腳本。如果有一些每個頁面都用到的靜態的頭部,也可以將這部分隨和一塊一起發送: <!doctype html> <html> <head> <title>My App</title> </head> <body> <div id="header"> <img src="logo.png" /> ... </div> <!-- end of chunk #1 --> ... The full body of the page ... <!-- end of chunk #2 --> <script src="all_20100426.js"></script> </body> </html> <!-- end of chunk #3 --> 這種方法很適合使用漸進增強思想的網站(關鍵業務不依賴JavaScript)。當HTML的第二塊發送完畢的時候,瀏覽器已經有了一個加載、顯示完畢并且可用的頁面,就像禁用JavaScript時的情況。當JavaScript隨著第三塊到達時,它會進一步增強頁面,為頁面錦上添花。 ### 動態\<script\>元素實現非阻塞下載 前面已經說到過,JavaScript會阻塞后面文件的下載,但有一些模式可以防止阻塞: - 使用XHR加載腳本,然后作為一個字符串使用eval()來執行。這種方法受同源策略的限制,而且引入了eval()這種“反模式”。 - 使用defer和async屬性,但有瀏覽器兼容性問題 - 使用動態\<script\>元素 最后一種是一個很好并且實際可行的模式。和介紹JSONP時所做的一樣,創建一個新的script元素,設置它的src屬性,然后將它放到頁面上。 這是一個異步加載JavaScript,不阻塞其它文件下載的示例: var script = document.createElement("script"); script.src = "all_20100426.js"; document.documentElement.firstChild.appendChild(script); 這種模式的缺點是,在這之后加載的腳本不能依賴這個腳本。因為這個腳本是異步加載的,所以無法保證它什么時候會被加載進來,如果要依賴的話,很可能會訪問到(因還未加載完畢導致的)未定義的對象。 如果要解決這個問題,可以讓內聯的腳本不立即執行,而是作為一個函數放到一個數組中。當依賴的腳本加載完畢后,再執行數組中的所有函數。所以一共有三個步驟。 首先,創建一個數組用來存儲所有的內聯代碼,定義的位置盡量靠前: var mynamespace = { inline_scripts: [] }; 然后你需要將這些單獨的內聯腳本包裹進一個函數中,然后將每個函數放到inline_scripts數組中,也就是這樣: // was: // <script>console.log("I am inline");</script> // becomes: <script> mynamespace.inline_scripts.push(function () { console.log("I am inline"); }); </script> 最后一步是使用異步加載的腳本遍歷這個數組,然后執行函數: var i, scripts = mynamespace.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); 當你能控制結構的時候,這樣做沒有問題,但是如果你在寫掛件(widget)或者是廣告時,你并不知道托管它的是一個什么樣的頁面。甚至可能頁面上連\<head\>和\<body\>都沒有,盡管document.body在絕大多數沒有\<body\>標簽的時候也可以工作: document.body.appendChild(script); 可以肯定頁面上一定存在的一個標簽是你正在運行的腳本所處的位置——script標簽。(對內聯或者外部文件來說)如果沒有script標簽,那么代碼就不會運行。可以利用這一事實,在頁面的第一個script標簽上使用insertBefore(): var first_script = document.getElementsByTagName('script')[0]; first_script.parentNode.insertBefore(script, first_script); frist_script是頁面中一定存在的一個script標簽,script是你創建的新的script元素。 ### 延遲加載 所謂的延遲加載是指在頁面的load事件之后再加載外部文件。通常,將一個大的合并后的文件分成兩部分是有好處的: - 一部分是頁面初始化和綁定UI元素的事件處理函數必須的 - 第二部分是只在用戶交互或者其它條件下才會用到的 目標就是逐步加載頁面,讓用戶盡快可以進行一些操作。剩余的部分可以在用戶可以看到頁面的時候再在后臺加載。 加載第二部分JavaScript的方法也是使用動態script元素,將它加在head或者body中: .. The full body of the page ... <!-- end of chunk #2 --> <script src="all_20100426.js"></script> <script> window.onload = function () { var script = document.createElement("script"); script.src = "all_lazy_20100426.js"; document.documentElement.firstChild.appendChild(script); }; </script> </body> </html> <!-- end of chunk #3 --> 對很多應用來說,延遲加載的部分大部分情況下會比核心部分要大,因為我們關注的“行為”(比如拖放、XHR、動畫)只在用戶初始化之后才會發生。 ### 按需加載 前面的模式會在頁面加載后無條件加載其它的JavaScript,并假設這些代碼很可能會被用到。但我們是否可以做得更好,分部分加載,在真正需要使用的時候才加載那一部分? 假設你頁面的側邊欄上有一些tabs。點擊tab會發出一個XHR請求獲取內容,然后更新tab的內容,然后有一個更新的動畫。如果這是頁面上唯一需要XHR和動畫庫的地方,而用戶又不點擊tab的話會怎樣? 下面介紹按需加載模式。你可以創建一個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(); } }; // others newjs.onload = function () { callback(); }; newjs.src = file; script.parentNode.insertBefore(newjs, script); } 這個實現的幾點說明: - 在IE中需要訂閱readystatechange事件,然后判斷狀態是否為“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()函數: require('ondemand.js.php', function () { extraFunction('loaded from the parent page'); document.body.appendChild(document.createTextNode('done!')); }); 這段代碼會在console中打印兩條,然后頁面中會顯示“done!”,你可以在<http://jspatterns.com/book/7/ondemand.html>看到示例。 ### 預加載JavaScript 在延遲加載模式和按需加載模式中,我們加載了當前頁面需要用到的腳本。除此之外,我們也可以加載當前頁面不需要但可能在接下來的頁面中需要的腳本。這樣的話,當用戶進入第二個頁面時,腳本已經被預加載過,整體體驗會變得更快。 預加載可以簡單地通過動態腳本模式實現。但這也意味著腳本會被解析和執行。解析僅僅會在頁面加載時間中增加預加載消耗的時間,但執行卻可能導致JavaScript錯誤,因為預加載的腳本會假設自己運行在第二個頁面上,比如找一個特寫的DOM節點就可能出錯。 僅加載腳本而不解析和執行是可能的,這也同樣適用于CSS和圖像。 在IE中,你可以使用熟悉的圖片信標模式來發起請求: new Image().src = "preloadme.js"; 在其它的瀏覽器中,你可以使用\<object\>替代script元素,然后將它的data屬性指向腳本的URL: var obj = document.createElement('object'); obj.data = "preloadme.js"; document.body.appendChild(obj); 為了阻止object可見,你應該設置它的width和height屬性為0。 你可以創建一個通用的preload()函數或者方法,使用條件初始化模式(第4章)來處理瀏覽器差異: var preload; if (/*@cc_on!@*/false) { // IE sniffing with conditional comments 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是否是“function”來代替嗅探。但這種方法其實沒有作用,因為所有的瀏覽器都支持new Image();只是有一些瀏覽器會為圖片單獨做緩存,意味著作為圖片緩存下來的組件(文件)在第二個頁面中不會被作為腳本取出來,而是會重新下載。 > 瀏覽器嗅探中使用條件注釋很有意思,這明顯比在navigator.userAgent中找字符串要安全得多,因為用戶可以很容易地修改這些字符串。 > 比如: > var isIE = /*@cc_on!@*/false; > 會在其它的瀏覽器中將isIE設為false(因為忽略了注釋),但在IE中會是true,因為在條件注釋中有取反運算符!。在IE中就像是這樣: > var isIE = !false; // true 預加載模式可以被用于各種組件(文件),而不僅僅是腳本。比如在登錄頁就很有用。當用戶開始輸入用戶名時,你可以使用打字的時間開始預加載(非敏感的東西),因為用戶很可能會到第二個也就是登錄后的頁面。 ## 小結 在前一章中我們討論了JavaScript核心的模式,它們與環境無關,這一章主要關注了只在客戶端瀏覽器環境中應用的模式。 我們看了: - 分離的思想(HTML:內容,CSS:表現,JavaScript:行為),只用于增強體驗的JavaScript以及基于特性檢測的瀏覽器探測。(盡管在本章的最后你看到了如何打破這個模式。) - DOM編程——加速DOM訪問和操作的模式,主要通過將DOM操作集中在一起來實現,因為頻繁和DOM打交道代碼是很高的。 - 事件,跨瀏覽器的事件處理,以及使用事件代碼來減少事件處理函數的綁定數量以提高性能。 - 兩種處理長時間大計算量腳本的模式——使用setTimeout()將長時間操作拆分為小塊執行和在現代瀏覽器中使用web workers。 - 多種用于遠程編程,進行服務器和客戶端通訊的模式——XHR,JSONP,框架和圖片信標。 - 在生產環境中部署JavaScript的步驟——將腳本合并為更少的文件,壓縮和gzip(總共節省85%),可能的話托管到CDN并發送Expires頭來提升緩存效果。 - 基于性能考慮引入頁面腳本的模式,包括:放置\<script\>元素的位置,同時也可以從HTTP分塊獲益。為了減少頁面初始化時加載大的腳本文件引起的初始化工作量,我們討論了幾種不同的模式,比如延遲加載、預加載和按需加載。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看