前言
大家都知道在浏览器环境下JS是单线程执行的,估摸着应该是为了执行方面性能的考虑,增加了web worker api,可以让浏览器在后台开辟一个worker线程,处理某些事务。那么基于我们之前的《面试准备-异步》的学习中我们已经知道,web worker api是有一个专门的agent(代理),并且我们也通过不同agent知道在web中至少存在3种类型的worker分别是:
Web WorkerShared workerService 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.nameworker的名称,据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)。转移后的对象将不再具有可用性(读写)。以下是所有转移对象的类型,具体关于转移对象的介绍可以查看原文ArrayBufferMessagePortReadableStreamWritableStreamTransformStreamAudioDataImageBitmapVideoFrameOffscreenCanvasRTCDataChannel
总结
那么我们在了解完了以后实际上发现,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的内容我们就学习到这里,如果有更多的补充欢迎留言
完美!
下一章:面试准备-浏览器存储