前言
随着技术的不断发展,应用的性能已经优化用户体验逐渐成为重中之重。所以网页秒开就是提升用户体验重要的一部分。
我负责的业务线是广告投放、推广,经常会碰到高并发,刚上线时经常因此导致服务器崩溃,页面打不开,被用户投诉,只能和后端小伙伴们一点点优化。这次记录一下网页离线缓存
技术。可以使用户在无网络、弱网、服务器崩溃的情况下有一个较好的用户体验,延长用户留存时间。
Service Worker
类似一个在web应用程序和浏览器之间的代理服务器,javaScript是单线程的运行方式,而Service Worker
独立于主线程不会造成js堵塞,因此它也不能访问dom
元素,可以通过postMessage
与页面实现通信。Service Worker
可以实现向后台传递消息
、网络代理
、请求转发
、伪造响应
、离线缓存
、消息推送
等功能。处于安全性考虑,Service Worker
只能被使用在https
环境或者本地环境
下。
Service Worker
基于事件监听机制。最常用的事件包括以下几个:
install
安装Service Worker
,通常在这个事件缓存资源,以便于用户在下次访问时能提速。activate
第一次加载会在install
之后被触发。fetch
激活后对网页的请求拦截。message
在接收到消息的时候执行。
使用Service Worker
注册服务
因为Service Worker
兼容性较低,所以需要先判断浏览器是否支持。将Service Worker
放到load()
事件内加载是为了保证优先加载页面资源,先保证页面渲染,不占用额外的cpu。
图为 caniuser.com
提供的兼容性:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
<script src="js/jqurey.js"></script>
</head>
<body>
<img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/154cd131b76e431897c2c04af97880df~tplv-k3u1fbpfcp-watermark.image" width="100px">
<div>这是一串文字</div>
<div class="list"></div>
<script>
let baseUrl = ""
function getList() {
$.ajax({
type: "get",
url: "http://localhost:3000/api/getList",
data: {},
dataType: "json",
success: res => {
let html = ""
for (let i in res.data) {
html += "<div>" + res.data[i].name + "</div>"
}
$(".list").html(html)
}
})
}
getList()
</script>
<script>
window.addEventListener("load", event => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("sw.js")
.then(registration => {
console.log("注册成功");
})
.catch(error => {
console.log("注册失败", error);
});
}
});
</script>
</body>
</html>
这里模拟了大多数网页的场景,页面包含文字、图片以及ajax资源(Service Worker
无法缓存post
请求)。
register()
可以注册一个服务,这个方法接收两个参数,第一个为Service Worker
要加载的文件,第二个参数可选,用来指定要控制的文件目录,默认是跟sw.js
所在的同级目录。该方法返回一个 Promise
。如果注册失败,可以通过 then和catch
来捕获成功或错误信息。
安装 installing
sw.js
是Service Worker
的核心模块,里面定义了各个生命周期如何去缓存资源。当我们注册完Service Worker
后,就会触发install
事件。
var verson = "v1"
self.addEventListener('install', event => {
event.waitUntil(caches.open(verson).then(cache => {
return cache.addAll(['demo.html', 'js/jqurey.js','https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/154cd131b76e431897c2c04af97880df~tplv-k3u1fbpfcp-watermark.image'])
}))
})
waitUntil
该事件接收一个promise
,可以延长事件的作用事件。caches.open(String)
可以创建名为String的缓存空间,将需要缓存的文件添加进去;cache.addAll(Array)
选择哪些文件被缓存;
此时我们打开控制台 > Application > Cache Storage
会发现多出了一个叫v1的缓存库。里面缓存的资源正好是我们设置的资源。
激活 activated
只有在第一次安装Service Worker
会进入activated
状态,如果有正在运行中的Service Worker
则该事件不会执行。需要等待新的Service Worker
被安装或者调用self.skipWaiting()
才会执行。
activated
阶段可以更新缓存。
self.addEventListener('activate', event => {
console.log("activate")
var fn = caches.keys().then(function (cacheList) {
return Promise.all(
cacheList.map(function (cacheName) {
if (cacheName !== verson) {
return caches.delete(cacheName);
}
})
);
})
event.waitUntil(Promise.all([fn])
.then(() => {
return self.clients.claim()
})
)
});
claim()
让新的Service Worker
接管页面
fetch
写到这一步我们断开网络刷新页面,发现还是提示的无网络页面,这是因为少了最重要的一步:监听fetch
事件,该事件会拦截请求,判断缓存是否存在,优先取缓存中的数据,缓存中不存在的话就继续请求。
self.addEventListener('fetch', function (event) {
console.log("fetch",event.request.url)
event.respondWith(
caches.match(event.request).then(function (response) {
if (response) {
return response
}
return fetch(event.request)
})
)
})
至此,我们就完成了静态资源的缓存。这时我们来测试一下,修改页面的文字内容以及接口返回的数据刷新页面对比一下,可以看到只有接口返回的数据发生了变化,页面的文字修改并没有更新到页面。
修改前:
修改后:
如果我们想将接口的数据也缓存起来,可以将请求拷贝出来放入缓存,如果请求失败或者post
方式则直接返回,代码如下:
self.addEventListener('fetch', function (event) {
console.log("fetch", event.request.url)
event.respondWith(
caches.match(event.request).then(function (response) {
if (response) {
return response;
}
var request = event.request.clone();
return fetch(request).then(function (httpRes) {
if (!httpRes || (httpRes.status !== 200 && httpRes.status !== 304 && httpRes.type !== 'opaque') || request.method === 'POST') {
return httpRes;
}
var responseClone = httpRes.clone();
caches.open(verson).then(function (cache) {
cache.put(event.request, responseClone);
});
return httpRes;
});
})
);
});
更新机制
如果代码有了新版本,想让页面都得到更新怎么办呢?Service Worker
中的sw.js
每次都会被执行,利用这个特性我们可以更新sw.js
文件的版本号,然后在install
事件中调用 self.skipWaiting()
方法使Service Worker
直接进入activate
事件。然后在activate
事件内通过版本号判断删除旧版本缓存,然后通过elf.clients.claim()
更新缓存。
var verson = "v1"
self.addEventListener('install', event => {
console.log("install")
this.skipWaiting()
})
self.addEventListener('activate', event => {
console.log("activate")
caches.open(verson).then(cache => {
return cache.addAll(['demo.html', 'js/jqurey.js', 'https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/154cd131b76e431897c2c04af97880df~tplv-k3u1fbpfcp-watermark.image'])
})
var delFn = caches.keys().then(function (cacheList) {
return Promise.all(
cacheList.map(function (cacheName) {
if (cacheName !== verson) {
return caches.delete(cacheName);
}
})
);
})
event.waitUntil(Promise.all([delFn])
.then(() => {
return self.clients.claim()
})
)
});
self.addEventListener('fetch', function (event) {
console.log("fetch",event.request.url)
event.respondWith(
caches.match(event.request).then(function (response) {
if (response) {
return response
}
return fetch(event.request)
})
)
})