HTML5 Web Workers來加速您的移動Web應用

一直以來,Web 應用程序被侷限在一個單線程世界中。這的確限制了開發人員在他們的代碼中的做爲,由於任何太複雜的東西都存在凍結應用程序 UI 的風險。經過將多線程引入 Web 應用程…javascript

在本文中,您將使用最新的 Web 技術開發 Web 應用程序。這裏的 大部分代碼只是 HTML、JavaScript 和 CSS — 全部 Web 開發人員的核心技術。所需的最重要的工具是用於進行測試的瀏覽器。本文大部分代碼將在最新桌面瀏覽器上運行,但也有一些例外,咱們將在文章中進行說明。固然,您也必須在移動瀏覽器上測試,爲此,您須要最新的 iPhone 和 Android SDKs。本文將使用 iPhone SDK 3.1.3 和 Android SDK 2.1。本文的樣例還將使用一個代理服務器來從瀏覽器訪問遠程服務。這個代理服務器是一個簡單的 Java™ servlet,但也可使用以 PHP、Ruby 以及其餘語言編寫的代理輕鬆替換。html

移動設備上的多線程 JavaScripthtml5

對於大多數開發人員來講,多線程或併發編程並不新鮮。可是,JavaScript 並非一種支持併發編程的語言。JavaScript 的建立者認爲,對於 JavaScript 這樣旨在 Web 頁面上執行簡單任務的語言來講,併發編程容易出現問題,並且沒有必要。然而,因爲 Web 頁面已經發展成爲 Web 應用程序,使用 JavaScript 完成的任務的複雜程度已經大大增長,向 JavaScript 提出了與其餘語言同等的要求。與此同時,使用其餘支持併發編程的語言工做的開發人員常常面臨伴隨線程和 mutexes 這樣的併發原語而來的超高複雜性的困擾。實際上,最近像 Scala、Clojure 和 F# 這樣的幾種新語言已經發展,它們都有可能簡化併發性。java

經常使用縮略詞
  • Ajax:異步 JavaScript + XML
  • API:應用程序編程接口
  • CSS:層疊樣式表
  • DOM:文檔對象模型
  • HTML:超文本標記語言
  • REST:具象狀態傳輸
  • SDK:軟件開發工具包
  • UI:用戶界面
  • URL:統一資源定位符
  • W3C:萬維網聯盟
  • XML:可擴展標記語言

Web Worker 規範不僅是向 JavaScript 和 Web 瀏覽器添加併發性,並且是以一種智慧的方式添加,這種方式將增長開發人員的能力,但不會向他們提供一種會致使問題的工具。 例如,多年來,桌面應用程序開發人員一直在使用多線程來支持他們的應用程序訪問多個 I/O 資源,以免在等待這些資源時凍結 UI。然而,當這些多線程更改共享的資源(包括 UI)時,這樣的應用程序一般會出現問題,由於這種行爲可能會致使應用程序凍結或崩潰。有了 Web Workers,這種狀況就不會發生。衍生線程不能訪問主 UI 線程訪問的資源。事實上,衍生線程中的代碼甚至不能與主 UI 線程執行的代碼位於同一個文件中。shell

您甚至必須提供相應的外部文件做爲構造函數的一部分,如 清單 1 所示。編程

這個進程使用三個資源:api

  1. 在主線程上執行的 Web 頁面 JavaScript(我稱其爲頁面腳本)。
  2. Worker 對象,這是用於執行 Web Worker 函數的 JavaScript 對象。
  3. 將在新衍生的線程上執行的腳本。我稱其爲 Worker 腳本。

讓咱們首先看看 清單 1 中的頁面腳本。數組


清單 1.在頁面腳本中使用一個 Web Worker瀏覽器

JavaScript Code複製內容到剪貼板
  1. var worker = new Worker("worker.js");  
  2. worker.onmessage = function(message){  
  3.     // do stuff  
  4. };  
  5. worker.postMessage(someDataToDoStuffWith);  

 

