今天真正要说的是 -- JavaScript 中的 worker 们:
在 HTML5 规范中提出了工作线程(Web Worker
)的概念,允许开发人员编写能够脱离主线程、长时间运行而不被用户所中断的后台程序,去执行事务或者逻辑,并同时保证页面对用户的及时响应。
Web Worker
又分为 Dedicated Worker
和 SharedWorker
。
随后 ServiceWorker
也加入进来,用于更好的控制缓存和处理请求,让离线应用成为可能。
先来复习一下基础知识:
传统页面中(HTML5 之前)的 JavaScript 的运行都是以单线程的方式工作的,虽然有多种方式实现了对多线程的模拟(例如:JavaScript 中的 setinterval 方法,setTimeout 方法等),但是在本质上程序的运行仍然是由 JavaScript 引擎以单线程调度的方式进行的。
为了避免多线程 UI 操作的冲突(如线程1要求浏览器删除DOM节点,线程2却希望修改这个节点的某些样式风格),JS 将处理用户交互、定时执行、操作DOM树/CSS样式树等,都放在了 JS 引擎的一个线程中执行。
从 2008 年 W3C 制定出第一个 HTML5 草案开始,HTML5 承载了越来越多崭新的特性和功能。它不但强化了 Web 系统或网页的表现性能,而且还增加了对本地数据库等 Web 应用功能的支持。
随之而来的,还有上面提到的几种 worker
,首先解决的就是多线程的问题。
那么,来看看解决线程问题的东西为什么叫 worker
,这来源于一种设计模式:
Master-Worker模式是常用的并行设计模式。其核心思想是:系统有两个进程协同工作:Master进程和Worker进程。Master进程负责接收和分配任务,Worker进程负责处理子任务。当各个Worker进程将子任务处理完后,将结果返回给Master进程,由Master进行归纳和汇总,从而得到系统结果
Node 的内置模块 cluster,可以通过一个主进程管理若干子进程的方式来实现集群的功能
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;if (cluster.isMaster) { //主进程 console.log(`Master ${process.pid} is running`); //分配子任务
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
} else { //子进程 //应用逻辑根本不需要知道自己是在集群还是单边
//每个HTTP server都能监听到同一个端口
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world\n');
}).listen(8000); console.log(`Worker ${process.pid} started`);
}
运行 node server.js
后,输出:
Master 3596 is running
Worker 4324 started
Worker 4520 started
Worker 6056 started
Worker 5644 started
在 HTML5 中,
Web Worker
的出现使得在 Web 页面中进行多线程编程成为可能
HTML5 中的多线程是这样一种机制:它允许在 Web 程序中并发执行多个 JavaScript 脚本,每个脚本执行流都称为一个线程,彼此间上下文互相独立,并且由浏览器中的 JavaScript 引擎负责管理
HTML5 规范列出了 Web Worker
的三大主要特征:
HTML5 中的 Web Worker
可以分为两种不同线程类型,一个是专用线程 Dedicated Worker
,一个是共享线程 Shared Worker
。
专用线程是指标准 worker,一个专用 worker 仅仅能被生成它的脚本所使用
也就是说,所谓的专用线程(dedicated worker)并没有一个显示的DedicatedWorker
构造函数,其实指的就是普通的Worker
构造函数。
? 在解释概念前,先来看一个呗儿简单的小栗子:
//myWorker.jsself.onmessage = function(event) {
var info = event.data;
self.postMessage(info + " from worker!");
};
//主页面<input type="text" name="wkInput1" />
<button id="btn1">test!</button><script>
if (window.Worker) {
const myWorker = new Worker("myWorker.js");
myWorker.onmessage = function (event) {
alert(event.data);
};
const btn = document.querySelector('#btn1');
btn.addEventListener('click', e=>{
const ipt = document.querySelector('[name=wkInput1]');
const info = "hello " + ipt.value;
myWorker.postMessage(info);
});
}
</script>
很显然,运行的效果无非是点击按钮后弹出包含文本框内容的字符串。
例子很简单,但携带的关键信息还算丰富,那么结合规范中的一些定义来看一看上面的代码:
首先是专用 worker 在运行的过程中,会隐式的使用一个MessagePort
对象,其接口定义如下:
interface MessagePort {
void postMessage(message, optional transfer = []);
attribute onmessage;
void start();
void close();
attribute onmessageerror;
};
我们把最重要的两个成员放在了前面,一个是postMessage()
方法,另一个是onmessage
属性。
postMessage()
方法用来发送数据:第一个参数除了可以发送字符串,还可以发送 JS 对象(有的浏览器需要JSON.stringify()
);可选的第二个参数可用来发送 ArrayBuffer 对象数据(一种二进制数组,配合XHR、File API、Canvas等读取字节流数据用)onmessage
属性应被指定一个事件处理函数,用于接收传递过来的消息;也可以选择使用 addEventListener 方法,其实现方式和作用和 onmessage 相同然后来看看简化后的 Worker
的定义:
interface AbstractWorker {
attribute onerror;
};
interface Worker {
Constructor(scriptURL, optional workerOptions);
void terminate(); void postMessage(message, optional transfer = []);
attribute onmessage;
attribute onmessageerror;
};Worker implements AbstractWorker;
AbstractWorker
接口,也就是说有一个onerror
回调用来管理错误;myWorker.onerror = function(event){
console.log(event.message);
console.log(event.filename);
console.log(event.lineno);
}
MessagePort
接口,可以 postMessage/onmessage
;terminate()
方法去终止该线程通过workerOptions 中的选项可以支持 es6 模块化等,这里不展开论述
至此,已经可以理解“主页面”中的各种定义和调用行为了;而"myWorker.js"中的self
又是怎样的呢,继续来看看相关定义:
interface WorkerGlobalScope {
readonly attribute self; //WorkerGlobalScope
readonly attribute location;
readonly attribute navigator;
void importScripts(urls); attribute onerror;
attribute onlanguagechange;
attribute onoffline;
attribute ononline;
attribute onrejectionhandled;
attribute onunhandledrejection;
};
interface DedicatedWorkerGlobalScope {
readonly attribute name; void postMessage(
message,
optional transfer = []
); void close(); attribute onmessage;
attribute onmessageerror;
};
专用 worker 实现了以上两个接口,可知:
worker
中的全局对象就是其本身WorkerGlobalScope
的 self
只读属性来获得这个对象本身的引用MessagePort
接口方法。看起来很简单,两边都可以 postMessage/onmessage
,就可以愉快的通信了。
除了上述这些,其他的一些要点包括:
在现代浏览器和移动端上,可以说专用 worker 已经被支持的不错了:
共享线程指的是一个可以被多个页面通过多个连接所使用的 worker
? 还是先看一个栗子:
//wk.jsvar arr = [];self.onconnect = function(e) {
var port = e.ports[0];
port.postMessage('hello from worker!'); port.onmessage = function(evt) {
var val = evt.data;
if (!~arr.indexOf(val)) {
arr.push(val);
}
port.postMessage(arr.toString());
}
}
<!DOCTYPE html>
<html><body>
page 1
<script>
if (window.SharedWorker) {
var wk = new SharedWorker('wk.js');
wk.port.onmessage = function(e) {
console.log(e.data);
}
wk.port.postMessage(1);
}//输出
//hello from worker!
//1
</script>
</body></html>
<!DOCTYPE html>
<html><body>
page 2
<script>
if (window.SharedWorker) {
var wk = new SharedWorker('wk.js');
wk.port.onmessage = function(e) {
console.log(e.data);
}
wk.port.postMessage(2);
}
//输出
//hello from worker!
//1,2
</script>
</body></html>
运行效果也不难理解,引用共享 worker 的两个同域的页面,共享了其中的 arr 数组。也就是说,专用 worker 一旦被某个页面引用,该页面就拥有了一个独立的子线程上下文;与之不同的是,某个共享 worker 脚本文件如果被若干页面(要求是同源的)引用,则这些页面会共享该 worker 的上下文,拥有共同影响的变量等。
interface SharedWorker {
Constructor(
scriptURL,
optional (DOMString or WorkerOptions) options
);
readonly attribute port;
};SharedWorker implements AbstractWorker;
另一个非常大的区别在于,前面也提到过,与一个专用 worker 通信,对MessagePort
的实现是隐式进行的(直接在 worker 上进行postMessage/onmessage
);而共享 worker 必须通过端口(MessagePort
类型的worker.port
)对象进行。
此外的几个注意点:
var wk = new SharedWorker('wk.js', 'foo');
//or
var wk = new SharedWorker('wk.js', {name: 'foo'});
interface SharedWorkerGlobalScope {
readonly attribute name;
void close();
attribute onconnect;
};
self.name
获得self.onconnect = function(e) {
var port = e.ports[0];
port.postMessage('hello from worker!');
//...
}
var wk = new SharedWorker('wk.js');
wk.port.onmessage = function(e) {
console.log(e.data);
}//-->
wk.port.addEventListener('message', function(e) {
console.log(e.data);
});
wk.port.start();
addEventListener
代替onmessge
,则需要额外调用 start()
方法才能建立连接移动端尚不支持、IE11/Edge也没戏;测试时 Mac 端的 chrome/firefox 也是状况频频无法成功,最后在 chrome@win10 以及 opera@mac 才可以
Service Worker 基于 Web Worker 的事件驱动,提供了用来管理安装、版本、升级的一整套系统。
专用 worker 或共享 worker 专注于解决 “耗时的 JS 执行影响 UI 响应” 的问题, -- 一是后台运行 JS,不影响主线程;二是使用postMessage()/onmessage
消息机制实现了并行。
而 service worker 则是为解决 “因为依赖并容易丢失网络连接,从而造成 Web App 的用户体验不如 Native App” 的问题而提供的一系列技术集合;它比 web worker 独立得更彻底,可以在页面没有打开的时候就运行。
并且相比于已经被废弃的 Application Cache 缓存技术:
<html manifest="appcache.manifest">
...
</html>
CACHE MANIFEST
# appcache.manifest text file, version: 0.517NETWORK:
#CACHE:
assets/loading.gif
assets/wei_shop_bk1.jpg
assets/wei_shop_bk2.jpg
assets/wei_ios/icons.png
assets/wei_ios/icon_addr.png
assets/wei_ios/icon_tel.pngNETWORK:
scripts/wei_webapp.js
styles/meishi_wei.css
service worker 拥有更精细、更完整的控制;作为一个页面与服务器之间代理中间层,service worker 可以捕获它所负责的页面的请求,并返回相应资源,这使离线 web 应用成为了可能。
? 一如既往的先看一个直观的小栗子:
<!--http://localhost:8000/service.html--><h1>hello service!</h1><img src="deer.png" /><script>
if (navigator.serviceWorker) {
window.onload = function() {
navigator.serviceWorker.register(
'myService.js',
{scope: '/'}
).then(registration=>{
console.log('SW register OK with scope: ', registration.scope); registration.onmessage = function(e) {
console.log(e.data)
}
}).catch(err=>{
console.log('SW register failed: ', err);
});
}
// SW register OK with scope: http://localhost:8000/
}
</script>
//myService.jsvar CACHE_NAME = 'my-site-cache-v1';var urlsToCache = [
'/styles/main.css',
'/script/main.js'
];self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(urlsToCache);
})
);
});self.addEventListener('fetch', function(event) {
const url = new URL(event.request.url);
if (url.pathname == '/deer.png') {
event.respondWith(
fetch('/horse.jpg').catch(ex=>console.log(ex))
);
} else {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
return fetch(event.request);
}
)
);
}
});
结合这个简单的示例,来梳理一下其中反映出的信息:
和其他两种 worker 不同的是,service worker 中的各项技术广泛地利用了 Promise
Promises 是一种非常适用于异步操作的机制,一个操作依赖于另一个操作的成功执行。这也成为了 service worker 的通用工作机制
Response 的构造函数允许创建一个自定义的响应对象:
new Response('<p>Hello from service worker!</p>', {
headers: { 'Content-Type': 'text/html' }
})
但更常见的是:通过其他的 API 操作返回了一个 Response 对象,例如一个 service worker 的 event.respondWith
,或者一个简单的 fetch()
在 service worker 中使用 Response 对象时,通常还要通过 response.clone()
来取得一个克隆使用;这样做的原因是,一个 response 是一个流,只用被消费一次,而我们想让浏览器、缓存等多次操作这个响应,就需要 clone 出不同的对象来;对于 Request 请求对象的使用也是类似的道理
在 service worker 中无法使用传统的 XMLHttpRequest
,只能使用 fetch
;而后者的优势正在于,可以使用 Request
和 Response
对象
每次网络请求,都会触发对应的 service worker 中的 fetch
事件
在我们的例子中,页面上有一个指向 deer.png
的图片元素,最后却由 fetch
事件回调拦截并返回了 /horse.jpg
,实现了指鹿为马的自定义资源指向
self.addEventListener('fetch', function(event) {
const url = new URL(event.request.url);
if (url.pathname == '/deer.png') {
event.respondWith(
fetch('/horse.jpg').catch(ex=>console.log(ex))
);
}
});
在 service worker 规范中包含了原生的缓存能力,用以替代已被废弃的 Application Cache 标准。
Cache
API 提供了一个网络请求的持久层,并可以使用 match 操作查询这些请求。
在 service worker 中最主要用到 Cache 的地方,还是在上面提到的 fetch
事件回调中。
通过使用本地缓存中的资源,不但能省去对网络的昂贵访问,更有了在 离线、掉线、网络不佳 等情况下维持应用可用的能力。
相关的定义如下:
interface Cache {
match(request, optional cacheQueryOptions);
matchAll(optional request, optional cacheQueryOptions);
add(request);
addAll(requests);
put(request, response);
delete(request, optional cacheQueryOptions);
keys(optional request, optional cacheQueryOptions);
};
同时 service worker 也可以用 self.caches
来取得缓存:
interface WindowOrWorkerGlobalScope {
readonly attribute caches; //CacheStorage
};interface CacheStorage {
match(request, optional options); //Promise
has(cacheName); //Promise
open(cacheName); //Promise
delete(cacheName); //Promise
keys(); //Promise
};
反映在例子中就是(版本的部分会在稍后提到):
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(urlsToCache);
})
);
});self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
return fetch(event.request);
}
)
);
});
将 server worker 的生命周期设计成这样,其目的在于:
重要的比如:
install
事件:使用register()
注册时会触发activate
事件:register()
注册时也会触发activate事件具体到各个事件的回调中,event 参数对应的类型如下:
事件名称 | 接口 |
---|---|
install | ExtendableEvent |
activate | ExtendableEvent |
fetch | FetchEvent |
message | ExtendableMessageEvent |
messageerror | MessageEvent |
其中有代表性的两个事件的定义如下:
interface ExtendableEvent {
void waitUntil(promiseFunc);
};interface FetchEvent {
readonly attribute request;
readonly attribute clientId;
readonly attribute reservedClientId;
readonly attribute targetClientId; void respondWith(promiseFunc);
};
所以,才可以在例子中调用 event.waitUntil()
和 event.respondWith()
:
self.addEventListener('install', function(event) {
event.waitUntil( //用一个 promise 检查安装是否成功
//...
);
});self.addEventListener('fetch', function(event) {
event.respondWith( // 返回符合期望的 Response 对象
//...
);
不同于其他两种 worker 的是,service worker 不再用 new 来实例化,而是直接通过 navigator.serviceWorker
取得
navigator.serviceWorker
实际上实现了 ServiceWorkerContainer
接口:
interface ServiceWorkerContainer {
readonly attribute controller;
readonly attribute ready; //promise register(scriptURL, optional registrationOptions); getRegistration(optional clientURL = "");
getRegistrations(); void startMessages(); attribute oncontrollerchange;
attribute onmessage; // event.source is a worker
attribute onmessageerror;
};
比如我们在例子中的主页面所做的:
navigator.serviceWorker.register(
'myService.js',
{scope: '/'}
).then().catch()
scope 参数是选填的,可以被用来指定想让 service worker 控制的内容的子目录;service worker 能控制的最大权限层级就是其所在的目录
运行 register()
方法成功的话,会在 navigator.serviceWorker
的 Promise 的 then 回调中得到一个 ServiceWorkerRegistration
类型的对象;
正如例子中所示,主页面中就可以用这个实例化后的 'registration' 对象调用 onmessage
了
interface ServiceWorkerRegistration {
readonly attribute installing;
readonly attribute waiting;
readonly attribute active;
readonly attribute scope;
readonly attribute updateViaCache; update(); //in promise
unregister(); //in promise attribute onupdatefound;
};
同时如果 register()
成功,service worker 就在 ServiceWorkerGlobalScope
环境中运行;
也就是说,myService.js
中引用的 self
就是这个类型了,可以调用 self.skipWaiting()
等方法;
这是一个特殊类型的 worker 上下文运行环境,与主运行线程相独立,同时也没有访问 DOM 等能力
interface ServiceWorkerGlobalScope {
readonly attribute clients;
readonly attribute registration; skipWaiting(); attribute oninstall;
attribute onactivate;
attribute onfetch; attribute onmessage; // event.source is a client
attribute onmessageerror;
};
和 shared worker 类似,需要小心 service worker 脚本里的全局变量: 每个页面不会有自己独有的worker
在 service worker 注册之后,install 事件会被触发
在 install 回调中,一般执行以下任务:
出现在 activate 回调中的一个常见任务是缓存管理。在这个步骤进行缓存管理,而不是在之前的安装阶段进行,原因在于:如果在 install 步骤中清除了任何旧缓存,则继续控制所有当前页面的任何旧 service worker 将突然无法从缓存中提供文件
self.addEventListener('activate', function(event) { var cacheWhitelist = ['pages-cache-v1', 'blog-posts-cache-v1']; event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});
DOMException: Only secure origins are allowed (see: https://goo.gl/Y0ZkNV).
http://localhost
进行的后台同步(Background Sync)是基于 service worker 构建的另一个功能。允许用户一次性或按间隔时间请求后台数据同步。
navigator.serviceWorker.register('sw.js');//...navigator.serviceWorker.ready.then(registration=>{
registration.sync.register('update-leaderboard').then(function() {
// registration succeeded
}, function() {
// registration failed
});
});
//sw.js
self.addEventListener('sync', function(event) {
if (event.id == 'update-leaderboard') {
event.waitUntil(
caches.open('mygame-dynamic').then(function(cache) {
return cache.add('/leaderboard.json');
})
);
}
});
Push API 是基于 service worker 构建的另一个功能。该 API 允许唤醒 service worker 以响应来自操作系统消息传递服务的消息。
正是基于 service worker,chrome 在网络不可用时会显示小恐龙冒险的离线游戏,按下空格键,就可以开始了~
由于一些相关的 google 服务无法用,iOS 上对其的支持也有限并在试验阶段,所以尚不具备大规模应用的条件;
但作为渐进式网络应用技术 PWA 中的最重要的组成部分,国内很多厂商已经在尝试推进相关的支持,未来值得期待:
Master-Worker
是常用的并行设计模式,用worker
表示线程相关的概念就来源于此web worker
的出现使得在 Web 页面中进行多线程编程成为可能(end)