使用 Web Workers 提高 web 应用程序可用性

IBMdW 发布于 2011/11/03 21:35
阅读 542
收藏 4

简介: Web Workers,一个新的 JavaScript 编程模型,可以提高您 web 应用程序的交互性。有了它您就可以以一种多线程方法运行 JavaScript,而且可以在后台运行脚本而不依赖任何用户界面脚本。本文介绍了 Web Workers,并引导您了解一个实践示例,向您展示如何将 Web Workers 运用到您的 web 应用程序中。

简介

随着 Ajax 和 Web 2.0 应用程序的出现,终端用户被快速响应的 web 应用程序宠坏了。要让 web 应用程序响应得更快,瓶颈一定要解决。瓶颈包括 JavaScript 和后台 I/O 庞大的计算量,这需要从主 UI 显示流程中移除,交给 Web Workers 处理。

Web Workers 规范提供不依赖任何用户界面脚本在后台运行脚本的能力。长期运行脚本不会被响应单击或其他用户交互的脚本中断。Web Workers 允许执行长期任务,同时也不影响页面响应。

Web Workers 出现之前,JavaScript 是现代 web 应用程序的核心。JavaScript 和 DOM 本质上都是单线程的:在任何时间都只能执行一个 JavaScript 方法。即使您的计算机有 4 个内核,在进行长期计算时,也只有一个内核比较繁忙。例如,您在计算到达月球的最佳轨道时,您的浏览器不能渲染一个显示轨迹的动画,以及 — 同时 — 对用户事件作出响应(比如鼠标单击或键盘输入)。

Web Workers 打破了传统 JavaScript 的单线程模式,引入了多线程编程模式。一个 worker 是一个独立的线程。有多个任务需要处理的 web 应用程序不再需要逐个处理任务。反之,应用程序可以将任务分配给不同的 workers。

在本文中,您将学习 Web Workers API。一个实例引导您逐步使用 Web Workers 来构建一个 web 页面。

从下面的 下载表格 下载本文示例的源代码。

基本概念

Web Workers 的基本组成:

Worker
一个新线程,在后台运行,不会阻塞任何主用户界面脚本(作为后台脚本被调用)。Workers 是相对重量级的,不要大规模使用。

一个 worker 可以执行不少任务,包括并行计算、后台 I/O、以及客户端数据库操作。worker 不应该中断主 UI 或直接操作 DOM;它应该向主线程返回一个消息,并让主线程更新主 UI。

Subworker
在一个 worker 中创建的 worker。Subworkers 必须与父页面同根同源。subworkers 的 URI 是根据父 worker 的地址而不是自己页面地址确定的。
Shared worker

一个可以被多个页面通过多个连接所使用的 worker,共享 worker 和普通 worker 的工作方式略有不同,只有一小部分浏览器支持这一特性。

 

Web Workers API

本小节介绍 Web Workers API 的基本概念.

创建一个 worker

要创建一个新 worker,您只需要调用 worker 构造函数,worker 脚本 URL 是惟一参数。worker 创建完成的同时启动一个新线程(或者可能是一个新进程,根据您浏览的实现而定)。

worker 完成工作或者遇到一个错误时,您可以使用作业实例的 onmessageonerror 属性从 worker 获取通知。清单 1 是一个样例 worker。


清单 1. 样例 worker myWorker.js
// receive a message from the main JavaScript thread
onmessage = function(event) {
// do something in this worker
var info = event.data;
postMessage(info + “ from worker!”);
};

如果您运行清单 2 中的 JavaScript 代码,您将得到 “Hello World from worker” 。


清单 2. 主 JavaScript 线程中的 Worker

// create a new worker
var myWorker = new Worker("myWorker.js");
// send a message to start the worker
var info = “Hello World”;
myWorker.postMessage(info);
// receive a message from the worker
myWorker.onmessage = function (event) {
// do something when receiving a message from worker
alert(event.data);
};

终止 worker

一个 worker 是一个线程,是一个高资源消耗的 OS 级对象。当分配给 worker 的任务完成后,或者想要终止时,调用 worker 的 terminate 方法来终止正在运行的 worker。worker 线程或进程即可终止,没有机会完成它的操作以及自身清理。清单 3 是一个示例。


清单 3. 终止 myWorker 
myWorker.terminate();

错误处理

和普通 JavaScript 代码类似,运行时错误也可出现在运行的 worker 中。要处理这些错误,您需要为 worker 建立 onerror 处理程序,如果在脚本运行期间出现错误,将会调用该处理程序。要防止发生默认活动,worker 可以调用 worker 错误事件的 preventDefault() 方法。


清单 4. 为 myWorker 添加错误句柄

myWorker.onerror = function(event){
console.log(event.message);
console.log(event.filename);
console.log(event.lineno);
}

错误事件有以下 3 个字段,可能对调试有帮助:

  • message:一个人们可读的错误消息
  • filename:出现错误消息的脚本文件的名称
  • lineno:出现错误消息的脚本文件的行数