在 清單 1 中,您能夠看到使用 Web Workers 的三個基本步驟。首先,您建立一個 Worker 對象並向它傳遞將在新線程中執行的腳本的 URL。Worker 將執行的全部代碼都必須包含在一個 Worker 腳本中,該腳本的 URL 將被傳遞到這個 Worker 的構造函數中。這個 Worker 腳本的 URL 受到瀏覽器的同源策略的限制 — 它必須來自加載這個頁面的同一個域,該頁面已加載正在建立這個 Web Worker 的頁面腳本。緩存

下一步是使用 onmessage 函數指定一個回調處理器函數。這個回調函數將在該 Worker 腳本執行後調用。message 是從該 Worker 腳本返回的數據,您能夠隨意處理該消息。回調函數在主線程上執行,所以它能訪問 DOM。Worker 腳本在一個不一樣的線程內運行且不能訪問 DOM,所以,您須要未來自這個 Worker 腳本的數據返回主線程,在那裏,您能夠安全地修改 DOM 來更新您的應用程序的 UI。這是 Web Workers 的無共享設計的關鍵特性。

清單 1 中的最後一行展現如何經過調用 Worker 的 postMessage 函數來啓動它。這裏,您傳遞一條消息(重申一下,它只是數據)給 Worker。固然,postMessage 是一個異步函數;您調用它,它就當即返回。

如今,檢查這個 Worker 腳本。清單 2 中的代碼是來自 清單 1 的 worker.js 文件的內容。


清單 2. 一個 Worker 腳本

JavaScript Code複製內容到剪貼板
  1. importScripts("utils.js");  
  2. var workerState = {};  
  3. onmessage = function(message){  
  4.      workerState = message.data;  
  5.       // do stuff with the message  
  6.     postMessage({responseXml: this.responseText});  
  7. }  

 

能夠看到,這個 Worker 腳本擁有本身的 onmessage 函數。該函數在您從主線程調用 postMessage 時調用。從頁面腳本傳來的數據被傳遞到 message 對象中的 postMessage 函數。您經過檢索 message 對象的 data 屬性來訪問該數據。當您處理完 Worker 腳本中的數據時,調用 postMessage 函數將數據返回主線程。主線程也能夠經過訪問它接收到的消息的 data 屬性來訪問該數據。

至此,您已經見識了 Web Workers 的這個簡單、但強大的語義。接下來,您將瞭解如何應用這個語義來加速移動 Web 應用程序。在此以前,有必要先討論一下設備支持。畢竟,這些是移動 Web 應用程序,且處理不一樣瀏覽器之間的功能的區別對於移動 Web 應用程序開發很重要。

設備支持

從 Android 2.0 開始,Android 瀏覽器就擁有了對 HTML 5 Web Worker 規範的全面支持。在撰寫本文之時,最新的 Android 設備(包括很是流行的 Motorola Droid)已配置了 Android 2.1。另外,此特性在運行 Maemo 操做系統的 Nokia 設備上的 Mozilla Fennec 瀏覽器以及 Windows Mobile 設備上受到徹底支持。這裏須要引發注意的遺漏是 iPhone。iPhone OS 3.1.3 和 3.2 版(在 iPad 上運行的 OS 的版本)並不支持 Web Workers。可是,此特性已在 Safari 上受到支持,所以,此特性在運行在 iPhone 上的 Mobile Safari 瀏覽器上出現應該只是一個時間問題。鑑於 iPhone 的主導地位(尤爲是在美國),最好不要依賴 Web Workers 的存在,且不要只在您檢測到它們的存在時才使用它們來加強您的移動 Web 應用程序。意識到這一點後,咱們來看看如何使用 Web Workers 來加速您的移動 Web 應用程序。

使用 Workers 改善性能

智能手機瀏覽器上的 Web Worker 支持很不錯,並且一直在不斷改進。這就提出了一個問題:何時須要在移動 Web 應用程序中使用 Workers?答案很簡單:須要完成耗時的任務的任什麼時候候。有些示例展現瞭如何將 Workers 用於執行密集的數學計算,好比計算 1 萬位數的圓周率。極可能您永遠也不須要在 Web 應用程序上執行這樣一個計算,在移動 Web 應用程序上執行這種計算的概率則更小。可是,從遠程資源檢索數據則至關常見,這也是本文示例的關注點。

