共享web worker(或共享worker)的行为类似专用worker,但是它可被多个可信执行环境访问。例如,两个同一个源的不同标签可以访问同一个worker。共享worker和worker的外部和内部消息接口有轻微不同。
当开发者想要减少计算开销,允许多个执行环境共享一个worker时,共享worker很有用。例如用一个共享worker为多个同源页面管理发送和接收消息的websocket。同源环境之间也可以通过共享worker进行通讯。
共享Worker基础
从行为上来说,共享worker可以认为是专用worker的扩展。worker的创建、选项,安全限制和importScripts()方法都是相同的。共享worker也在独立的执行环境中运行,也只能和其他环境进行异步通讯。
创建共享Worker
和专用worker一样,创建共享worker的最一般方法是通过加载JavaScript文件。给SharedWorke构造函数提供文件路径,它就在后台异步加载脚本并实例化worker。
下面的简单里子从绝对路径文件创建了一个空的共享worker:
EMPTYSHAREDWORKER.JS
// empty JS worker file
MAIN.JS
console.log(location.href); // "https://example.com/"
const sharedWorker = new SharedWorker(
location.href + 'emptySharedWorker.js');
console.log(sharedWorker); // SharedWorker {}
前面的例子可以改为使用相对路径;然而,这要求main.js的执行路径与emptySharedWorker.js的路径相同才能载入脚本。
const worker = new Worker('./emptyWorker.js');
console.log(worker); // Worker {}
共享worker也可以从内联脚本创建,但这么做的意义不大:每个从内联脚本字符串创建的blob都被赋予一个唯一的 in-browser URL,因此由内联脚本创建的共享worker总是独一无二的。在下一节说明原因。
共享Worker的身份和单个占用
共享worker和专用worker的一个重要区别是:Worker()总是创建新的worker实例,SharedWorker()构造函数仅在没有具有相同身份的worker实例存在时才会创建一个新的。如果一个共享worker和已存在的共享worker身份匹配,那么它就会和已存在的共享worker形成一个新的连接。
共享worker身份由解析脚本URL,worker名,和文档源确定。例如,下面的脚本会实体化单个共享worker并加入两个连接。
// Instantiates single shared worker
// - Constructors called all on same origin
// - All scripts resolve to same URL
// - All workers have same name
new SharedWorker('./sharedWorker.js');
new SharedWorker('./sharedWorker.js');
new SharedWorker('./sharedWorker.js');
类似的,因为所有下面的三个脚本解析为相同的URL,仅创建了一个共享worker:
// Instantiates single shared worker
// - Constructors called all on same origin
// - All scripts resolve to same URL
// - All workers have same name
new SharedWorker('./sharedWorker.js');
new SharedWorker('sharedWorker.js');
new SharedWorker('https://www.example.com/sharedWorker.js');
因为可选的worker名是共享worker身份的一部分,使用不同的worker名(一个名为’foo‘,一个名为’bar‘)会让浏览器强制创建多个共享worker,尽管它们的源和脚本URL相同
// Instantiates two shared workers
// - Constructors called all on same origin
// - All scripts resolve to same URL
// - One shared worker has name 'foo', one has name 'bar'
new SharedWorker('./sharedWorker.js', {name: 'foo'});
new SharedWorker('./sharedWorker.js', {name: 'foo'});
new SharedWorker('./sharedWorker.js', {name: 'bar'});
顾名思义,共享worker可以在标签,窗口,iframe或其他运行在同一个源的worker之间共享。因此,下面运行在多个标签上的脚本将在首次执行时创建一个worker,后续的执行将连接到同一个worker:
// Instantiates single shared worker
// - Constructors called all on same origin
// - All scripts resolve to same URL
// - All workers have same name
new SharedWorker('./sharedWorker.js');
共享worker的身份是URL限定的,因此下面将创建两个共享worker,即使加载的是同一个脚本。
// Instantiates two shared workers
// - Constructors called all on same origin
// - '?' token differentiates URLs
// - All workers have same name
new SharedWorker('./sharedWorker.js');
new SharedWorker('./sharedWorker.js?');
如果该脚本是在两个不同的标签运行,仍然是创建两个共享worker。每个构造函数都检查匹配的共享worker,如果存在就连接它。
使用SharedWorker对象
由SharedWorker()返回的SharedWorker对象可用作与新创建的专用worker通讯的单点。它可用作在worker和父环境之间通过MessagePort传输信息,并捕获专用worker发出的错误事件。
SharedWorker对象支持下面属性:
(1)onerror—可以分配一个事件处理程序,当ErrorEvent类型的错误从worker上冒泡时调用。
- 当错误从worker内部抛出时,事件发生。
- 事件也可以使用sharedWorker.addEventListener('error',handler)方法处理。
(2)port—专用的MessagePort,用于和共享worker通讯。
SharedWorkerGlobalScope
在共享worker内部,全局作用域是SharedWorkerGlobalScope的一个实例。它继承自WorkerGlobalScope ,因此包括它的所有属性和方法。和专用worker一样,共享worker可以通过self访问全局作用域。
(1)SharedWorkerGlobalScope 扩展了WorkerGlobalScope,具有如下属性和方法:
(2)name—可选的字符串标识符,可以提供给SharedWorker构造函数。
(3)importScripts()—用于加载任意数量的脚本进入worker。
(4)close()—对应于worker.terminate()。它用于立即终止worker。没有提供给worker清理的机会;脚本立即结束。
(5) onconnect—设置的事件处理程序用于当和共享worker有新的连接时。connect事件包括一个MessagePort实例的端口数组,它可用于发送消息回父环境。
- 通过worker.port.onmessage 或 worker.port.start()都能触发connect事件。
- 这一事件也可以使用sharedWorker.addEventListener('connect', handler)方法处理.
注意:根据浏览器的实现不同,在SharedWorker内部向控制台log记录可能不会向默认的浏览器控制台打印。
理解共享Worker 的生命周期
共享worker的生命周期与专用worker的生命周期有相同的阶段。不同在于,专用worker和单个页面密不可分,而只要还有一个环境保持和共享worker连接,共享worker就会持续。
考虑下面的脚本,每次执行都创建一个专用worker:
new Worker('./worker.js');
下面的表格详细说明了当三个标签依次打开关闭时,它们生成的worker发生了什么。
如表中所示,脚本的执行次数,打开标签的个数,在运行的worker数是相等的。接下来,考虑如下的简单代码,它每次执行将创建或连接到一个共享worker。
new SharedWorker('./sharedWorker.js');
下面的表格详细说明了当三个标签依次打开关闭时,它们生成的worker发生了什么。
如表中所示,在tab2和tab3调用new SharedWorker()将连接到已存在的worker。当连接从worker添加或移除都会检查连接的总数。单连接数变为0时,终止worker。
重要的一点,不能人为编程终止共享worker。注意SharedWorker对象上没有terminate()方法。此外,在共享worker端口(本章后面讨论)上调用close()不会触发worker的终止,即便只有一个端口连接到该worker。
SharedWorker对象的“连接”与相应的MessagePort 或MessageChannel的状态无关。一旦建立和共享worker的连接,连接的管理就由浏览器负责。建立的连接将持续在页面的生命周期中持续,只有页面销毁,共享worker上没有更多连接的时候,浏览器终止该worker。
连接共享Worker
每次调用SharedWorker构造函数时,在共享worker内部触发connect事件,包括创建worker。下面的例子说明了这一点,这里构造函数在循环中调用:
SHAREDWORKER.JS
let i = 0;
self.onconnect = () => console.log(`connected ${++i} times`);
MAIN.JS
for (let i = 0; i < 5; ++i) {
new SharedWorker('./sharedWorker.js');
}
// connected 1 times
// connected 2 times
// connected 3 times
// connected 4 times
// connected 5 times
connect事件出现时,SharedWorker 构造函数隐式创建一个MessageChannel ,并给传入一个对SharedWorker 实例来说是唯一的MessagePort所有权。这个MessagePort在connect事件对象内部可用ports数组访问。因为conncet事件仅代表单个连接,ports数组的长度刚好为1。
下面说明如何访问事件的ports数组。这里使用Set保证跟踪的对象没有重复。
SHAREDWORKER.JS
const connectedPorts = new Set();
self.onconnect = ({ports}) => {
connectedPorts.add(ports[0]);
console.log(`${connectedPorts.size} unique connected ports`);
};
MAIN.JS
for (let i = 0; i < 5; ++i) {
new SharedWorker('./sharedWorker.js');
}
// 1 unique connected ports
// 2 unique connected ports
// 3 unique connected ports
// 4 unique connected ports
// 5 unique connected ports
重要的一点,共享worker在建立和删除连接时行为不同。每个新的SharedWorker连接都触发一个事件,但SharedWorker实例断开连接时(例如页面关闭)没有对应的事件。
在前面的例子中,当页面连接并断开同一个共享worker时,connectedPorts Set将会被“死去的”端口污染,而无法鉴别它们。一种解决办法是,在页面将要被销毁前的beforeunload 事件发生时,显式的发送一条teardown消息,这可以让共享worker做清理工作。