在本文中,您將使用最新的 Web 技術(shù)開發(fā) Web 應(yīng)用程序。這里的 大部分代碼只是 HTML、JavaScript 和 CSS — 所有 Web 開發(fā)人員的核心技術(shù)。所需的最重要的工具是用于進(jìn)行測(cè)試的瀏覽器。本文大部分代碼將在最新桌面瀏覽器上運(yùn)行,但也有一些例外,我們將在文章中進(jìn)行說明。當(dāng)然,您也必須在移動(dòng)瀏覽器上測(cè)試,為此,您需要最新的 iPhone 和 Android SDKs。本文將使用 iPhone SDK 3.1.3 和 Android SDK 2.1。本文的樣例還將使用一個(gè)代理服務(wù)器來從瀏覽器訪問遠(yuǎn)程服務(wù)。這個(gè)代理服務(wù)器是一個(gè)簡(jiǎn)單的 Java™ servlet,但也可以使用以 PHP、Ruby 以及其他語(yǔ)言編寫的代理輕松替換。
移動(dòng)設(shè)備上的多線程 JavaScript
對(duì)于大多數(shù)開發(fā)人員來說,多線程或并發(fā)編程并不新鮮。但是,JavaScript 并不是一種支持并發(fā)編程的語(yǔ)言。JavaScript 的創(chuàng)建者認(rèn)為,對(duì)于 JavaScript 這樣旨在 Web 頁(yè)面上執(zhí)行簡(jiǎn)單任務(wù)的語(yǔ)言來說,并發(fā)編程容易出現(xiàn)問題,而且沒有必要。然而,由于 Web 頁(yè)面已經(jīng)發(fā)展成為 Web 應(yīng)用程序,使用 JavaScript 完成的任務(wù)的復(fù)雜程度已經(jīng)大大增加,向 JavaScript 提出了與其他語(yǔ)言同等的要求。與此同時(shí),使用其他支持并發(fā)編程的語(yǔ)言工作的開發(fā)人員經(jīng)常面臨伴隨線程和 mutexes 這樣的并發(fā)原語(yǔ)而來的超高復(fù)雜性的困擾。實(shí)際上,最近像 Scala、Clojure 和 F# 這樣的幾種新語(yǔ)言已經(jīng)發(fā)展,它們都有可能簡(jiǎn)化并發(fā)性。
常用縮略詞- Ajax:異步 JavaScript + XML
- API:應(yīng)用程序編程接口
- CSS:層疊樣式表
- DOM:文檔對(duì)象模型
- HTML:超文本標(biāo)記語(yǔ)言
- REST:具象狀態(tài)傳輸
- SDK:軟件開發(fā)工具包
- UI:用戶界面
- URL:統(tǒng)一資源定位符
- W3C:萬(wàn)維網(wǎng)聯(lián)盟
- XML:可擴(kuò)展標(biāo)記語(yǔ)言
Web Worker 規(guī)范不只是向 JavaScript 和 Web 瀏覽器添加并發(fā)性,而且是以一種智慧的方式添加,這種方式將增加開發(fā)人員的能力,但不會(huì)向他們提供一種會(huì)導(dǎo)致問題的工具。 例如,多年來,桌面應(yīng)用程序開發(fā)人員一直在使用多線程來支持他們的應(yīng)用程序訪問多個(gè) I/O 資源,以避免在等待這些資源時(shí)凍結(jié) UI。然而,當(dāng)這些多線程更改共享的資源(包括 UI)時(shí),這樣的應(yīng)用程序通常會(huì)出現(xiàn)問題,因?yàn)檫@種行為可能會(huì)導(dǎo)致應(yīng)用程序凍結(jié)或崩潰。有了 Web Workers,這種情況就不會(huì)發(fā)生。衍生線程不能訪問主 UI 線程訪問的資源。事實(shí)上,衍生線程中的代碼甚至不能與主 UI 線程執(zhí)行的代碼位于同一個(gè)文件中。
您甚至必須提供相應(yīng)的外部文件作為構(gòu)造函數(shù)的一部分,如 清單 1 所示。
這個(gè)進(jìn)程使用三個(gè)資源:
- 在主線程上執(zhí)行的 Web 頁(yè)面 JavaScript(我稱其為頁(yè)面腳本)。
- Worker 對(duì)象,這是用于執(zhí)行 Web Worker 函數(shù)的 JavaScript 對(duì)象。
- 將在新衍生的線程上執(zhí)行的腳本。我稱其為 Worker 腳本。
讓我們首先看看 清單 1 中的頁(yè)面腳本。
清單 1.在頁(yè)面腳本中使用一個(gè) Web Worker
- var worker = new Worker("worker.js");
- worker.onmessage = function(message){
- // do stuff
- };
- worker.postMessage(someDataToDoStuffWith);
在 清單 1 中,您可以看到使用 Web Workers 的三個(gè)基本步驟。首先,您創(chuàng)建一個(gè) Worker 對(duì)象并向它傳遞將在新線程中執(zhí)行的腳本的 URL。Worker 將執(zhí)行的所有代碼都必須包含在一個(gè) Worker 腳本中,該腳本的 URL 將被傳遞到這個(gè) Worker 的構(gòu)造函數(shù)中。這個(gè) Worker 腳本的 URL 受到瀏覽器的同源策略的限制 — 它必須來自加載這個(gè)頁(yè)面的同一個(gè)域,該頁(yè)面已加載正在創(chuàng)建這個(gè) Web Worker 的頁(yè)面腳本。
下一步是使用 onmessage 函數(shù)指定一個(gè)回調(diào)處理器函數(shù)。這個(gè)回調(diào)函數(shù)將在該 Worker 腳本執(zhí)行后調(diào)用。message 是從該 Worker 腳本返回的數(shù)據(jù),您可以隨意處理該消息。回調(diào)函數(shù)在主線程上執(zhí)行,因此它能訪問 DOM。Worker 腳本在一個(gè)不同的線程內(nèi)運(yùn)行且不能訪問 DOM,因此,您需要將來自這個(gè) Worker 腳本的數(shù)據(jù)返回主線程,在那里,您可以安全地修改 DOM 來更新您的應(yīng)用程序的 UI。這是 Web Workers 的無共享設(shè)計(jì)的關(guān)鍵特性。
清單 1 中的最后一行展示如何通過調(diào)用 Worker 的 postMessage 函數(shù)來啟動(dòng)它。這里,您傳遞一條消息(重申一下,它只是數(shù)據(jù))給 Worker。當(dāng)然,postMessage 是一個(gè)異步函數(shù);您調(diào)用它,它就立即返回。
現(xiàn)在,檢查這個(gè) Worker 腳本。清單 2 中的代碼是來自 清單 1 的 worker.js 文件的內(nèi)容。
清單 2. 一個(gè) Worker 腳本
- importScripts("utils.js");
- var workerState = {};
- onmessage = function(message){
- workerState = message.data;
- // do stuff with the message
- postMessage({responseXml: this.responseText});
- }
可以看到,這個(gè) Worker 腳本擁有自己的 onmessage 函數(shù)。該函數(shù)在您從主線程調(diào)用 postMessage 時(shí)調(diào)用。從頁(yè)面腳本傳來的數(shù)據(jù)被傳遞到 message 對(duì)象中的 postMessage 函數(shù)。您通過檢索 message 對(duì)象的 data 屬性來訪問該數(shù)據(jù)。當(dāng)您處理完 Worker 腳本中的數(shù)據(jù)時(shí),調(diào)用 postMessage 函數(shù)將數(shù)據(jù)返回主線程。主線程也可以通過訪問它接收到的消息的 data 屬性來訪問該數(shù)據(jù)。
至此,您已經(jīng)見識(shí)了 Web Workers 的這個(gè)簡(jiǎn)單、但強(qiáng)大的語(yǔ)義。接下來,您將了解如何應(yīng)用這個(gè)語(yǔ)義來加速移動(dòng) Web 應(yīng)用程序。在此之前,有必要先討論一下設(shè)備支持。畢竟,這些是移動(dòng) Web 應(yīng)用程序,且處理不同瀏覽器之間的功能的區(qū)別對(duì)于移動(dòng) Web 應(yīng)用程序開發(fā)很重要。
設(shè)備支持
從 Android 2.0 開始,Android 瀏覽器就擁有了對(duì) HTML 5 Web Worker 規(guī)范的全面支持。在撰寫本文之時(shí),最新的 Android 設(shè)備(包括非常流行的 Motorola Droid)已配置了 Android 2.1。另外,此特性在運(yùn)行 Maemo 操作系統(tǒng)的 Nokia 設(shè)備上的 Mozilla Fennec 瀏覽器以及 Windows Mobile 設(shè)備上受到完全支持。這里需要引起注意的遺漏是 iPhone。iPhone OS 3.1.3 和 3.2 版(在 iPad 上運(yùn)行的 OS 的版本)并不支持 Web Workers。但是,此特性已在 Safari 上受到支持,因此,此特性在運(yùn)行在 iPhone 上的 Mobile Safari 瀏覽器上出現(xiàn)應(yīng)該只是一個(gè)時(shí)間問題。鑒于 iPhone 的主導(dǎo)地位(尤其是在美國(guó)),最好不要依賴 Web Workers 的存在,且不要只在您檢測(cè)到它們的存在時(shí)才使用它們來增強(qiáng)您的移動(dòng) Web 應(yīng)用程序。意識(shí)到這一點(diǎn)后,我們來看看如何使用 Web Workers 來加速您的移動(dòng) Web 應(yīng)用程序。
使用 Workers 改善性能
智能手機(jī)瀏覽器上的 Web Worker 支持很不錯(cuò),而且一直在不斷改進(jìn)。這就提出了一個(gè)問題:什么時(shí)候需要在移動(dòng) Web 應(yīng)用程序中使用 Workers?答案很簡(jiǎn)單:需要完成耗時(shí)的任務(wù)的任何時(shí)候。有些示例展示了如何將 Workers 用于執(zhí)行密集的數(shù)學(xué)計(jì)算,比如計(jì)算 1 萬(wàn)位數(shù)的圓周率。很可能您永遠(yuǎn)也不需要在 Web 應(yīng)用程序上執(zhí)行這樣一個(gè)計(jì)算,在移動(dòng) Web 應(yīng)用程序上執(zhí)行這種計(jì)算的幾率則更小。但是,從遠(yuǎn)程資源檢索數(shù)據(jù)則相當(dāng)常見,這也是本文示例的關(guān)注點(diǎn)。
在這個(gè)示例中,您將從 eBay 檢索一個(gè) Daily Deals(每天都在變化的交易)列表。這個(gè)交易列表包含關(guān)于每筆交易的簡(jiǎn)短信息。更詳細(xì)的信息可以通過使用 eBay 的 Shopping API 獲取。當(dāng)用戶瀏覽這個(gè)交易列表選擇感興趣的商品時(shí),您將使用 Web Workers 來預(yù)取這個(gè)附加信息。要從您的 Web 應(yīng)用程序訪問所有這些 eBay 數(shù)據(jù),您需要通過使用一個(gè)泛型代理(generic proxy)來處理瀏覽器的同源策略。一個(gè)簡(jiǎn)單的 Java servlet 將用于這個(gè)代理,它包含在本文的代碼中,但不在這里單獨(dú)展示。相反,我們將把注意力集中在處理 Web Workers 的代碼上。清單 3 展示了這個(gè)交易應(yīng)用程序的基本 HTML 頁(yè)面。
清單 3. 交易應(yīng)用程序 HTML
- <!DOCTYPE HTML>
- <html>
- <head>
- <meta http-equiv="content-type" content="text/html; charset=UTF-8">
- <meta name = "viewport" content = "width = device-width">
- <title>Worker Deals</title>
- <script type="text/javascript" src="common.js"></script>
- </head>
- <body onload="loadDeals()">
- <h1>Deals</h1>
- <ol id="deals">
- </ol>
- <h2>More Deals</h2>
- <ul id="moreDeals">
- </ul>
- </body>
- </html>
可以看出,這是一段非常簡(jiǎn)單的 HTML;它只是一個(gè) shell。您使用 JavaScript 檢索數(shù)據(jù)并生成 UI。這是移動(dòng) Web 應(yīng)用程序的優(yōu)化設(shè)計(jì),因?yàn)樗试S將所有代碼和靜態(tài)標(biāo)記緩存到設(shè)備上,用戶只需等待來自服務(wù)器的數(shù)據(jù)。注意,在 清單 3 中,一旦那個(gè) body 加載,您就調(diào)用 loadDeals 函數(shù),在那里,您將加載 清單 4 中的應(yīng)用程序的初始數(shù)據(jù)。
清單 4. loadDeals 函數(shù)
- var deals = [];
- var sections = [];
- var dealDetails = {};
- var dealsUrl = "http://deals.ebay.com/feeds/xml";
- function loadDeals(){
- var xhr = new XMLHttpRequest();
- xhr.onreadystatechange = function(){
- if (this.readyState == 4 && this.status == 200){
- var i = 0;
- var j = 0;
- var dealsXml = this.responseXML.firstChild;
- var childNode = {};
- for (i=0; i< dealsXml.childNodes.length;i++){
- childNode = dealsXml.childNodes.item(i);
- switch(childNode.localName){
- case 'Item':
- deals.push(parseDeal(childNode));
- break;
- case "MoreDeals":
- for (j=0;j<childNode.childNodes.length;j++){
- var sectionXml= childNode.childNodes.item(j);
- if (sectionXml && sectionXml.hasChildNodes()){
- sections.push(parseSection(sectionXml));
- }
- }
- break;
- default:
- break;
- }
- }
- deals.forEach(function(deal){
- var entry = createDealUi(deal);
- $("deals").appendChild(entry);
- });
- loadDetails(deals);
- sections.forEach(function(section){
- var ui = createSectionUi(section);
- $("moreDeals").appendChild(ui);
- loadDetails(section.deals);
- });
- }
- };
- xhr.open("GET", "proxy?url=" + escape(dealsUrl));
- xhr.send(null);
- }
清單 4 展示了 loadDeals 函數(shù),以及應(yīng)用程序中使用的全局變量。您使用了一個(gè) deals 數(shù)組和一個(gè) sections 數(shù)組。它們是相關(guān)交易的附加組(例如,Deals under $10)。還有一個(gè)名為 dealDetails 的映射,其鍵是 Item IDs(來自于交易數(shù)據(jù)),其值是從 eBay Shopping API 獲取的詳細(xì)信息。
您首先調(diào)用一個(gè)代理,該代理又將調(diào)用 eBay Daily Deals REST API。這將把交易列表作為一個(gè) XML 文檔提供給您。您解析用于進(jìn)行 Ajax 調(diào)用的 XMLHttpRequest 對(duì)象的 onreadystatechange 函數(shù)中的文檔。您還使用其他兩個(gè)函數(shù),parseDeal 和 parseSection,來將 XML 節(jié)點(diǎn)解析為更易于使用的 JavaScript 對(duì)象。這些函數(shù)可以在可下載的代碼樣例(參見 下載 部分)中找到,但由于它們只是令人厭煩的 XML 解析函數(shù),因此我在這里沒有包括它們。最后,在解析了 XML 后,您還使用了另外兩個(gè)函數(shù),createDealUi 和createSectionUi,來修改 DOM。此時(shí),這個(gè) UI 如 圖 1 所示。
圖 1. Mobile Deals UI
如果您返回 清單 4,就會(huì)注意到在加載主交易之后,您對(duì)這些交易的每個(gè)部分都調(diào)用了 loadDetails 函數(shù)。在這個(gè)函數(shù)中,您通過使用 eBay Shopping API 加載每個(gè)交易的附加細(xì)節(jié) — 但前提是瀏覽器支持 Web Workers。清單 5 展示了 loadDetails 函數(shù)。
清單 5. 預(yù)取交易細(xì)節(jié)
- function loadDetails(items){
- if (!!window.Worker){
- items.forEach(function(item){
- var xmlStr = null;
- if (window.localStorage){
- xmlStr = localStorage.getItem(item.itemId);
- }
- if (xmlStr){
- var itemDetails = parseFromXml(xmlStr);
- dealDetails[itemDetails.id] = itemDetails;
- } else {
- var worker = new Worker("details.js");
- worker.onmessage = function(message){
- var responseXmlStr =message.data.responseXml;
- var itemDetails=parseFromXml(responseXmlStr);
- if (window.localStorage){
- localStorage.setItem(
- itemDetails.id, responseXmlStr);
- }
- dealDetails[itemDetails.id] = itemDetails;
- };
- worker.postMessage(item.itemId);
- }
- });
- }
- }
在 loadDetails 中,您首先檢查全局作用域(window 對(duì)象)中的 Worker 函數(shù)。如果該函數(shù)不在那里,那么無需做任何事。反之,您首先檢查 XML 的 localStorage 以獲取這個(gè)交易的細(xì)節(jié)。這是移動(dòng) Web 應(yīng)用程序常用的本地緩存策略,本系列第 2 部分(參見 參考資料 部分的鏈接)詳細(xì)介紹過這種策略。
如果 XML 位于本地,那么您在 parseFromXml 函數(shù)中解析它并將交易細(xì)節(jié)添加到 dealDetails 對(duì)象。反之,則衍生一個(gè) Web Worker 并使用 postMessage 向其發(fā)送 Item ID。當(dāng)這個(gè) Worker 檢索到數(shù)據(jù)并將數(shù)據(jù)發(fā)布回主線程后,您解析 XML,將結(jié)果添加到dealDetails,然后將 XML 存儲(chǔ)到 localStorage 中。清單 6 展示了這個(gè) Worker 腳本:details.js。
清單 6. 交易細(xì)節(jié) Worker 腳本
- importScripts("common.js");
- onmessage = function(message){
- var itemId = message.data;
- var xhr = new XMLHttpRequest();
- xhr.onreadystatechange = function(){
- if (this.readyState == 4 && this.status == 200){
- postMessage({responseXml: this.responseText});
- }
- };
- var urlStr = generateUrl(itemId);
- xhr.open("GET", "proxy?url=" + escape(urlStr));
- xhr.send(null);
- }
這個(gè) Worker 腳本非常簡(jiǎn)單。您使用 Ajax 調(diào)用代理,該代理又調(diào)用 eBay Shopping API。當(dāng)您收到來自代理的 XML 后,使用一個(gè) JavaScript 對(duì)象文字(object literal)將其發(fā)送回主線程。注意,即使您能夠使用來自一個(gè) Worker 的 XMLHttpRequest,但所有信息都將返回它的 responseText 屬性,而不是它的 responseXml 屬性。這是因?yàn)檫@個(gè) Worker 腳本范圍內(nèi)沒有 JavaScript DOM 解析器。注意,generateUrl 函數(shù)來自 common.js 文件(見 清單 7)。您使用 importScripts 函數(shù)導(dǎo)入 common.js 文件。
清單 7. Worker 導(dǎo)入的腳本
- function generateUrl(itemId){
- var appId = "YOUR APP ID GOES HERE";
- return "http://open.api.ebay.com/shopping?callname=GetSingleItem&"+
- "responseencoding=XML&appid=" + appId + "&siteid=0&version=665"
- +"&ItemID=" + itemId;
- }
現(xiàn)在,您已經(jīng)知道如何(為支持 Web Workers 的瀏覽器)填充交易細(xì)節(jié),我們返回 圖 1 研究一下如何在應(yīng)用程序中使用這種方法。注意,每筆交易旁邊都有一個(gè) Show Details 按鈕,單擊該按鈕修改這個(gè) UI,如 圖 2 所示。
圖 2. 顯示的交易細(xì)節(jié)
這個(gè) UI 將在您調(diào)用 showDetails 函數(shù)時(shí)顯示。清單 8 展示了這個(gè)函數(shù)。
清單 8. showDetails 函數(shù)
- function showDetails(id){
- var el = $(id);
- if (el.style.display == "block"){
- el.style.display = "none";
- } else {
- el.style.display = "block";
- if (!el.innerHTML){
- var details = dealDetails[id];
- if (details){
- var ui = createDetailUi(details);
- el.appendChild(ui);
- } else {
- var itemId = id;
- var xhr = new XMLHttpRequest();
- xhr.onreadystatechange = function(){
- if (this.readyState == 4 &&
- this.status == 200){
- var itemDetails =
- parseFromXml(this.responseText);
- if (window.localStorage){
- localStorage.setItem(
- itemDetails.id,
- this.responseText);
- }
- dealDetails[id] = itemDetails;
- var ui = createDetailUi(itemDetails);
- el.appendChild(ui);
- }
- };
- var urlStr = generateUrl(id);
- xhr.open("GET", "proxy?url=" + escape(urlStr));
- xhr.send(null);
- }
- }
- }
- }
您收到了即將顯示的交易的 ID 并切換是否顯示它。當(dāng)該函數(shù)第一次調(diào)用時(shí),它將檢查細(xì)節(jié)是否已經(jīng)存儲(chǔ)到 dealDetails 映射中。如果瀏覽器支持 Web Workers,那么這些細(xì)節(jié)已經(jīng)存儲(chǔ)且它的 UI 已經(jīng)創(chuàng)建并添加到 DOM 中。如果這些細(xì)節(jié)還沒有加載,或者,如果瀏覽器不支持 Workers,那么您需要執(zhí)行一個(gè) Ajax 調(diào)用來加載此數(shù)據(jù)。這就是這個(gè)應(yīng)用程序無論在有無 Workers 時(shí)都同樣能正常工作的原因。這意味著,如果 Workers 受到支持,那么數(shù)據(jù)就已被加載且 UI 將立即響應(yīng)。如果沒有 Workers,UI 仍將加載,只是需要花費(fèi)幾秒鐘時(shí)間。
結(jié)束語(yǔ)
對(duì)于 Web 開發(fā)人員來說,Web Workers 聽起來就像一種外來的新技術(shù)。但是,如本文所述,它們是非常實(shí)用的應(yīng)用程序。這對(duì)于移動(dòng) Web 應(yīng)用程序來說尤其正確。這些 Workers 可用于預(yù)取數(shù)據(jù)或執(zhí)行其他預(yù)先操作,從而提供一個(gè)更加實(shí)時(shí)的 UI。這對(duì)于需要通過網(wǎng)速可能較慢的網(wǎng)絡(luò)加載數(shù)據(jù)的移動(dòng) Web 應(yīng)用程序來說尤其正確。結(jié)合使用這種技術(shù)和緩存策略,您的應(yīng)用程序的快捷反應(yīng)將使您的用戶感到驚喜!