在這個示例中,您將從 eBay 檢索一個 Daily Deals(天天都在變化的交易)列表。這個交易列表包含關於每筆交易的簡短信息。更詳細的信息能夠經過使用 eBay 的 Shopping API 獲取。當用戶瀏覽這個交易列表選擇感興趣的商品時,您將使用 Web Workers 來預取這個附加信息。要從您的 Web 應用程序訪問全部這些 eBay 數據,您須要經過使用一個泛型代理(generic proxy)來處理瀏覽器的同源策略。一個簡單的 Java servlet 將用於這個代理,它包含在本文的代碼中,但不在這裏單獨展現。相反,咱們將把注意力集中在處理 Web Workers 的代碼上。清單 3 展現了這個交易應用程序的基本 HTML 頁面。

清單 3. 交易應用程序 HTML

XML/HTML Code複製內容到剪貼板
  1. <!DOCTYPE HTML>  
  2. <html>  
  3.   <head>  
  4.     <meta http-equiv="content-type" content="text/html; charset=UTF-8">  
  5.     <meta name = "viewport" content = "width = device-width">  
  6.     <title>Worker Deals</title>  
  7.     <script type="text/javascript" src="common.js"></script>  
  8.   </head>  
  9.   <body onload="loadDeals()">  
  10.     <h1>Deals</h1>  
  11.     <ol id="deals">  
  12.     </ol>  
  13.     <h2>More Deals</h2>  
  14.     <ul id="moreDeals">  
  15.     </ul>  
  16.   </body>  
  17. </html>  

 

能夠看出,這是一段很是簡單的 HTML;它只是一個 shell。您使用 JavaScript 檢索數據並生成 UI。這是移動 Web 應用程序的優化設計,由於它容許將全部代碼和靜態標記緩存到設備上,用戶只需等待來自服務器的數據。注意,在 清單 3 中,一旦那個 body 加載,您就調用 loadDeals 函數,在那裏,您將加載 清單 4 中的應用程序的初始數據。


清單 4. loadDeals 函數

JavaScript Code複製內容到剪貼板
  1. var deals = [];  
  2. var sections = [];  
  3. var dealDetails = {};  
  4. var dealsUrl = "http://deals.ebay.com/feeds/xml";  
  5. function loadDeals(){  
  6.     var xhr = new XMLHttpRequest();  
  7.     xhr.onreadystatechange = function(){  
  8.         if (this.readyState == 4 && this.status == 200){  
  9.                var i = 0;  
  10.                var j = 0;  
  11.                var dealsXml = this.responseXML.firstChild;  
  12.                var childNode = {};  
  13.                for (i=0; i< dealsXml.childNodes.length;i++){  
  14.                    childNode = dealsXml.childNodes.item(i);  
  15.                    switch(childNode.localName){  
  16.                    case 'Item':   
  17.                        deals.push(parseDeal(childNode));  
  18.                        break;  
  19.                    case "MoreDeals":  
  20.                        for (j=0;j<childNode.childNodes.length;j++){  
  21.                            var sectionXml= childNode.childNodes.item(j);  
  22.                            if (sectionXml && sectionXml.hasChildNodes()){  
  23.                                sections.push(parseSection(sectionXml));  
  24.                            }  
  25.                        }  
  26.                        break;      
  27.                    default:  
  28.                        break;  
  29.                    }  
  30.                }  
  31.                deals.forEach(function(deal){  
  32.                    var entry = createDealUi(deal);  
  33.                    $("deals").appendChild(entry);  
  34.                });  
  35.                loadDetails(deals);  
  36.                sections.forEach(function(section){  
  37.                    var ui = createSectionUi(section);  
  38.                    $("moreDeals").appendChild(ui);  
  39.                    loadDetails(section.deals);  
  40.                });  
  41.         }  
  42.     };  
  43.     xhr.open("GET", "proxy?url=" + escape(dealsUrl));  
  44.     xhr.send(null);  
  45. }  

 

清單 4 展現了 loadDeals 函數,以及應用程序中使用的全局變量。您使用了一個 deals 數組和一個 sections 數組。它們是相關交易的附加組(例如,Deals under $10)。還有一個名爲 dealDetails 的映射,其鍵是 Item IDs(來自於交易數據),其值是從 eBay Shopping API 獲取的詳細信息。

