第八章 Web Workers API
HTML5 Web workers可以让Web应用程序具备后台处理能力。它对多线程的支持性非常好,因此,使用了HTML5的JavaScript应用程序可以充分利用多核CPU带来的优势。
将耗时长的任务分配给自HTML5 Web Workers执行,可以避免弹出脚本运行缓慢的警告,见图8-1。图中显示的是JavaScript程序循坏持续几秒钟后弹出的警告窗口。
图8-1 Firefox中的脚本运行缓慢警告
尽管Web workers功能强大,但也不是万能的,有些事情它还做不到。例如,在WebWorkers中执行的脚本不能访问该页面的window对象(window.document),换句话说,Web workers不能直接访问Web页面和DOM API。 虽然Web workers不会导教阔览器UI停止响应,但是仍然会消耗CPU周期,导致系统反应速度变慢。
如果开发人员创建的Web应用程序需要执行一些后台数据处理,但又不希望这些数据处理任务影响Web页面本身的交互性,那么可以通过WebWorkers生成一个Web workers去执行数据处理任务,同时添加一个事件监听器去监听它发出的消息。
Web Workers的另一个用途是监听由后台服务器广播的新闻信息,收到后台服务器的消息后,将其显示在Web页面上。像这种与后台服务器对话的场景中,Web Workers可能会使用到WebSockets或Server-sent事件。
在本章中,我们将探讨如何使用WebSockets。首先,讨论WebSockets的工作机制和在编写本书时各浏览器的支持情况。接下来,将探讨如何使用API创建新的worker以及如何在worker和生成该worker的上下文之间进行通信。最后,我们会演示如何建立一个Web Workers应用。
8.1 HTML5 Web Workers
浏览器对于Web workers的支持情况各不相同,并且扔在持续更新发展中。如表8-1所示,在编写书本时,Web Workers已经得到了很多浏览器的支持。
表8-1 浏览器支持情况
浏览器 | 支持情况 |
Chrome | 版本3及以上支持 |
Firefox | 版本3.5及以上支持 |
Internet explorer | 暂不支持 |
Opera | 版本10.6及以上支持 |
Safari | 版本4及以上支持 |
8.2 使用HTML5 Web Workers API
本节,我们将从细节上探讨Web Workers API的使用。为便于说明。我们创建了一个简单的页面:echo.html。Web Workers的使用方格非常简单,只需创建一个Web Workers 对象,并传入希望执行的JavaScript文件即可。另外,在页面中再设置一个事件监听器,用来监听由WebWorker发来的消息和错误信息。如果想要在页面与WebWorkers之间建立通信,数据需通过postmessage函数传递。对于Web Workers JavaScript文件中的代码也是如此:必须通过设置事件处理程序来处理发来的消息和错误倍息,通过调用postMessage函数实现与页面的数据交互。
8.2.1 浏览器支持性检查
在调用Web WorkersAPI函数之前,首先要确认浏览器是否支持。如果不支持。可以提供一些备用信息,提醒用户使用最新的浏览器。代码清单8-1是可以用来测试浏览器支持性的代码。
代码清单8-1 检查浏览器支持情况的代码
function loadDemo() { if (typeof(Worker) !== "undefined") { document.getElementById("support").innerHTML = "Excellent! Your browser supports HTML5 Web Workers"; } }
这个示倒中,使用loadDemo函数来检测浏览器的支持情况,可在页面加载时调用该函数。调用typeof(Worker)会返回全局Window对象的Worker属性,如果浏览器不支持Web Workers API,返回结果将是“undefined”。这段代码在检测了浏览器支持性之后,会将检测结果反映到页面上。通过重新页画中预先定义好的support元素中的内容,为用户显示适当的提示, 如图8-2所示。
图8-2 检测HTML5 Web Workers支持性的示例
8.2.2 创建HTML5 Web Workers
Web Workers初始化时会接受一个JavaScript文件的URL地址,其中包含了供Worker执行的代码。这段代码会设置事件监听器,并与生成Worker的容器进行通信。JavaScript文件的URL可以是相对或者绝对路径,只要是同源(相同的协议、主机和端口)即可:
worker = new Worker("echoWorker.js");
8.2.3 多个JavaScript文件的加载与执行
对于由多个JavaScript文件组成的应用程序来说,可以通过包含<script>元素的方式。在页面加载的时候同步加载JavaScript文件。然而,由于WebWorkers没有访问document 对象的权限,所以在Worker中必须使用另外一种方法导人其他的JavaScript文件—importScripts:
importScripts("helper.js");
导人的JavaScript文件只会在某一个已有的Worker中加载和执行。多个脚本的导入同样也可以使用importScripts函数,它们会按顺序执行:
importScripts("helper.js", "anotherHelper.js");
8.2.4 与HTML5 Web Workers通信
Web worker一旦生成。就可以使用postMessage API传送和接收数据。postMessage API还支持跨框想和跨窗口通信。大多数JavaScript对象都可以通过postMessage发迭,但含有循环引用的除外。
假设要建立这样一个简单的Web Workers示倒:用户可以向Worker发送信息,然后Worker把信息返回来。也许这个示例并没有太大的实际意义,但是其理念是构建复杂应用时所必需的。图8-3显示的是这个示例的Web 页面和正在执行中的Web Workers。对应的程序代码列子本节的末尾。
图8-3 一个简单的使用HTML5 Web Workers的Web页面(本示例需在安装了Firebug的Firefox浏览器中测试,并请启用firebug控制台。)
为了能与Web Workers成功通信,除了要在主页(调用WebWorkers的页面)中添加代码以外,Worker JavaScript文件中也需要添加相应代码。
8.3 编写主页
为实现页面到Web Workers 的通信,我们将调用postMessage函数以传入所需数据。同时,我们将建立一个监听器。用来监听由WebWorkers发送到页面的消息和错误信息。
为建立主页和Web Workers之间的通信,首先在主页中添加对postMessage函数的调用,如下所示:
document.getElementById("helloButton").onclick = function() { worker.postMessage("Here's a message for you"); }
用户点击”post a Message”按钮后,相应信息会被发送给Web Workers。然后。我们将事件监听器添加到页面中,用来监听从WebWorkers发来的信息:
worker.addEventListener("message", messageHandler, true); function messageHandler(e) { // process message from worker }
编写HTML5 Web Workers JavaScript文件
在Web Workers JavaScript文件中,也需要添加类似的代码:必须添加事件监听器以监听发来的消息和错误信息,并且通过调用postMessage函数实现与页面之间的通信。
为了完成页面与WebWorkers 之间的通信功能,首先,我们添加代码调用postMessage 函数。例如,在messageHandler函数中可以添加如下代码:
function messageHandler(e) { postMessage("worker says: " + e.data + " too"); }
接下来,在Web Workers JavaScript文件中添加事件监听器,以处理从主页发来的信息:
addEventListener("message", messageHandler, true);
在这个示例中,接收到信息后会马上调用messagehandler函数以保证信息能及时返回。
8.3.1 处理错误
HTML5 Web Workers脚本中未处理的错误会引发Web Workers对象的错误事件。特性是在调用到了Web Workers的脚本时,对错误事件的监听就显得尤为重要。下面显示的是Web Workers JavaScript文件中的错误处理函数,他将错误记录在控制台上:
function errorHandler(e) { console.log(e.message, e); }
为了处理错误,还必须在主页上添加一个事件监听器:
worker.addEventListener("error", errorHandler, true);
8.3.2 HTML5 Web Workers
Web Workers不能自行终止。但能够被启用它们的页面所终止。开发人员都希望在不再需要Web Workers时回收其所占资源,比如当Web Workers通知主页它已执行完成的时候。另外,还有可能在用户干预的情况下取消一个运行耗时较长的任务,等等。这些情况下我们都需要终止Web Workers,可以调用terminate函数来实现。被终止的WebWorkers 将不再响应任何信息或者执行任何其他的计算。终止之后, Worker不能被重新启动,但可以使用同样的URL创建一个新的Worker。
worker.terminate();
8.3.3 HTML5 Web Workers的嵌套使用
Worker的API 能够在WebWorkers 脚本中嵌套使用,以创建于Worker:
var subWorker = new Worker("subWorker.js");
大量的worker
“如果递归生成了多个Worker都包含了同一个Javascript源文件,保守估计,你将看到一些有趣的结果,如下图所示。”
——peter
8.3.4 使用定时器
虽然HTML5 Web Workers不能访问window对象,但是它可以与属于window对象的Javascript定时器API 协作:
var t = setTimeout(postMessage, 2000, "delayed message");
8.3.5 示例代码
为完整起见。代码清单8-2与代码清单8-3中展示了上述页面及其Web Workers JavaScript文件的源代码。
代码清单8-2 使用HTML5 Web Workers的HTML页面代码
<!DOCTYPE html> <title>Simple HTML5 Web Workers Example</title> <link rel="stylesheet" href="styles.css"> <h1>Simple HTML5 Web Workers Example</h1> <p id="support">Your browser does not support HTML5 Web Workers.</p> <button id="stopButton" >Stop Task</button> <button id="helloButton" >Post a Message</button> <script> function stopWorker() { worker.terminate(); } function messageHandler(e) { console.log(e.data); } function errorHandler(e) { console.warn(e.message, e); } function loadDemo() { if (typeof(Worker) !== "undefined") { document.getElementById("support").innerHTML = "Excellent! Your browser supports HTML5 Web Workers"; worker = new Worker("echoWorker.js"); worker.addEventListener("message", messageHandler, true); worker.addEventListener("error", errorHandler, true); document.getElementById("helloButton").onclick = function() { worker.postMessage("Here's a message for you"); } document.getElementById("stopButton").onclick = stopWorker; } } window.addEventListener("load", loadDemo, true); </script>
代码清单8-3 HTML5 Web Worker JavaScript文件
function messageHandler(e) { postMessage("worker says: " + e.data + " too"); } addEventListener("message", messageHandler, true);
8.4 构建HTML5 Web Worker应用
刚才我们讨论的是Web Workers API的各种用法。Web Workers API到底有多强大?现在我们通过建立一个HTML5 Web Workers应用来演示。我们将在这个应用中实现一个带有图像模糊过滤器的Web 页面,过滤动作将由多个Web Workers并行执行。图8-4显示了该应用程序的执行效果。
图8-4 基于HTML5 Web Worker的图像模糊过滤器页面
这个应用程序首先从canvas向多个Web Worker(可以指定数量)发送图像数据,然后Web Workers使用box-blur过滤对这些图像数据进行处理。处理时间大约是几秒钟,取决于图像大小和可用的计算资源(即使电脑配备高速CPU. 也可能因为巳加载了其他进程,导致需要更多的时钟周期来执行JavaScript)。
Web Workers承担了所有繁重的任务,因此不存在弹出脚本运行缓慢警告的风险,也不需要手动将任务分割成多份执行——在不能使用Web Workers的情况下,这是必须得考虑的事情。
8.4.1 编写blur.js辅助脚本
如代码清单8-4所示,在blur.js 应用脚本中,我们可以直接使用模糊过滤器,使之一直循环运行直到将输入数据全部处理完。
码清单8-4 blur.js文件中JavaScript box-blur过滤器的实现
function inRange(i, width, height) { return ((i>=0) && (i < width*height*4)); } function averageNeighbors(imageData, width, height, i) { var v = imageData[i]; // cardinal directions var north = inRange(i-width*4, width, height) ? imageData[i-width*4] : v; var south = inRange(i+width*4, width, height) ? imageData[i+width*4] : v; var west = inRange(i-4, width, height) ? imageData[i-4] : v; var east = inRange(i+4, width, height) ? imageData[i+4] : v; // diagonal neighbors var ne = inRange(i-width*4+4, width, height) ? imageData[i-width*4+4] : v; var nw = inRange(i-width*4-4, width, height) ? imageData[i-width*4-4] : v; var se = inRange(i+width*4+4, width, height) ? imageData[i+width*4+4] : v; var sw = inRange(i+width*4-4, width, height) ? imageData[i+width*4-4] : v; // average var newVal = Math.floor((north + south + east + west + se + sw + ne + nw + v)/9); if (isNaN(newVal)) { sendStatus("bad value " + i + " for height " + height); throw new Error("NaN"); } return newVal; } function boxBlur(imageData, width, height) { var data = []; var val = 0; for (var i=0; i<width*height*4; i++) { val = averageNeighbors(imageData, width, height, i); data[i] = val; } return data; }
简而言之。该算法通过求附近像素值的平均值对图像进行模糊处理。对于一个有着百万级像素的大图像而言,需要执行相当长的时间。在UI线程中,运行这样的循环是极不合适的。即使不弹出脚本运行缓慢的警告,在循环终止之前,用户界面也无法响应用户的其他操作。不过,利用Web Workers在后台执行计算倒是可以接受的。
8.4.2 编写blur.html应用页面
代码清单8-5是调用Web Workers的HTML页面代码。为了方便说明,撞该HTML示例也化繁为简了。我们的目的不在于建立漂亮的界面,而是通过搭建一个简洁的框架,演示如何控制WebWorkers并实际运行。应用程序的页面嵌入了canvas元素来显示输入的图像。页面上还有一组按钮,包括开始模糊(Blur、停止模糊(Stop Workers) ,重置图像(Reload) 和指定生成的Worker数量(number of workers)。
代码清单8-5 blur.html页面代码
<!DOCTYPE html> <title>HTML5 Web Workers</title> <link rel="stylesheet" href = "styles.css"> <h1>HTML5 Web Workers</h1> <p id="status">Your browser does not support HTML5 Web Workers.</p> <button id="startBlurButton" disabled>Blur</button> <button id="stopButton" disabled>Stop Workers</button> <button onclick="document.location = document.location;">Reload</button> <label for="workerCount">Number of Workers</label> <select id="workerCount"> <option>1</option> <option selected>2</option> <option>4</option> <option>8</option> <option>16</option> </select> <div id="imageContainer"></div> <div id="logOutput"></div>
接下来在blur.html中摇加添加创建Worker的代码。我们通过传递IavaScript文件的URL来实例化每个Worker对象。每个实例化的worker运行的代码相同,分别负责处理输入图像的不同部分:
function initWorker(src) { var worker = new Worker(src); worker.addEventListener("message", messageHandler, true); worker.addEventListener("error", errorHandler, true); return worker; }
最后为blur.html文件增加错误处理的代码。这样,在worker发生错误的时候,页面不会毫无反应,而是显示相应的错误信息。我们的示例应该不会遇到任何问题,但在实际开发中监听错误事件是个好习惯,而且对调试工作的意义也很大。
function errorHandler(e) { log("error: " + e.message); }
8.4.3 编写blurworker.js
现在,我们将worker用来与页面同学的代码添加到blurworker.js文件中(如代码清单8-6所示)。Web workers完成运算即可使用postmessage通知页面。我们将利用这个信息更新主页上的图像。Web workers创建完成后,会等待包含图像数据和开始模糊指令的消息。此类消息是一个Javascript对象,其中包含有消息类型和数值数组形式的图像数据。
代码清单8-6 在blurworker.js文件中发送和处理图像数据
function sendStatus(statusText) { postMessage({"type" : "status", "statusText" : statusText}); } function messageHandler(e) { var messageType = e.data.type; switch (messageType) { case ("blur"): sendStatus("Worker started blur on data in range: " + e.data.startX + "-" + (e.data.startX+e.data.width)); var imageData = e.data.imageData; imageData = boxBlur(imageData, e.data.width, e.data.height, e.data.startX); postMessage({"type" : "progress", "imageData" : imageData, "width" : e.data.width, "height" : e.data.height, "startX" : e.data.startX }); sendStatus("Finished blur on data in range: " + e.data.startX + "-" + (e.data.width+e.data.startX)); break; default: sendStatus("Worker got message: " + e.data); } } addEventListener("message", messageHandler, true);
8.4.4 与Web Worker通信
在blurworker.js文件中,我们可以通过给Worker发送一些代表模糊任务的数据和参数来使用Worker,方法是使用postMessage函数发送一个JavaScript对象。这个JavaScript对象包含了worker负责处理的RGBA图像数据阵列、图像的尺寸和像索范围。每个Worker根据接收到的信息分别对图像的不同部分进行处理:
function sendBlurTask(worker, i, chunkWidth) { var chunkHeight = image.height; var chunkStartX = i * chunkWidth; var chunkStartY = 0; var data = ctx.getImageData(chunkStartX, chunkStartY, chunkWidth, chunkHeight).data; worker.postMessage({ 'type' : 'blur', 'imageData' : data, 'width' : chunkWidth, 'height' : chunkHeight, 'startX' : chunkStartX }); }
CANVAS图像数据
“postMessage可以对imagedata对象进行高效序列化,以便通过canvasAPI 使用。不过一些支持Worker和postMessage API的浏览器也许还不能支持postMessage的这种扩展的序列化能力。例如, Firefox 3.5不能通过postMessage传送imageData对象,不过未来版本可能会提供此支持。
因此,在本章中介绍的图像处理示例中,以传送imageData . data (数据序列化方式向JavaScript数组一样)的方式代替传送imageData对象本身。在Web workers执行运算任务时,它们同时将其状态和结果返回到页面上。代码清单8-6显示了数据如何在经模糊过滤器处理之后从Worker传送到页画上。同样地,消息包含了一个JavaScript 对象,这个对象中含有图像数据和标记处理范围的坐标信息。”
——Frank
HTML 页面上的消息处理程序接收上述数据,并用新的像素值更新canvas。处理后的图像数据到达时, HTML页面即时显示结果。现在,我们创建了一个可以处理图像的示例应用,并且具有利用多核CPU的潜在优势。此外,Web Workers执行时,程序也不会锁定用户界面。不会让用户界面停止响应。应用执行时的状态见图8-5。
8.4.5 运行程序
为了让示例运行起来。blur.html页面需要部署在一个Web服务器中{例如Apache或者时python的SimpleHTTPserver) 。以Python的SimpleHTTPserver为例,其部署步骤如下。
- 安装时Python。
- 打开包含示例文件(blur.html)的目录。
(3) 输入命令启动Python:
Python-m simplehttpserver 9999
- 打开浏览器,输入http://localhost:9999/blur.btml。应谁就可以看到图8-5所示的页面了。
8.4.6 示例代码
为完整起见,代码清单8-7、代码清单8-8和代码清单8-9列出了示例的完整代码。
代码清单8-7 blur.html文件的内容
<!DOCTYPE html> <title>HTML5 Web Workers</title> <link rel="stylesheet" href = "styles.css"> <h1>HTML5 Web Workers</h1> <p id="status">Your browser does not support HTML5 Web Workers.</p> <button id="startBlurButton" disabled>Blur</button> <button id="stopButton" disabled>Stop Workers</button> <button onclick="document.location = document.location;">Reload</button> <label for="workerCount">Number of Workers</label> <select id="workerCount"> <option>1</option> <option selected>2</option> <option>4</option> <option>8</option> <option>16</option> </select> <div id="imageContainer"></div> <div id="logOutput"></div> <script> var imageURL = "example2.png"; var image; var ctx; var workers = []; function log(s) { var logOutput = document.getElementById("logOutput"); logOutput.innerHTML = s + "<br>" + logOutput.innerHTML; } function setRunningState(p) { // while running, the stop button is enabled and the start button is not document.getElementById("startBlurButton").disabled = p; document.getElementById("stopButton").disabled = !p; } function initWorker(src) { var worker = new Worker(src); worker.addEventListener("message", messageHandler, true); worker.addEventListener("error", errorHandler, true); return worker; } function startBlur() { var workerCount = parseInt(document.getElementById("workerCount").value); var width = image.width/workerCount; for (var i=0; i<workerCount; i++) { var worker = initWorker("blurWorker.js"); worker.index = i; worker.width = width; workers[i] = worker; sendBlurTask(worker, i, width); } setRunningState(true); } function sendBlurTask(worker, i, chunkWidth) { var chunkHeight = image.height; var chunkStartX = i * chunkWidth; var chunkStartY = 0; var data = ctx.getImageData(chunkStartX, chunkStartY, chunkWidth, chunkHeight).data; worker.postMessage({ 'type' : 'blur', 'imageData' : data, 'width' : chunkWidth, 'height' : chunkHeight, 'startX' : chunkStartX }); } function stopBlur() { for (var i=0; i<workers.length; i++) { workers[i].terminate(); } setRunningState(false); } function messageHandler(e) { var messageType = e.data.type; switch (messageType) { case ("status"): log(e.data.statusText); break; case ("progress"): var imageData = ctx.createImageData(e.data.width, e.data.height); for (var i = 0; i<imageData.data.length; i++) { var val = e.data.imageData[i]; if (val === null || val > 255 || val < 0) { log("illegal value: " + val + " at " + i); return; } imageData.data[i] = val; } ctx.putImageData(imageData, e.data.startX, 0); // blur the same tile again sendBlurTask(e.target, e.target.index, e.target.width); break; default: break; } } function errorHandler(e) { log("error: " + e.message); } function loadImageData(url) { var canvas = document.createElement('canvas'); ctx = canvas.getContext('2d'); image = new Image(); image.src = url; document.getElementById("imageContainer").appendChild(canvas); image.onload = function(){ canvas.width = image.width; canvas.height = image.height; ctx.drawImage(image, 0, 0); window.imgdata = ctx.getImageData(0, 0, image.width, image.height); n = ctx.createImageData(image.width, image.height); setRunningState(false); log("Image loaded: " + image.width + "x" + image.height + " pixels"); }; } function loadDemo() { log("Loading image data"); if (typeof(Worker) !== "undefined") { document.getElementById("status").innerHTML = "Your browser supports HTML5 Web Workers"; document.getElementById("stopButton").onclick = stopBlur; document.getElementById("startBlurButton").onclick = startBlur; loadImageData(imageURL); document.getElementById("startBlurButton").disabled = true; document.getElementById("stopButton").disabled = true; } } window.addEventListener("load", loadDemo, true); </script>
代码清单8-8 blurworker.js文件内容
importScripts("blur.js"); function sendStatus(statusText) { postMessage({"type" : "status", "statusText" : statusText} ); } function messageHandler(e) { var messageType = e.data.type; switch (messageType) { case ("blur"): sendStatus("Worker started blur on data in range: " + e.data.startX + "-" + (e.data.startX+e.data.width)); var imageData = e.data.imageData; imageData = boxBlur(imageData, e.data.width, e.data.height, e.data.startX); postMessage({"type" : "progress", "imageData" : imageData, "width" : e.data.width, "height" : e.data.height, "startX" : e.data.startX }); sendStatus("Finished blur on data in range: " + e.data.startX + "-" + (e.data.width+e.data.startX)); break; default: sendStatus("Worker got message: " + e.data); } } addEventListener("message", messageHandler, true);
代码清单8-9 blur.js文件内容
function inRange(i, width, height) { return ((i>=0) && (i < width*height*4)); } function averageNeighbors(imageData, width, height, i) { var v = imageData[i]; // cardinal directions var north = inRange(i-width*4, width, height) ? imageData[i-width*4] : v; var south = inRange(i+width*4, width, height) ? imageData[i+width*4] : v; var west = inRange(i-4, width, height) ? imageData[i-4] : v; var east = inRange(i+4, width, height) ? imageData[i+4] : v; // diagonal neighbors var ne = inRange(i-width*4+4, width, height) ? imageData[i-width*4+4] : v; var nw = inRange(i-width*4-4, width, height) ? imageData[i-width*4-4] : v; var se = inRange(i+width*4+4, width, height) ? imageData[i+width*4+4] : v; var sw = inRange(i+width*4-4, width, height) ? imageData[i+width*4-4] : v; // average var newVal = Math.floor((north + south + east + west + se + sw + ne + nw + v)/9); if (isNaN(newVal)) { sendStatus("bad value " + i + " for height " + height); throw new Error("NaN"); } return newVal; } function boxBlur(imageData, width, height) { var data = []; var val = 0; for (var i=0; i<width*height*4; i++) { val = averageNeighbors(imageData, width, height, i); data[i] = val; } return data; }
8.5 小结
本章,我们讨论如何使用WebWorkers搭建具有后台处理能力的Web应用程序。首先,我们了解了WebWorkers的工作机制,说明了编写本书时各浏览器的支持情况。然后,我们讨论了如何使用API创建worker,以及如何在一个Worker与生成它的上下文之间通信。最后,我们演示了如何使用Web workers构建Web应用程序。下一章中。我们将演示如何通过HTML5 保存数据的本地副本,以此来减少应用的网络开销。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论