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 应用程序来讲尤为正确。结合使用这种技术和缓存策略,您的应用程序的快捷反应将使您的用户感到惊喜!

相关文章
相关标签/搜索