您首先調用一個代理,該代理又將調用 eBay Daily Deals REST API。這將把交易列表做爲一個 XML 文檔提供給您。您解析用於進行 Ajax 調用的 XMLHttpRequest 對象的 onreadystatechange 函數中的文檔。您還使用其餘兩個函數,parseDeal 和 parseSection,來將 XML 節點解析爲更易於使用的 JavaScript 對象。這些函數能夠在可下載的代碼樣例(參見 下載 部分)中找到,但因爲它們只是使人厭煩的 XML 解析函數,所以我在這裏沒有包括它們。最後,在解析了 XML 後,您還使用了另外兩個函數,createDealUi 和createSectionUi,來修改 DOM。此時,這個 UI 如 圖 1 所示。


圖 1. Mobile Deals UI
帶有樣例交易的 Mobile Deals UI 的屏幕截圖,每一個交易都有一個 Show Details 按鈕 

若是您返回 清單 4,就會注意到在加載主交易以後,您對這些交易的每一個部分都調用了 loadDetails 函數。在這個函數中,您經過使用 eBay Shopping API 加載每一個交易的附加細節 — 但前提是瀏覽器支持 Web Workers。清單 5 展現了 loadDetails 函數。


清單 5. 預取交易細節

JavaScript Code複製內容到剪貼板
  1. function loadDetails(items){  
  2.     if (!!window.Worker){  
  3.         items.forEach(function(item){  
  4.             var xmlStr = null;  
  5.             if (window.localStorage){  
  6.                 xmlStr = localStorage.getItem(item.itemId);  
  7.             }  
  8.             if (xmlStr){  
  9.                 var itemDetails = parseFromXml(xmlStr);  
  10.                 dealDetails[itemDetails.id] = itemDetails;  
  11.             } else {  
  12.                 var worker = new Worker("details.js");  
  13.                 worker.onmessage = function(message){  
  14.                     var responseXmlStr =message.data.responseXml;  
  15.                     var itemDetails=parseFromXml(responseXmlStr);  
  16.                     if (window.localStorage){  
  17.                         localStorage.setItem(  
  18.                                         itemDetails.id, responseXmlStr);  
  19.                     }  
  20.                     dealDetails[itemDetails.id] = itemDetails;  
  21.                 };  
  22.                     worker.postMessage(item.itemId);  
  23.             }  
  24.         });  
  25.     }  
  26. }  

 

在 loadDetails 中,您首先檢查全局做用域(window 對象)中的 Worker 函數。若是該函數不在那裏,那麼無需作任何事。反之,您首先檢查 XML 的 localStorage 以獲取這個交易的細節。這是移動 Web 應用程序經常使用的本地緩存策略,本系列第 2 部分(參見 參考資料 部分的連接)詳細介紹過這種策略。

若是 XML 位於本地,那麼您在 parseFromXml 函數中解析它並將交易細節添加到 dealDetails 對象。反之,則衍生一個 Web Worker 並使用 postMessage 向其發送 Item ID。當這個 Worker 檢索到數據並將數據發佈回主線程後,您解析 XML,將結果添加到dealDetails,而後將 XML 存儲到 localStorage 中。清單 6 展現了這個 Worker 腳本:details.js。


清單 6. 交易細節 Worker 腳本

JavaScript Code複製內容到剪貼板
  1. importScripts("common.js");  
  2. onmessage = function(message){  
  3.     var itemId = message.data;  
  4.     var xhr = new XMLHttpRequest();  
  5.     xhr.onreadystatechange = function(){  
  6.         if (this.readyState == 4 && this.status == 200){  
  7.             postMessage({responseXml: this.responseText});  
  8.         }  
  9.     };  
  10.     var urlStr = generateUrl(itemId);  
  11.     xhr.open("GET", "proxy?url=" + escape(urlStr));  
  12.     xhr.send(null);  
  13. }  