导入脚本和库

Worker 线程可以访问一个全局函数,importScripts(),该函数支持将脚本和数据库导入它们的作用域。它可以不接收参数,也可以接收多个要导入的资源的 URL 作为参数。


清单 5. 导入脚本

//import nothing
importScripts();
//import just graph.js
importScripts('graph.js');
//import two scripts
importScripts('graph.js', 'controller.js');

使用 Web Workers

本小节简要介绍 Web Workers 的一个实际用例。该示例包括显示一个含有多个基于 Dojo 的 Website Displayer 小部件的页面。这些小部件过去通常使用 iFrame 来显示一个网站。没有 Web Workers 时,您必须通过 Ajax 请求来获取小部件定义,然后在一个独立的 JavaScript 线程中显示它们。如果小部件定义含有大量数据,这个过程是非常慢的。

该示例创建一些 workers 来获取小部件定义。每个 worker 的任务是获取一个小部件定义,而且负责通知主 UI JavaScript 线程来显示它,这是一个较快的解决方案。

该示例用的是 Dojo 1.4。如果您想在您的浏览器中运行该示例,下载本文所使用的 Dojo 库(见 参考资料)和源代码(见 下载 )。图 1 展示了示例应用程序的结构。


图 1. Web Workers 应用程序
'Web Workers' 目录下目录结构的屏幕截图

在图 1 中:

  • lib 是一个 dojo 库。
  • /widgets/WebsiteDisplayer.js 是一个基于 dojo 的 Website Displayer 小部件实现。
  • /loadwidget/widgets/widgetDefinition[0....3] 是每个 Website Displayer 小部件的定义。
  • /loadwidget/Workers.js 是 worker 实现。
  • /loadwidget/XMLHttpRequest.js 是一个 js 库,含有一个创建 XMLHttpRequst 的方法。
  • /loadwidget/LoadWidget.html 是带有激活的 Web Workers 的演示的主页面,它将会是主 JavaScript 线程。
  • /loadwidget/LoadWidget-none-web-workers.html 是在没有 Web Workers 的情况下实现的主页面。

创建一个 Website Displayer 小部件

Website Displayer 小部件是一个非常简单的基于 Dojo-TitlePane-dijit 的小部件。它将显示一个规范化标题栏的 UI,如图 2 所示。


图 2. Website Displayer 小部件
 Website Displayer 小部件的屏幕截图,标题栏为 ‘Load Widget Use Web Workers’

清单 6 是 WebsiteDisplayer.js 的代码。


清单 6. WebsiteDisplayer.js 的内容
dojo.require("dijit._Widget");
dojo.require("dijit._Templated");
dojo.require("dijit.TitlePane");

dojo.declare("loadWidget.WebsiteDisplayer", [dijit.TitlePane], {
    title: "",
    url: "",
    postCreate: function() {
	var ifrm = dojo.create("iframe", {
           src: this.url,
	    style: "width:100%;height:20%;"
	});
	dojo.place(ifrm, this.domNode.children[1], "first");
	this.inherited(arguments);
	var contentFrame = this.domNode.children[1].children[0];
	if (contentFrame.attachEvent) {
	    contentFrame.attachEvent("onload",
		function() {
		    dojo.publish("frameEvent/loaded");
		}
	    );
	} else {
	    contentFrame.onload = function() {
		dojo.publish("frameEvent/loaded");
	    };
	}
    }
});


创建一个 worker

要实现 worker.js,导入一个全局 JavaScript 文件 XMLHttpRequest.js,其中含有全局方法 creatXMLHTTPRequest。该方法将返回一个 XMLHttpRequest 对象。

worker 主要将 XMLHttpRequest 发送到服务器端,然后检索小部件定义返回给主线程。清单 7 和清单 8 展示了一个示例。


清单 7. Worker.js 的内容
importScripts("XMLHttpRequest.js");

onmessage = function(event) {
  var xhr = creatXMLHTTPRequest();
  xhr.open('GET', 'widgets/widgetDefinition' + event.data + '.xml', true);
  xhr.send(null);
  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4) {
      if (xhr.status == 200 || xhr.status ==0) {
	 postMessage(xhr.responseText);
      } else {
	 throw xhr.status + xhr.responseText;
      }
    } 
  }
}

清单 8. widgetDefinition0.xml

<div dojoType="loadWidget.WebsiteDisplayer" title="This is Test Widget 0"
   url="http://www.yahoo.com" ></div>

创建主 web 页

主 web 页就是您进行这些操作的地方:创建几个 workers;发送消息到 workers 并启动 workers;从 workers 中检索消息;使用检索的消息操作主 UI。


