本系列其他译文请看JS工作机制 - 小白1991的专栏 - 掘金 (juejin.cn)
本文推荐指数: 2
WebWorker使用的场景是有限的,如果不是计算密集型任务可能很少用到。
你可能已经知道渐进式应用逐渐流行,因为它旨在创造拥有更加流畅的用户体验的网络应用和创建类 App 的原生应用体验而非浏览器端那样的外观和体验。
最主要的一个需求是它在网络和加载方面非常稳定,它可以在网络较差甚至不存在的条件下使用。
这一章节,我们要深入讨论一下Service Workers:他们的功能是什么样子的,以及我们需要关注点什么。 在最后,我们介绍一些Service Workers独一无二的优势,以及我们使用它的一些经验。 为便于写作,下面我们将用SerWorker来替代Service Workers。
概述
如果想了解SerWorker的所有细节,可以先从我们上一章博客开始。 SerWorker只是Web Worker中的一类,更多细节看Shared Worker:
- SerWorker在我们自己的全局脚本上下文中运行
- 不能关联到一个具体的web页面
- 不能访问 DOM
SerWorker API 这么令人振奋的一个主要院院士,让你的应用支持离线使用,让开发者能够完全控制网络应用的行为
SerWorker的生命周期
SerWorker 是跟你的网络页面完全独立的,它包含了以下几个阶段
- 下载
- 安装
- 激活
下载
This is when the browser downloads the .js file which contains the Service Worker.
这里浏览器下载包含SerWorker的.js 文件的时候
安装
安装一个SerWorker到你的app,你需要在你的JS代码中先注册。当一个SerWorker注册完成了,通知浏览器在后台启动一个SerWorker安装步骤。
By registering the Service Worker, you tell the browser where your Service Worker JavaScript file lives. Let’s look at the following code: 通过注册SerWorker,你告知浏览器,你的JS文件所在的位置,看一下代码
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js').then(function(registration) {
// 注册成功
console.log('ServiceWorker registration successful');
}, function(err) {
// 注册失败
console.log('ServiceWorker registration failed: ', err);
});
});
}
代码检查了当前环境是否支持SerWorker API。如果支持,就注册 /sw.js SerWorker
可以在每次页面加载的时候,可以调用 register() --浏览器将会指出SerWorker是否已经注册了,然后会恰当的处理它。
register()函数的一个重要细节是SerWorker文件的位置。在这个例子中,你可以看到SerWorker文件是在服务器的根目录之下。这意味着SerWorker的作用范围是整个源。换句话说,这个SerWorker将接受这个域名下的所有fetch 事件(稍后我们讨论)。如果我们注册在/example/sw.js中注册了SerWorker,你将会看到以/example/开头的URL所有的fetch 事件(比如/example/page1/, /example/page2/)
在安装阶段,最好去加载和缓存一些静态资源。一旦资源成功缓存,SerWoker就完成了安装。如果没有成功,将会重试一次。一旦安装成功,就知道了静态资源已经在缓存中了。
这就是加载之后需要注册的原因。如果不是必须,但是非常推荐。
为什么呢?假如用户第一次访问你的app。此时没有任何的SerWorker,浏览器是无法提前知道是否有SerWorker需要安装。如果进行安装,则浏览器将会为增加的线程开辟额外的 CPU 和内存,而这些资源原本是用来渲染网页的。
底线是,如果你只是在页面中安装一个SerWokrt,就可能有一个延迟加载的风险,以及重绘的问题---不能够让用户尽快地访问网页。
注意这个在页面初次访问的时候是很重要的。随后的访问,不会收到SerWoker的影响。一旦一个SerWorker 在第一次访问时激活了,它可以为后面的访问处理加载/缓存事件。这是正确的,Service Worker 需要加载好以处理有限的网络带宽。
激活
SerWoker安装之后,下一步就是激活了。这一步是管理之前缓存的巨大机会。
一旦激活,这个SerWorker会控制作用域内所有页面。 首次注册SerWorker的页面将不会被控制,知道它再次加载。一旦SerWorker被控制,将会变为以下状态之一
- 处理来自页面的网络或者消息请求所触发的 fetch 及 message 事件
- 中断,为了节约内存
看一下它的生命周期:
处理 Service Worker 内部的安装过程
页面处理完注册流程,SerWorker脚本内部发生了什么呢?它监听 Service Worker 实例的 install 事件。
Those are the steps that need to be taken when the install event is handled:
处理install 事件时需要做以下几步
- 打开缓存
- 缓存我们的文件
- 确认是否所有需要的资源已经缓存了
看一个简单的例子,Service Worker内部如何安装的:
var CACHE_NAME = 'my-web-app-cache';
var urlsToCache = [
'/',
'/styles/main.css',
'/scripts/app.js',
'/scripts/lib.js'
];
self.addEventListener('install', function(event) {
// event.waitUntil 使用 promise 来获得安装时长及安装是否失败
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
如果所有的文件已经成功缓存,然后SerWoker将会被安装。如果任意文件没有下载成功,安装步骤都会失败。所以要小心防止你的文件。
处理 install 事件是可选的,如果你在这一步没什么可做的就可以不处理它。
运行时缓存请求
这部分是干货了。在这里,你将会看到如何劫持请求,如何返回已创建的缓存。
SerWorker安装之后,用户导航到另一个页面或者刷新了当前页面,SerWorker将会接受fetch事件。这里有一个例子演示了如何返回缓存的静态资源或执行一个新的请求并缓存返回结果的过程的
self.addEventListener('fetch', function(event) {
event.respondWith(
// 该方法查询请求然后返回 Service Worker 创建的任何缓存数据。
caches.match(event.request)
.then(function(response) {
// 若有缓存,则返回
if (response) {
return response;
}
// 复制请求。请求是一个流且只能被使用一次。因为之前已经通过缓存使用过一次了,所以为了在浏览器中使用 fetch,需要复制下该请求。
var fetchRequest = event.request.clone();
// 没有找到缓存。所以我们需要执行 fetch 以发起请求并返回请求数据。
return fetch(fetchRequest).then(
function(response) {
// 检测返回数据是否有效
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 复制返回数据,因为它也是流。因为我们想要浏览器和缓存一样使用返回数据,所以必须复制它。这样就有两个流
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
// 把请求添加到缓存中以备之后的查询用
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
看看里面发生了什么:
- The
event.respondWith()will determine how we’ll respond to thefetchevent. We pass a promise fromcaches.match()which looks at the request and finds if there are any cached results from any of the caches that have been created. -
event.respondWith()会决定如何响应fetch事件。caches.match()查询请求然后返回之前创建的缓存中的任意缓存数据并返回 promise。 - 如果有缓存,响应就会恢复
- 否则,执行
fetch - 检查状态码是否是
200。同时检查响应类型是否为 basic,即检查请求是否同域 在这个案例中,请求第三方资源是不会被缓存的。 - 响应内容被添加到缓存中。
请求和回应需要被克隆,因为他们是流。这个流的主体只可以被用一次。由于缓存和浏览器都需要使用它们,所以必须进行复制。
更新Service Worker
用户访问你的app时,浏览器会尝试重新下载包含SerWorker代码的.js文件---这会在后台执行。
下载的文件跟当前SerWorker文件对比,即使仅仅有一个个字符的不同,浏览器将会认为文件已经改变,然后就启动一个新的SerWorker.
新的SerWoker将会启动,然后会触发安装事件。在这个时候,旧的SerWorker依然控制着app的页面,新的SerWorker将进入一个waiting状态。
一旦当前打开的页面被关闭了,旧的SerWoker将会被浏览器傻屌,新安装的SerWoker将取得完全的控制权。这时它的激活事件将会被触发。
为什么这些都是必要的?这是为了避免在不同tab中同时运行不同版本的的app所造成的问题-可能会产生新的 bug(比如当在浏览器中本地存储数据的时候却拥有不同的数据库结构)。
删除缓存数据
activate回调中最常见的一步是缓存管理。如果你打算清除所有的安装步骤旧缓存,你就可能需要走这一步,旧的SerWorker将会突然失去从缓存中户获取文件的能力。
Here is an example how you can delete some files from the cache that are not whitelisted (in this case, having page-1 or page-2 under their names):
这里有一个例子,如何删除缓存中白名单之外的文件(该情况下,以 page-1 或者 page-2 来进行命名)
self.addEventListener('activate', function(event) {
var cacheWhitelist = ['page-1', 'page-2'];
event.waitUntil(
// 获得缓存中所有键
caches.keys().then(function(cacheNames) {
return Promise.all(
// 遍历所有的缓存文件
cacheNames.map(function(cacheName) {
// 若缓存文件不在白名单中,删除之
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});
HTTPS 要求
构建你的app时,你可以通过localhost使用SerWorker,但是一旦你发布了产品,就需要使用HTTPS(这也是使用HTTPS的最后原因啦)。 可以利用 Service Worker劫持网络连接和伪造响应数据。如果不使用 HTTPS,网络应用容易遭受中间人 攻击。
为了保证安全,必须通过 HTTPS 在页面上注册 Service Workers,这样就可以保证浏览器接收到的 Service Worker 没有在传输过程中被篡改
Browser支持
浏览器支持已经越来也好了:
你可以跟踪浏览器支持情况— jakearchibald.github.io/isservicewo….
## Service Workers 提供了更多的功能的可能
Service Worker 提供了一些独一无二的特性:
- 推送通知 — 允许用户选择定时接收网络应用的推送更新
- 后台同步 — 允许延迟操作直到网络连接稳定之后。这样就可以保证用户即时发送数据。
- 定期同步(以后支持) — 提供了管理进行定期后台数据同步的功能
- Geofencing(以后支持) — 可以自定义参数,也即 geofences ,该参数包含着用户所感兴趣的地区。当设备穿过某片地理围栏的时候会收到通知,这就能够让你基于用户的地理位置来提供有用的用户体验
这里提到的每个功能将会该系列之后的文章中进行详细阐述。