這個 Worker 腳本很是簡單。您使用 Ajax 調用代理,該代理又調用 eBay Shopping API。當您收到來自代理的 XML 後,使用一個 JavaScript 對象文字(object literal)將其發送回主線程。注意,即便您可以使用來自一個 Worker 的 XMLHttpRequest,但全部信息都將返回它的 responseText 屬性,而不是它的 responseXml 屬性。這是由於這個 Worker 腳本範圍內沒有 JavaScript DOM 解析器。注意,generateUrl 函數來自 common.js 文件(見 清單 7)。您使用 importScripts 函數導入 common.js 文件。


清單 7. Worker 導入的腳本

JavaScript Code複製內容到剪貼板
  1. function generateUrl(itemId){  
  2.     var appId = "YOUR APP ID GOES HERE";  
  3.     return "http://open.api.ebay.com/shopping?callname=GetSingleItem&"+  
  4.         "responseencoding=XML&appid=" + appId + "&siteid=0&version=665"  
  5.             +"&ItemID=" + itemId;  
  6. }  

 

如今,您已經知道如何(爲支持 Web Workers 的瀏覽器)填充交易細節,咱們返回 圖 1 研究一下如何在應用程序中使用這種方法。注意,每筆交易旁邊都有一個 Show Details 按鈕,單擊該按鈕修改這個 UI,如 圖 2 所示。


圖 2. 顯示的交易細節
顯示交易細節的屏幕截圖,包含兩個 Golla 小包(MP三、移動電話和相機)的產品說明、圖片和價格 

這個 UI 將在您調用 showDetails 函數時顯示。清單 8 展現了這個函數。


清單 8. showDetails 函數

JavaScript Code複製內容到剪貼板
  1. function showDetails(id){  
  2.     var el = $(id);  
  3.     if (el.style.display == "block"){  
  4.         el.style.display = "none";  
  5.     } else {  
  6.         el.style.display = "block";  
  7.         if (!el.innerHTML){  
  8.             var details = dealDetails[id];  
  9.             if (details){  
  10.                 var ui = createDetailUi(details);  
  11.                 el.appendChild(ui);  
  12.             } else {  
  13.                 var itemId = id;  
  14.                 var xhr = new XMLHttpRequest();  
  15.                 xhr.onreadystatechange = function(){  
  16.                     if (this.readyState == 4 &&   
  17.                                       this.status == 200){  
  18.                         var itemDetails =   
  19.                                         parseFromXml(this.responseText);  
  20.                         if (window.localStorage){  
  21.                             localStorage.setItem(  
  22.                                               itemDetails.id,   
  23.                                               this.responseText);  
  24.                         }  
  25.                         dealDetails[id] = itemDetails;  
  26.                         var ui = createDetailUi(itemDetails);  
  27.                         el.appendChild(ui);  
  28.                     }  
  29.                 };  
  30.                 var urlStr = generateUrl(id);  
  31.                 xhr.open("GET", "proxy?url=" + escape(urlStr));  
  32.                 xhr.send(null);                          
  33.             }  
  34.         }  
  35.     }  
  36. }  

 

您收到了即將顯示的交易的 ID 並切換是否顯示它。當該函數第一次調用時,它將檢查細節是否已經存儲到 dealDetails 映射中。若是瀏覽器支持 Web Workers,那麼這些細節已經存儲且它的 UI 已經建立並添加到 DOM 中。若是這些細節尚未加載,或者,若是瀏覽器不支持 Workers,那麼您須要執行一個 Ajax 調用來加載此數據。這就是這個應用程序不管在有無 Workers 時都一樣能正常工做的緣由。這意味着,若是 Workers 受到支持,那麼數據就已被加載且 UI 將當即響應。若是沒有 Workers,UI 仍將加載,只是須要花費幾秒鐘時間。

結束語

對於 Web 開發人員來講,Web Workers 聽起來就像一種外來的新技術。可是,如本文所述,它們是很是實用的應用程序。這對於移動 Web 應用程序來講尤爲正確。這些 Workers 可用於預取數據或執行其餘預先操做,從而提供一個更加實時的 UI。這對於須要經過網速可能較慢的網絡加載數據的移動 Web 應用程序來講尤爲正確。結合使用這種技術和緩存策略,您的應用程序的快捷反應將使您的用戶感到驚喜!

相關文章
相關標籤/搜索