前言
大家都知道在浏览器环境下JS是单线程执行的,估摸着应该是为了执行方面性能的考虑,增加了web worker api
,可以让浏览器在后台开辟一个worker线程,处理某些事务。那么基于我们之前的《面试准备-异步》的学习中我们已经知道,web worker api
是有一个专门的agent
(代理),并且我们也通过不同agent
知道在web中至少存在3种类型的worker
分别是:
Web Worker
Shared worker
Service Worker
我们今天就来一起学习一下,web worker api
到底是怎么回事
worker是什么?
worker
是允许你在浏览器环境在后台开辟一个线程去执行脚本的api
。当然它自身也有一定的限制,比如它无法直接访问DOM元素,或者无法使用document
,window
。因为它的执行上下文与主线程的执行上下文存在一定差异。不同的Worker
有不同的全局对象。但是Worker
是可以操作ajax
或者IndexedDB
等。所以我们可以理解为,worker可以帮我们处理一些幕后的事情,比如进行大量的数据计算,发起高频的网络请求或者是使用数据存储相关的api操作等。同时在whatwg
可以了解到,worker
的实际上是一个较重的API它对浏览器是有一定的负担的,所以建议上也是需要认真分析当前的场景是否是需要使用worker
进行业务的实现或者性能的优化。 我们来看看兼容情况
这里是关于Worker API
的兼容性情况,还是很不错的但是针对不同的Worker
类型,兼容情况可能都有不同,我们在使用对应的Worker
时,也是需要去检查一下对应的兼容性情况,再确定是否使用。
那么简单的介绍就结束了,接下来就让我们依次了解一下不同Worker
的作用以及能力吧
web worker
web worker
或者直接叫做worker
,他支持使用Worker(...)
构造来生成一个worker
实例对象,他的定义主要如下
[Exposed=(Window,DedicatedWorker,SharedWorker)]
interface Worker : EventTarget {
constructor(USVString scriptURL, optional WorkerOptions options = {});
undefined terminate();
undefined postMessage(any message, sequence<object> transfer);
undefined postMessage(any message, optional StructuredSerializeOptions options = {});
attribute EventHandler onmessage;
attribute EventHandler onmessageerror;
};
dictionary WorkerOptions {
WorkerType type = "classic";
RequestCredentials credentials = "same-origin"; // credentials is only used if type is "module"
DOMString name = "";
};
enum RequestCredentials { "omit", "same-origin", "include" };
enum WorkerType { "classic", "module" };
Worker includes AbstractWorker;
构造函数
我们可以看到worker
继承于EventTarget
,它本身就是采用的浏览器事件接口的。除此之外可以了解到,构造函数接受两个参数分别是scriptURL
以及options
-
scriptURL
用于指定要加载的worker模块的脚本地址,部分浏览器环境支持使用data URI
-
options
对象类型,然后有三个可选的属性值,分别是type
,credentials
以及name
-
options.type
选择加载类型,可以使用的值有classic
以及module
,使用module
允许使用ES Module
对文件进行处理 -
options.credentials
用于制定加载时文件时,是否携带cookie
等身份认证信息,点击查看详细介绍 -
options.name
worker的名称,据MDN上说一般用于调试
那么了解完构造函数的的参数后,我们来尝编写一下自己的worker
脚本,首先我们需要一个静态资源服务服务,搭建的方式可以参考《面试准备-JS模块化》中的服务搭建内容来进行,搭建完成后我们来尝试一下,首先我们编写*html
如下
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>web worker api</title>
<style>
</style>
</head>
<body>
<script>
const myWorker = new Worker('./myWorker.js')
myWorker.postMessage({name: 'mainMessage'})
myWorker.addEventListener('message', (...argements) => {
console.log(argements)
})
</script>
</body>
</html>
然后编写我们的worker文件myWorker.js
其中内容如下:
console.log(self)
function run () {
setTimeout(() => {
self.postMessage('workMessage')
}, 1000)
}
self.addEventListener('message', (...argements) => {
console.log(argements)
run()
})
浏览器输入完地址以后我们打开控制台可以看到以下内容
其中第一条是我们在myWorker
的全局执行上下文DedicatedWorkerGlobalScope
,我们来一起了解一下。
首先我们需要知道一下DedicatedWorkerGlobalScope
的继承关系。
首先DedicatedWorkerGlobalScope
继承于EventTarget
和WorkerGlobalScope
。可以使用父类的方法。那么同时我们也可以分析出来,实际上worker
也就是一个事件目标对象,它也可以挂载EventListener进行时间监听。那么在了解完继承关系后我们再来梳理一下WorkerGlobalScope
提供了什么方法以及属性(EventTarget
之前在事件的文章中已经介绍过了,这里就不重复介绍了)
WorkerGlobalScope
WorkerGlobalScope
是继承了EventTarget
,并且实现了接口WindowTimers
, WindowBase64
, WindowEventHandlers
, GlobalFetch
。
它拥有以下属性以及方法:
WorkerGlobalScope.caches
(只读对象)
返回与当前上下文相关的CacheStorage
对象,它主要与缓存相关,一般用于service worker中,所以在后文我们会详细介绍它
自带属性以及方法
返回与worker
关联的 WorkerNavigator
。它是一个特定的导航器对象,适用worker
。
WorkerGlobalScope.self
(只读对象)
返回对 WorkerGlobalScope 本身的引用。大多数情况下,它是一个特定的范围,例如 DedicatedWorkerGlobalScope、SharedWorkerGlobalScope (en-US) 或 ServiceWorkerGlobalScope。
返回与worker
关联的 WorkerLocation
。它是一个特定的位置对象,适用于worker
。
-
WorkerGlobalScope.close()
丢弃在 WorkerGlobalScope 的事件循环中排队的任何任务,关闭当前作用域 -
WorkerGlobalScope.importScripts()
可以动态将多个脚本引入当前worker的上下文中
从其他接口实现的
(大部分就不解释了,大家都懂的)
WindowBase64.atob()
WindowBase64.btoa()
WindowTimers.clearInterval()
WindowTimers.clearTimeout()
ImageBitmapFactories.createImageBitmap()
GlobalFetch.fetch()
WindowTimers.setInterval()
WindowTimers.setTimeout()
数据方法对象
大部分的古有对象构造方法以及数据构造方法都是有的
浏览器存储
这里需要注意的是sessionStorage
和localStorage
是没有办法在WorkerGlobalScope
中使用的。在worker
中可以使用的浏览器存储有IndexedDB
。后续会专门出一期专门介绍浏览器的存储的文章
DedicatedWorkerGlobalScope
事件
DedicatedWorkerGlobalScope
实际上在WorkerGlobalScope
的基础上增加了两个事件分别是:
message
来源构建实例postMessage
的信息
messageerror
收到无法反序列化的message
时的报错
支持以DOM0的传统模式以及DOM2的EventListener
的方式进行监听
方法
以及增加了事件派发的方法:
DedicatedWorkerGlobalScope.postMessage
该方法可以传递多种类型的message的给到外部的worker
实例,通过message
事件进行监听。 除去第一个参数允许传递message
参数以外,在第二个参数中可以对message
中需要转移的转移对象进行声名。转移对象允许对象从一个上下文转移到另一个上下文(其实我理解这里是从一个agents的上下文栈转移到另一个agents)。转移后的对象将不再具有可用性(读写)。以下是所有转移对象的类型,具体关于转移对象的介绍可以查看原文ArrayBuffer
MessagePort
ReadableStream
WritableStream
TransformStream
AudioData
ImageBitmap
VideoFrame
OffscreenCanvas
RTCDataChannel
总结
那么我们在了解完了以后实际上发现,web worker实际的应用场景是会需要跟我们同源窗口代理(具体可看异步章节学习)进行隔离的,针对视图导航的方面的能力从DedicatedWorkerGlobalScope
的上下文出发我们可以发现基本是没有相关操作能力的。更多的主要是网络请求,数据处理(比如可转移对象),数据存储(IndexedDB)。比如数据大量计算,以及存储任务,处理任务等跟实时用户交互无关的任务可以交给它。
worker实例对象
了解完worker内部的全局对象后,我们再来了解一下worker实例。Worker
在上文提到过,也是继承于EventTarget
所以具备时间目标的相关属性以及方法。除此以外它还具备以下方法以及事件
方法
-
Worker.postMessage()
可以用于跟worker内部的上下文进行通讯,同DedicatedWorkerGlobalScope.postMessage
方法参数 -
Worker.terminate()
结束当前worker的行为,不会等待worker完成剩余的操作(作者认为这里等同于worker上下文内部调用close方法,结束当起当前agents的事件循环,并进行回收)
事件
-
message
用于接收来自于worker内部上下文的message。 -
messageerror
同DedicatedWorkerGlobalScope
的messageerror
事件,当worker内给外部实例传递一条无法返序列化的数据是有此报错 -
error
当在worker内执行上下文抛出错误时,会触发当前事件
Shared worker
SharedWorker
允许多个同源窗口或者iframe对其进行同时访问,它跟web worker具有不同的全局环境SharedWorkerGlobalScope
,那么首先我们先来尝试进行编写一个SharedWorker
的例子。
<!--index.html-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>web worker api</title>
<style>
</style>
</head>
<body>
<script>
const shareWorker = new SharedWorker("./myWorker.js");
// 接受信息
shareWorker.port.onmessage = (e)=> {
console.log(e.data)
}
// 发送信息
shareWorker.port.postMessage({
name: 'i am index.html'
});
</script>
</body>
</html>
<!--home.html-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>web worker api</title>
<style>
</style>
</head>
<body>
<script>
const shareWorker = new SharedWorker("./myWorker.js");
// 接受信息
shareWorker.port.onmessage = (e)=> {
console.log(e.data)
}
// 发送信息
shareWorker.port.postMessage({
name: 'i am home.html'
});
</script>
</body>
</html>
// myWorker.js
const allContenter = []
self.onconnect = function (e) {
const port = e.ports[0]
allContenter.push(port)
port.addEventListener('message', function (e) {
allContenter.forEach(item => {
item.postMessage(e.data)
})
})
port.start()
}
然后我们在按照顺序分别打开index.html
以及home.html
然后我们来查看index.html
的控制台打印
我们可以看到在index.html
打印出了我们在home.html
中所派发的message,OK到这里案例结束了。那么我们一起来学习一下Shared worker
的相关知识吧!
构造函数
首先Shared worker
的构造函数与Worker
是类似的都可以传递两个参数,区别点在于第二个参数在SharedWorker
中可以选择传递name
或者是option
。其中name
实际上等同于option.name
实例对象
我们发现我们在使用SharedWorker
实例时主要是port
属性进行调用的,port
返回了一个MessagePort
对象,用于通信和控制SharedWorker
。
SharedWorker
操作主要与MessagePort
相关,那么我们来看看什么是MessagePort
。
MessagePort
首先MessagePort
对象属于MessageChannel
的两个通信端口之一。那么我们可以猜测每次调用SharedWorker
实际上内部实现上调用了一次MessageChannel
构造函数,并分别派发到了worker上下文以及当前浏览上下文的worker实例中。
接着由于MessagePort
实际上是继承于EventTarget
,所以存在并实现父类的方法
方法
MessagePort.postMessage
从端口发送一条消息,并且可选是否将对象的所有权交给其他浏览器上下文(这里选择对象是否转交也是指的前文的可转移对象)。MessagePort.start
开始发送该端口中的消息队列 (只有使用EventTarget.addEventListener
的时候才需要调用;当使用MessagePort.onmessage
时,是默认开始的。)MessagePort.close
断开端口连接,它将不再是激活状态
事件
当 MessagePort
对象收到消息时会触发。
也可以通过 onmessage
属性使用。
当 MessagePort
对象收到无法被反序列化的消息时触发。
也可以通过 onmessageerror
属性使用。
那么了解完了MessagePort
,我们再来了解一下SharedWorker
的全局对象SharedWorkerGlobalScope
SharedWorkerGlobalScope
SharedWorkerGlobalScope
的继承关系如下图:
也就是说基于我们上文的学习,我们只需要了解SharedWorkerGlobalScope
本身所实现的方法/事件即可
方法
SharedWorkerGlobalScope.close()
同web worker的close方法 丢弃事件循环中的所有任务,然后关闭。
事件
当新的客户端(浏览上下文)连接到SharedWorker
时触发
总结
那么到这里我们就完成了对SharedWorker
的学习了解,后续我们有存在跨窗口或者iframe传递数据的需求时,我们就可以根据兼容性,选择使用当前方案完成场景
Service Worker
Service Worker
我们先来根据MDN上的简介对其了解一下,它跟我们之前接触的worker可能存在一定的差异。所以大家在学习的时候可能要先暂时放一放之前对于worker的理解来看待Service Worker
简介
Service workers
本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。
Service worker
是一个注册在指定源和路径下的事件驱动worker。它采用JavaScript控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你可以完全控制应用在特定情形(最常见的情形是网络不可用)下的表现。
Service worker
运行在worker上下文,因此它不能访问DOM。相对于驱动应用的主JavaScript线程,它运行在其他线程中,所以不会造成阻塞。它设计为完全异步,同步API(如XHR和localStorage (en-US))不能在Service worker
中使用。
出于安全考量,Service workers
只能由HTTPS承载,毕竟修改网络请求的能力暴露给中间人攻击会非常危险。在Firefox浏览器的用户隐私模式,Service Worker不可用。
根据阅读上文可知
Service workers
常用于离线体验的提供,实际上像是Vue的官网已经在使用这一项技术,大家可以尝试访问后开启飞行模式,重新刷新页面,依旧可以进行访问
同时除去提供一些基本的离线支持外,可以用来尝试以下等场景
- 后台数据同步
- 响应来自其它源的资源请求
- 集中接收计算成本高的数据更新,比如地理位置和陀螺仪信息,这样多个页面就可以利用同一组数据
- 在客户端进行CoffeeScript,LESS,CJS/AMD等模块编译和依赖管理(用于开发目的)
- 后台服务钩子
- 自定义模板用于特定URL模式
- 性能增强,比如预取用户可能需要的资源,比如相册中的后面数张图片
未来service workers能够用来做更多使web平台接近原生应用的事。 值得关注的是,其他标准也能并且将会使用service worker,例如:
- 后台同步:启动一个service worker即使没有用户访问特定站点,也可以更新缓存
- 响应推送:启动一个service worker向用户发送一条信息通知新的内容可用
- 对时间或日期作出响应
- 进入地理围栏
生命周期
单个Service Worker是存在声明周期的,整体而言包含以下生命周期:
- 下载
- 安装(指定某些安装工作,比如离线缓存)
- 激活(当没有窗口在使用当前版本的worker时在后台激活)
以上为MDN介绍时的生命周期,实际上我在Chrome DevTool中可以发现还有一个Wait
的阶段
所以实际上每个Service Worker的生命周期应该如下
- 下载
- 安装
Install
(指定某些安装工作,比如离线缓存) - 等待激活
Wait
(因为当前还有在工作的service worker 所以要等待本次会话窗口全部结束后才会激活) - 激活
Activate
(当没有窗口在使用当前版本的worker时在后台激活)
当然如果作为严格划分Register
的存在更加像是一个动作,所以实际上它的整个完整的执行链路更加像是(作者自己总结):
ServiceWorkerGlobalScope
接着我们来看一下,Service Worker
内部的全局对象ServiceWorkerGlobalScope
是什么样的。首先ServiceWorkerGlobalScope
的继承关系如下:
根据继承关系,我们直接来了解ServiceWorkerGlobalScope
上新增的方法/属性/事件即可
属性
-
ServiceWorkerGlobalScope.clients
包含与当前的service worker
相关的一个Clients
对象 -
ServiceWorkerGlobalScope.registration
包含Service Worker的 ServiceWorkerRegistration 对象。该对象上可以观测到是否当前不同的生命周期下,是否有正在注册的worker。同时它也可以用于进行浏览器消息通知。 -
ServiceWorkerGlobalScope.caches
返回一个与当前Service Worker相关的CacheStorage
对象
其中CacheStorage
在离线缓存中有着较大的比重,所以我们这里着重看看CacheStorage
CacheStorage
CacheStorage
实际上是Cache
对象群的接口代表(其实可以理解为存在多个不同的存储空间),那么实际上Cache
才是真正的存储对象类型,他可以通过cacheName
映射到对应的Cache
对象,CacheStorage
包含以下方法(来自MDN):
方法
检查给定的 Request
是否是 CacheStorage
对象跟踪的任何 Cache
对象的键,并返回一个resolve为该匹配的 Promise
.(所以这里存在如果当一个相同的Request存在于不同的Cache中,可能命中的Cache并非是理想的数据)
如果存在与 cacheName
匹配的 Cache
对象,则返回一个resolve为true的 Promise
.
返回一个 Promise
,resolve为匹配 cacheName
(如果不存在则创建一个新的cache)的 Cache
对象
查找匹配 cacheName
的 Cache
对象,如果找到,则删除 Cache
对象并返回一个resolve为true的 Promise
。如果没有找到 Cache
对象,则返回 false
.
返回一个 Promise
,它将使用一个包含与 CacheStorage
追踪的所有命名 Cache
对象对应字符串的数组来resolve. 使用该方法迭代所有 Cache
对象的列表。
了解完CacheStorage
我们再来看看Cache
Cache
Cache
接口为缓存的 Request
/ Response
对象对提供存储机制,例如,作为ServiceWorker
生命周期的一部分。请注意,Cache 接口像 workers 一样,是暴露在 window 作用域下的。尽管它被定义在 service worker 的标准中, 但是它不必一定要配合 service worker 使用(实际上我们在window的上下文中也可以根据caches
对象访问到具体的cache
).
一个域可以有多个命名 Cache 对象。你需要在你的脚本 (例如,在 ServiceWorker
中)中处理缓存更新的方式。除非明确地更新缓存,否则缓存将不会被更新;除非删除,否则缓存数据不会过期。使用 CacheStorage.open(cacheName)
打开一个Cache 对象,再使用 Cache 对象的方法去处理缓存.
你需要定期地清理缓存条目,因为每个浏览器都硬性限制了一个域下缓存数据的大小。缓存配额使用估算值,可以使用 StorageEstimate
API 获得(可以通过navigator.storage
(en-US)或WorkerNavigator.storage
(en-US)进行访问)。浏览器尽其所能去管理磁盘空间,但它有可能删除一个域下的缓存数据。浏览器要么自动删除特定域的全部缓存,要么全部保留。确保按名称安装版本缓存,并仅从可以安全操作的脚本版本中使用缓存。
方法
返回一个 Promise
对象,resolve的结果是跟 Cache
对象匹配的第一个已经缓存的请求。
返回一个Promise
对象,resolve的结果是跟Cache
对象匹配的所有请求组成的数组。
抓取这个URL, 检索并把返回的response对象添加到给定的Cache对象.这在功能上等同于调用 fetch(), 然后使用 Cache.put() 将response添加到cache中.
抓取一个URL数组,检索并把返回的response对象添加到给定的Cache对象。
同时抓取一个请求及其响应,并将其添加到给定的cache。
搜索key值为request的Cache
条目。如果找到,则删除该Cache
条目,并且返回一个resolve为true的Promise
对象;如果未找到,则返回一个resolve为false的Promise
对象。
返回一个Promise
对象,resolve的结果是Cache
对象key值组成的数组。
事件
activate
当前sw被激活时触发当前事件
fetch
当页面发起网络请求时触发当前事件(其中包括各种静态资源拉取)
install
安装发生的生命周期
message
消息事件
notificationclick
消息通知点击事件
notificationclose
消息通知关闭事件
push
当收到服务器的推送通知时
pushsubscriptionchange
推送订阅即将失效或者已经失效时触发
sync
当从 Service Worker 客户端页面调用 SyncManager.register 时会触发此事件。如果网络可用,则立即尝试同步,或者在网络可用时立即尝试同步。
方法
-
ServiceWorkerGlobalScope.skipWaiting()
允许当前的serview worker跳过等待阶段直接进入激活阶段 -
GlobalFetch.fetch()
网络请求API
OK 那么到这里我们就基本了解完我们当前的Serview Worker的API
我们来尝试编写一个小DEMO吧
案例
首先我们要知道Service worker
仅支持在HTTPS下使用,但是如果是localhost
进行访问是可以在本地的浏览器环境进行调试的
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>web worker api</title>
<style>
</style>
</head>
<body>
<div>
i am page
</div>
<script>
navigator.serviceWorker.register('sw.js', {
scope: './'
}).then(function(reg) {
// registration worked
console.log('Registration succeeded. Scope is ' + reg.scope);
}).catch(function(error) {
// registration failed
console.log('Registration failed with ' + error);
});
</script>
</body>
</html>
// sw.js
const cacheVersion = 'v1'
self.addEventListener('install', function (event) {
// 指定的安装工作,当前install需要等待waitUntil中的promise resolve后才会进入等待激活阶段waiting
event.waitUntil(
caches.open(cacheVersion).then(function (cache) {
return cache.addAll([
'/serviceWorkerStudy/index.html'
])
})
)
})
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (resp) {
return resp || fetch(event.request).then(function (response) {
return caches.open(cacheVersion).then(function (cache) {
// 不缓存chrome
if (!event.request.url.includes('chrome-extension')) {
cache.put(event.request, response.clone())
}
return response
})
})
})
)
})
self.addEventListener('activate', function (event) {
const cacheWhitelist = [cacheVersion]
event.waitUntil(
caches.keys().then(function (keyList) {
return Promise.all(
keyList.map((key) => {
if (cacheWhitelist.indexOf(key) === -1) {
return caches.delete(key)
}
return null
}).filter(d => !!d)
)
})
)
})
然后我们通过localhost:xxxx/xxx.html
进行访问,接着我们打开Chrome DevTools
打开查看我们的网络请求此时大概如下所示:
可以看到我们大小还是存在的,也就是说还是实际发起了网络请求的,那么我们打开应用(aplication)
来看看serview worker的一个运行情况
我们会发现由于我们是首次注册service worker 我们的worker已经进入到了激活阶段。 OK,那么我们修改一下html中的内容将i am page
改成i am hidden
然后我们刷新一下浏览器。我们会发现页面依旧会显示i am page
, 我们查看一下网络
此时我们会发现我们首页已经被缓存下来了,同时我们可以打开我们的应用(aplication)
查看缓存空间
我们可以查看的到我们已经被缓存的静态资源文件
OK 那么这里简单的静态资源缓存就结束了,如果大家要问如何刷新缓存呢? 可以修改一下在sw.js
文件中的cacheVersion
重新刷新两次页面就可以了,为什么需要两次呢,这就是课后作业。
那么关于worker
的内容我们就学习到这里,如果有更多的补充欢迎留言
完美!
下一章:面试准备-浏览器存储