清单 9. 主 web 页
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>
            Load widgets with Web Workers
        </title>
        <style type="text/css">
            @import "../lib/dijit/themes/soria/soria.css";
	    @import "../lib/dojo/resources/dojo.css";
	    @import "../lib/dojox/layout/resources/GridContainer.css";
            @import "../lib/dojox/layout/resources/DndGridContainer.css"
        </style>
        <script type="text/javascript" src="../lib/dojo/dojo.js" 
           djConfig="parseOnLoad: true,isDebug:true">
        </script>
        <script>
            dojo.require("dojo.parser");
            dojo.require("dojo.io.script");
	     dojo.require("dojox.layout.GridContainer");
            dojo.require("dijit.layout.LayoutContainer");
            dojo.require("dijit.TitlePane");
            dojo.require("dojox.layout.DragPane");
            dojo.registerModulePath("loadWidget", "../../loadWidget");
            dojo.require("loadWidget.WebsiteDisplayer");
	</script>
	<script type="text/javascript" language="javascript">
            var workersCount = 4;
            var haveLoadedCount = 0;
            var widgetCount = 4;
            var startTime = new Date().getTime();
            var endTime = null;
            var executeTime = 0;
            try {
                for (var i = 0; i < workersCount; i++) {
                    var loadWorker = new Worker("Worker.js");
                    loadWorker.postMessage(i);
                    loadWorker.onmessage = processReturnWidgetDefinition;
					loadWorker.onerror = handleWorkerError;
                }
            } catch(ex) {
                console.log(ex);
            }
			
            function processReturnWidgetDefinition(event) {
                var txt = document.createElement("p");
                txt.innerHTML = event.data;
                var div = document.getElementById("loadingDiv");
                div.appendChild(txt);
                haveLoadedCount++;
                if (haveLoadedCount == widgetCount) {
                    dojo.parser.parse();
                }
            }
			
	     function handleWorkerError(event){
		  console.log(event.message);
	     }
			
            dojo.subscribe("frameEvent/loaded", dojo.hitch(null, handelFrameLoaded));

            function handelFrameLoaded() {
                if (haveLoadedCount == widgetCount) {
                    endTime = new Date().getTime();
                    executeTime = endTime - startTime;
                    dojo.byId("loading").innerHTML = "Loading cost time:" + executeTime;
                }
            }
        </script>
    </head>
	
    <body class="soria">
       <div dojoType="dijit.TitlePane" title="Load widgets with Web Workers" 
          style="border: 2px solid black; padding: 10px;"
        id="main">
            <div id="loadingDiv">
                <div id="loading">
                    Widgets are loading......
                </div>
            </div>
        </div>
    </body>
	
</html>

将这个主页面嵌入到一个 web 应用程序中,然后运行它。结果如图 3 所示。

图 3. 使用 Web Workers 加载小部件
3 个小部件的屏幕截图,展示 3 个不同网站

想要查看使用 Web Workers 和不使用 Web Workers 的区别,分别运行 LoadWidget.html 和 LoadWidget-none-web-workers.html,然后查看结果。注意,在这里没有运行 Web Workers 的页面比运行 Web Workers 的页面完成得要快,这是因为代码样例处理的数据太少。实际上,节省的时间平衡了启动 worker 的成本。

使用 Web Workers 技巧

上面的示例只涉及 XMLHttpRequest 和计算;不是很大也不复杂。如果您让 worker 处理更复杂的任务,比如处理大量计算,它将会是一个功能强大的特性。在将这个很酷的技术运用到您的项目之前,了解一些使用技巧。

在 workers 中不能访问 DOM

为了安全,workers 不能直接对 HTML 进行操作。同一 DOM 上的多线程操作可能会引发线程安全问题。优势是您不再担忧 worker 实现中的多线程安全问题。

这在开发 worker 时有一些局限性,您不能在 worker 中调用 alert(),这是一个非常流行的调试 JavaScript 代码的方法。您也不能调用 document.getElementById(),因为它只能检索和返回变量(可能是字符串、数组、JSON 对象,等等)。

worker 中可用的对象

尽管 worker 不能访问 window 对象,但可以直接访问 navigator。您也可以在 navigator 对象中访问 appNameappVersionplatformuserAgent

location 对象可以以只读方式访问。您可以在 location 对象中获取 hostnameport

在 worker 中也支持 XMLHttpRequest,如本文示例所示。有了这一特性,您就可以将大量感兴趣的扩展添加到 worker 中。

此外还有:

  • importScripts() 方法(在同一个域上访问脚本文件)
  • JavaScript 对象,比如 ObjectArrayDateMathString
  • setTimeout()setInterval() 方法

postMessage 中携带的数据类型

postMessage 的使用十分频繁,因为它是主 JavaScript 线程的主要方法,用于和 workers 交互。然而,现在 postMessage 中携带的数据类型仅限于本地 JavaScript 类型,比如,Array、Date、Math、String、JSON 等等。结构复杂的自定义 JavaScript 对象不能被很好地支持。

范例代码:source.zip

原文出处: IBM developerWorks

加载中
返回顶部
顶部