新一代缓存解决方案——Service Worker
目前浏览器已经提供了不少存储相关的API,比如localStorage,application cache等,以及各种缓存相关的策略,memory cache, disk cache等。这些方案各有优缺点,但无论哪种方案要实现精确的强缓存都存在难度,不可避免的耦合缓存检测相关的代码到主代码中。而基于Service Worker的缓存解决方案可以将检测与缓存逻辑和主代码完全解耦,并利用浏览器本身的特性平滑的控制缓存文件。
Web Worker
在介绍Service Worker之前,有必要先了解其诞生背景及相关生态。我们知道JS在浏览器中是单线程运行的,这导致了数据的更新和UI的渲染这两项任务都只能统一处理。当JS复杂度变高时,单线程的环境限制必然会导致UI渲染的阻塞问题。为了解决这个问题提升浏览器性能,HTML5提出了Web Worker规范定义了一套API,允许JS运行在主线程之外的线程中,这一类线程就是Web Worker了。一般可分为如下三类。
Dedicated Worker
该类worker仅能被生成它的JS所使用,其父页面关闭时,它的生命周期也随之终止。
Shared Worker
该类Worker可以被同域下的所有JS使用,关联的所有页面关闭时该worker才会终止。
Service Worker
这也是今天要讨论的重点了,该类worker的生命周期和页面无关,是基于事件驱动的worker。
从以上生命周期的表现也可以看出,前两类worker和Service Worker的职责肯定是有类型上的区别的。前者主要用于解决高复杂度JS的性能问题,将复杂的计算任务分担到这两类线程上执行并通过消息机制实现计算结果的通信。而后者专注于提升web应用的体验,扮演角色为web应用和网络之间的代理服务器,有着提供有效的离线体验,拦截网络请求,推送通知等等特性。
可以看出Service Worker相比于普通的worker拥有着更重大的职责。
生命周期
既然要了解Service Worker如何使用,就必须要知道其从启动到终止究竟发生了什么。
register
首先需要检测SW是否可用,而SW本身是挂载到navigator上的对象,按如下方法检测即可。
if ('serviceWorker' in navigator) {
...
}
接下来就可以通过路由注册SW了。那么现在有个问题,注册时机在什么时候比较好呢?既然是要拦截请求,那么注册是不是越快越好以免请求丢失呢?但要知道,浏览器开启一个新的worker也是要消耗不少内存的,如果太早注册SW,第一次加载的体验肯定不会好。实际上W3C在制定相关规范时也考虑到了该问题,其设定SW即使在网页加载完成时也能拦截已经发出的请求。那么为了不影响用户体验,直接在onload事件中注册即可。
window.addEventListener('onload', function() {
navigator.serviceWorker.register('/sw.js').then(...);
})
这里需要注意一点,SW是有clients
和作用域的概念的,clients
即是享受SW服务的页面更或者worker,其功能只对作用域内的clients
有效。默认作用域为注册URL的层级,也就是说如果注册文件更改为/app/sw.js
的话,SW只会拦截/app
下的请求了。可以通过navigator.serviceWokrer.controller
查看某个页面是否处于SW的作用域内。
install
注册如果失败,则register
的Promise会呈拒绝态并销毁SW。成功后,第一个SW就初始化完成,进入安装流程。每个SW只会触发一次安装流程,也就是说以后每次修改SW后都会再次触发新的安装流程。通常监听该生命周期来处理缓存文件的获取。举例如下:
self.addEventListener("install", event => {
event.waitUntil(
caches.open("static-v1").then(cache => cache.addAll(["/js/main.js", "/css/main.css"]))
);
});
当以上缓存文件都获取成功时则可以进入下一步骤了。但需要注意,如果有一个文件下载失败,则安装失败,浏览器将会废弃这个SW。
fetch
我们上面已经提到了,如果文件下载失败,SW就会被废弃,这肯定不是我们想看到的,那么挽救的方法就可以通过监听该事件来完成。该事件的回调会传入一个event对象,包含本次请求的结果,如果失败我们可以手动执行请求并再次尝试缓存。示例如下:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
const fetchRequest = event.request.clone();
return fetch(fetchRequest).then(
function(response) {
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
可以看到这里我们频繁使用到了caches对象,这是SW提供的控制缓存相关API,在下文中再讲解。
activate
安装完成后就进入激活阶段了,这个阶段说明SW已经正常工作了,可以做一些功能型的事情。比如调用clients.claim
让没被控制的client
受控,检查文件是否过期等等。
update
这部分比较麻烦,因为SW是对时间比较敏感的模块,所以这部分主代码的更新会有很多细节问题。流程如下。
- SW文件更新,新的SW开始下载并进入
install
事件。 - 此时旧的SW仍然在工作并处于
activate
状态和新的SW共存,而新的SW处于waiting
状态 - 只有当旧的SW转换为
terminated
状态后,也就是网页被关闭一段时间或者手动清除SW,新的SW才会切换为activate
状态接管代理 - 此时旧的缓存文件仍然保存在
caches
中,通常需要监听新的SW的active
状态来更新缓存信息。
如果不想走这套等待替换的机制的话,也可以通过skipWaiting
方法强制替换。但明显的,如果有用户正在网页中就会造成相关资源丢失。该方法的应用也要看具体时机。
Cache Object和CacheStorage
之前也看到了,SW需要频繁和caches对象打交道。这个对象就是CacheStorage,该对象可以直接在window域下使用。可以理解为一个顶级的缓存目录管理,其中存放着所有Cache Object,也提供了相关的增删改查API。
Cache Object就是真正存储SW缓存的地方了,其通过fetch的流形式来处理内容缓存。可以看出SW的缓存管理实际上套了两层,外层通过CacheStorage查询对应的缓存管理对象,内层再通过缓存管理对象真正处理缓存文件内容。
结合Webpack工具流
为了方便开发者使用SW,Chrome提供了一个名为sw-precache的库,结合sw-precache-webpack-plugin就可以轻松的将SW集成到项目中。以该博客为例的配置如下。
new SWPrecachePlugin({
cacheId: 'blog',
filename: 'service-worker.js',
minify: true,
// 缓存hash,否则更换同名文件时sw无法缓存
dontCacheBustUrlsMatching: false,
// 过滤不需要缓存的文件
staticFileGlobsIgnorePatterns: [
/\.json$/,
/index\.html$/,
/\.map$/,
/\.css$/,
/\.eot$/],
// 合并静态资源处理配置
mergeStaticsConfig: true,
// 缓存未经过webpack打包的静态文件
staticFileGlobs: [
path.join(__dirname, '../dist/static/*.*')
],
// 处理真实访问url和缓存url不一致的问题
stripPrefixMulti: {
[path.join(__dirname, '../dist/static')]: '/static'
},
runtimeCaching: [
{
urlPattern: '/',
handler: 'networkFirst'
},
{
urlPattern: /service-worker.js/,
handler: 'networkOnly'
},
{
// note that this pattern will cache ajax request
urlPattern: /(.+\/[^\.]*$)/,
handler: 'networkFirst',
options: {
cache: {
maxEntries: 30,
name: 'blog-runtime-cache'
}
}
},
{
urlPattern: /\.(png|jpg|webp|gif)/,
handler: 'cacheFirst',
options: {
cache: {
maxEntries: 20,
name: 'blog-picture-cache'
}
}
}
]
})
小结
综上可以看出基于SW的强缓存处理还是有着很大的优势,缺陷在于该功能过于依赖浏览器本身的实现,无法polyfill,对于不支持的浏览器只好降级处理。
-- EOF --