原创首发于微信公众号:mp.weixin.qq.com/s/2MpWdflG8…
万字长文警告! 收藏 + 关注,学习不迷路 ~
你好,我是小甲,一枚前端开发攻城狮。
有这样一个场景不知道你有没有留意过:
你正在浏览网页,突然发现断网了!这时候你可能在页面上胡乱地点几下,但是你发现浏览的网站仍然能正常显示内容——就好像,你正在用“云”流量来上网……
你不觉得奇怪嘛 ~
明明没有网络,为啥浏览的网站仍然能正常工作呢?
这就是今天要分享的主题:离线缓存。
说到离线缓存,你可能会想到浏览器的 Cache API。没错,Cache API 确实可以为我们提供离线缓存的能力。不过,Cache API 的功能较为有限,它只能缓存 HTTP 响应。
那有没有其他方案呢?
答案是肯定的——Service Worker 最适合干这件事。
因此,我们今天要分享的完整内容主题便是:Service Worker 实现离线缓存。
如果你想让你的用户在离线状态下能继续使用你的网站,那么,你一定不要错过今天的内容。我可以保证,通过这篇文章,你将对 Service Worker 有更深入的理解,并能在你的项目中实现离线缓存的功能。
今天的内容主要有:
- 注册 Service Worker;
- 如何缓存资源;
- 如何拦截请求;
- 如何更新缓存;
- 离线分析用户行为数据;
- 最佳实践和注意事项。
如果你觉得不过瘾,还可以去看看上一篇关于 Service Worker 实现“推送通知”的文章,相信也会对你有所启发:当你的网站有新内容时,如何立即通知你的用户呢?。
现在,就让我们正式开始今天的内容吧 ~
1. 基础知识
关于什么是 Service Worker,在《当你的网站有新内容,如何立即通知你的用户呢?》一文中有提及,这里我再简单介绍一下。
Service Worker 运行在浏览器后台,其独立于网页,不会因为网页的关闭而停止运行。也就是说,Service Worker 能在没有用户交互的情况下执行任务,比如接收推送通知,或者是我们今天分享的——实现离线缓存。
那么,什么是离线缓存呢?离线缓存,顾名思义,就是在没有网络连接的情况下,通过缓存的方式让用户仍然能访问到网站的内容。这是通过 Service Worker 的拦截请求和返回缓存内容的能力实现的。
除此之外,你还需要了解 Service Worker 的生命周期。
Service Worker 的生命周期主要包括三个阶段:安装(install)、激活(activate)和等待(idle)。
在安装阶段,我们可以预缓存一些资源;在激活阶段,我们可以清理旧的缓存;在等待阶段,Service Worker 就可以开始接收和处理事件了。
那它的工作原理是什么呢?
首先,当用户第一次访问我们的网站时,Service Worker 会被安装并激活,然后我们可以在 Service Worker 中缓存需要的资源。这样,当用户再次访问我们的网站时,Service Worker 就可以拦截请求,直接返回缓存的资源,而不需要从网络获取。
这就是离线缓存的基本工作原理。
在你了解了上面这些基础知识后,我们就可以着手实现离线缓存的功能了。
准备好了嘛?
Let’s go ~
2. 注册 Service Worker
现在,我们就要进入实战环节了。第一步要做的,就是注册 Service Worker。
注册 Service Worker 的目的,是让浏览器知道我们要在网站上使用 Service Worker,并告诉浏览器 Service Worker 的位置。
不过呢,在注册 Service Worker 之前你需要先检查一下浏览器是否支持 Service Worker。
if ('serviceWorker' in navigator) {
// 浏览器支持 Service Worker
// 稍后,将注册 Service Worker 的代码放在这里……
} else {
// 浏览器不支持 Service Worker
}
如果浏览器支持 Service Worker,那我们就可以开始注册了。
navigator.serviceWorker.register('/sw.js')
.then(function(registration) {
// 注册成功
console.log('Service Worker 注册成功,作用域是: ', registration.scope);
})
.catch(function(err) {
// 注册失败
console.log('Service Worker 注册失败: ', err);
});
在上面这段代码中,注册 Service Worker 有两点需要注意:
- 注册 Service Worker 是一个异步操作。所以,
navigator.serviceWorker.register
会返回一个 Promise,可以使用.then
来处理注册成功的情况,使用.catch
来处理注册失败的情况。 - 注册时需要告诉浏览器 Service Worker 的文件位置。通常情况下,Service Worker 文件的位置放在网站根目录即可。代码中的
/sw.js
就是 Service Worker 的文件位置。
是不是很简单?Service Worker 的注册过程就是这几行代码。归纳起来有两点:先检查浏览器是否支持,如果支持就可以注册 Service Worker 了。
现在,我们已经成功注册了 Service Worker,那么,该如何缓存资源呢?
3. 缓存资源
前面我们提到 Service Worker 的生命周期主要包括三个阶段,那么,缓存资源这一步应该放在哪个阶段呢?
没错,在安装阶段。
在 Service Worker 安装阶段,把需要的资源都缓存起来,这样在没有网络的的时候,我们就可以从缓存中取出这些资源。
具体该怎么实现呢?
其实很简单,只需要在 Service Worker 的 install
事件中,打开一个缓存,然后把我们需要的资源添加到缓存中即可。
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('my-cache').then(function(cache) {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/script.js',
'/logo.png',
]);
})
);
});
在这段代码中,首先监听了 Service Worker 的 install
事件,然后在这个事件中,我们打开了一个名为 ‘my-cache’ 的缓存,然后把我们需要的资源添加到这个缓存中。
要缓存的这些资源包括首页 HTML 文件、CSS 文件、JS 文件和一个图片文件。
你可能会注意到,这段代码中用到了一个 event.waitUntil
方法。这个方法的作用是什么呢?
event.waitUntil
的作用是让 Service Worker 等待我们的缓存操作完成。这个方法是一个异步操作,其接受一个 Promise 作为参数,Service Worker 会等待这个 Promise 完成,然后才会进入到下一个生命周期阶段。如果这个 Promise 被 reject,那么 Service Worker 的安装也就失败了,这个 Service Worker 也不会被激活。
你可能会想,会不会出现资源缓存失败的情况呢?
确实,这种情况是存在的。比如说,如果网络连接不稳定,或者某个资源的 URL 错误,那么这个资源就可能无法被缓存。在这种情况下,你可以在 cache.addAll
方法后面添加一个 .cache
方法,用来处理可能出现的错误。这样即便某个资源无法被缓存,Service Worker 也可以正常安装。
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('my-cache').then(function(cache) {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/script.js',
'/logo.png',
]).catch(function(error) {
console.log('资源缓存失败:', error);
});
})
);
});
到这里,我们已经把需要的资源都缓存起来了。当用户再次访问我们的网站时,我们就可以拦截请求并返回缓存的资源。
那么,拦截请求的功能,该如何实现呢?
4. 拦截请求
当我们把网站资源缓存后,用户再次访问我们的网站时,如果缓存中有对应的资源,Service Worker 就拦截所有的网络请求,将我们缓存中的资源返回给用户。当缓存中没有对应的资源时,再去请求资源展示给用户。
具体做法如下。
当网站发起网络请求时,会触发一个叫做 fetch
的事件,我们可以在 Service Worker 中通过监听这个事件来实现我们的需求。在这个事件中,我们就可以做到检查网站的缓存,看看是否有对应的资源。
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// 缓存中有对应的资源,直接返回
if (response) {
return response;
}
// 缓存中没有对应的资源,从网络获取
return fetch(event.request);
})
);
});
代码中的 event.respondWith
是 Service Worker 中一个比较重要的方法,通过函数名你应该也能猜到它是干什么用的。是的,它用于告诉浏览器我们该如何响应监听的事件。
当你在 Service Worker 中监听一个事件(比如这里的 fetch
事件)时,你就可以用 event.respondWith
方法来控制这个事件的响应。该方法接受一个 Promise 作为参数,这个 Promise 会解析成一个 Response 对象,这个对象就是我们的响应内容。
之后,caches.match(event.request)
会在我们的缓存中检查是否有对应的资源。如果有,那么 response
就是我们的缓存资源,我们可以直接返回这个资源。而如果没有,那么 response
就是 undefined
,这时,我们就需要从网络获取资源。
这就是 Service Worker 拦截请求并返回缓存的基本过程。通过这种方式,你可以在没有网络连接的情况下,让你的用户仍然能访问到一部分内容。
不过,你可能会注意到,如果我们的缓存中没有对应的资源,我们就会从网络上获取相应的资源。那么,如果这时候用户的网络连接处于断开的状态,那么用户是无法通过网络获取到资源的,用户仍然会看到一个错误提示。
那么,在这种情况下,有没有办法也给用户展示一些有用的信息呢?
答案是肯定的。
我们可以在 Service Worker 中预缓存一个离线页面,然后当无法从网络获取资源时,就返回这个离线的页面。
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// 缓存中有对应的资源,直接返回
if (response) {
return response;
}
// 缓存中没有对应的资源,从网络获取
return fetch(event.request).catch(function() {
// 网络获取失败,返回离线页面
return caches.match('/offline.html');
});
})
);
});
在这段代码中,当用户网络请求失败的情况下,给用户返回了一个预缓存的离线页面 offline.html
。通过这种方式,在用户没有网络连接的情况下,用户仍然能看到一个”正常“显示的页面,你可以在这个离线页面中写一些有用的信息展示给用户。这样做的好处是,可以极大提升用户体验,尤其是在网络环境不稳定的情况下。
这时候你可能会问,我们缓存的资源会一直保留在用户的设备上吗?如果我们的网站更新了,用户怎么能看到最新的内容呢?
别急,这就是我们接下来的内容:如何更新缓存。
5. 更新缓存
首先,你需要明白一点:Service Worker 的更新是由浏览器自动完成的。
当我们对 Service Worker 文件做了任何改动,浏览器就会认为 Service Worker 有更新,然后就会开始更新整个缓存流程。这个流程包括重新安装 Service Worker,然后在新的 Service Worker 激活后,旧的 Service Worker 就会被替换掉。
那么,我们如何在新的 Service Worker 安装后更新我们的缓存呢?
其实不难,我们只需要在 Service Worker 的 activate
事件中,删除旧的缓存,然后在新的 Service Worker 安装时,创建新的缓存即可。
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheName !== 'my-new-cache') {
// 如果这个缓存不是我们新创建的缓存,那么就把它删除
return caches.delete(cacheName);
}
})
);
})
);
});
上面的代码中,我们通过 caches.keys()
获取了所有的缓存名称,然后遍历这些缓存名称,如果缓存名称不是我们新创建的缓存名称,那么我们就删除这个缓存。这样,我们就可以保证用户的设备上只保留最新的缓存。
不过,即使 Service Worker 已经更新了,用户看到的可能仍然是旧的页面内容。
这是因为,虽然新的 Service Worker 已经激活,但是旧的 Service Worker 控制的页面(也就是用户当前打开的页面)在用户关闭浏览器或者刷新页面之前,仍然会被旧的 Service Worker 控制。所以,如果你想让用户立即看到最新的内容,这里就需要处理一下。
常见的处理方法有两种:立即获取控制权,静默更新站点内容;页面上提示用户手动刷新以更新网站缓存。
这两种方式各有优缺点,你需要根据你的用户群体和你网站的特性来决定用哪种方案。下面我会简单介绍一下这两种方案,你可以根据需要自行选择。
5.1 立即获取控制权
这种方式可以通过 activate
事件中调用 self.clients.claim()
方法来实现。
self.addEventListener('activate', function(event) {
event.waitUntil(self.clients.claim());
});
activate
事件会在 Service Worker 安装完成且没有其他版本的 Service Worker 控制着浏览器时触发。self.clients.claim()
的作用是强制所有在这个 Service Worker 控制下的浏览器立即受到这个 Service Worker 的控制。
这样,新的 Service Worker 就可以立即开始控制页面,用户也就可以立即看到最新的页面内容了。
这是一种”静默“的方式,也就是说,用户可能感知不到网站已经更新。其优点是,用户无需进行任何操作,但缺点是用户可能会错过重要的更新信息。如果你的项目是企业内部的管理系统的话,可以考虑这种方式,因为需求方很有可能就是你的用户,他们应该知道这次上线的版本都迭代了哪些功能。因此,在这种情况下,该方案是可行的。
但是你的用户如果对更新的内容比较感兴趣呢?比如你的企业维护着一个对外的平台,每次上线,用户对更新的内容都比较感兴趣。那么这种情况下,你可以考虑下面的第二种方案。
5.2 提示用户手动刷新页面
这个方案在许多网站上都非常常见,如果你对 Vue.js 比较熟悉的话,你应该留意过,他们官方文档的站点在每次更新后,网页右下角都会有一个全局提示框,提示你文档有更新,请刷新页面。
这种做法可以让用户有更多的控制权,他们可以自行决定什么时候加载新的内容。当然,有的网站会在提示信息中添加一个导航到”更新日志“的超链接,感兴趣的用户可以通过这个链接导航到更新日志页面,看看这次更新都有哪些新功能或者修复了哪些问题。
那么,这个功能该怎么实现呢?
要实现这个功能,你需要在 Service Worker 更新后,向页面发送一个消息,然后在页面上监听这个消息,并在收到消息后显示一个提示框。
具体做法就是,在 Service Worker 的 install
事件中,你可以通过 self.skipWaiting()
方法来让新的 Service Worker 跳过等待状态,直接进入激活状态。然后,在 activate
事件中,你可以向所有用户发送一个消息,告诉他们 Service Worker 已经更新。
self.addEventListener('install', function(event) {
self.skipWaiting();
});
self.addEventListener('activate', function(event) {
event.waitUntil(
self.clients.matchAll().then(function(clients) {
clients.forEach(function(client) {
client.postMessage({ type: 'SW_UPDATED' });
});
})
);
});
之后,你可以在页面上监听 message
事件,当收到 SW_UPDATED
类型的消息时,显示一个提示框,让用户来选择是否刷新页面。
这样,当 Service Worker 更新后,用户就会收到一个提示,他们可以选择是否刷新页面来加载新的内容。
这个方案的优点是用户可以自主选择何时加载新的内容,但缺点是需要用户手动进行操作。
不论选择哪种方案,都是为了能给用户一个好的使用体验,强烈建议你私下里尝试一下。
到这里,整个离线缓存的功能就已经实现了。不过呢,为了能更好地分析我们的用户画像,有时候我们需要在离线状态下也能收集一些用户行为的数据。
这就涉及到一个问题:如何在网络连接恢复后发送这些数据呢?
6. 离线分析
用户在离线状态下浏览我们的网站时,我们该如何知道他们的行为呢?比如,他们点击了哪些链接,浏览了哪些页面,或者是在表单中填写了什么信息。这些都是我们非常关心的问题,因为这些信息可以帮助我们更好地完善用户画像,从而能更精准地对他们提供服务。
那么,我们该如何收集这些信息呢?
要收集用户的这些行为信息,可以在 Service Worker 中监听用户的行为,比如点击事件、表单提交事件等。然后,我们可以把这些事件的信息保存到 IndexedDB 中。
IndexedDB 是一个运行在浏览器中的非关系型数据库,它可以在用户的设备上存储大量的结构化数据。而且,IndexedDB 是异步的,因此它不会阻塞浏览器的主线程。
之后,当网络连接恢复后,我们可以在 Service Worker 中监听 sync 事件。这个事件会在网络连接恢复后触发,我们可以在这个事件中把 IndexedDB 中的数据发送到服务器。
这样,我们就可以在网络连接恢复后,把用户在离线状态下的行为数据发送到服务器,然后在服务器端进行分析。
这就是离线分析的基本思路。
不过,IndexedDB 涉及到很多内容,这里因篇幅有限就不多赘述了,将来我会专门和大家分享这方面的应用实践。
下面是一段示例代码,这段代码展示了离线分析这一过程:
// 监听用户的点击事件
self.addEventListener('click', function(event) {
// 把点击事件的信息保存到 IndexedDB 中
// ...
});
// 监听网络连接恢复后的 sync 事件
self.addEventListener('sync', function(event) {
// 从 IndexedDB 中获取数据
// ...
// 把数据发送到服务器
// ...
});
通过这种方式,我们就能做到在用户离线状态下收集用户的行为数据,当网络连接恢复后再将这些数据发送到服务器。这对我们理解用户行为、完善用户画像以及优化产品服务等都非常有帮助。
7. 最佳实践和注意事项
在使用 Service Worker 实现离线缓存的功能时,一定要小心谨慎。若使用不当,很有可能会导致用户看到的内容一直是旧的页面,这就要求你要了解 Service Worker 的生命周期和更新问题。
再者,合理使用缓存,避免过渡消耗用户的设备存储空间。一定要根据具体的业务需求来选择合适的缓存策略,比如,我们可以只缓存关键的资源,或者我们可以提供一个设置项,让用户选择是否启用离线缓存。
最后就是 Service Worker 的兼容性问题了。虽然现代浏览器多数都支持 Service Worker,但是你仍然要考虑那些不支持的浏览器。所以,你也需要做好兼容性处理,确保在不支持 Service Worker 的浏览器上,你的网站仍然能正常运行。
8. 小结
今天的内容主要是借助 Service Worker 实现离线缓存,包括注册 Service Worker,缓存资源,拦截请求,更新缓存,以及离线分析等。相信,你现在对 Service Worker 肯定有了更深的理解,同时,我也希望你能在自己的项目中尝试一下。
这还只是 Service Worker 的冰山一角,它还有很多其他用途,比如后台同步等。借今天分享的机会,希望你能再去深入了解一下 Service Worker。
最后,如果你有任何问题或者想法,欢迎在下方评论区留言,大家一起交流、进步。
感谢你的阅读,我们下期再见 🙂
文章出处戳 这里 ~