- 作者:老汪软件技巧
- 发表时间:2024-08-25 21:13
- 浏览量:
背景
做了一个基于 vue3 的web项目,该项目有离线缓存需求
运行在 electron 提供的 Chrome 浏览器中离线时首页不能白屏,有部分基础功能该网页可能一直不关闭,所以需要定时刷新更新版本理解 Service Worker
请看概念篇
理解 Workbox
上面我们已经确定了,需要使用 SW 缓存开发。那 Workbox 又是什么呢?
是 Google Chrome 团队推出的一套 PWA 的解决方案,这套解决方案当中包含了核心库和构建工具,因此我们可以利用 Workbox 实现Service Worker的快速开发。
workbox-webpack-plugin 是什么
我们项目是基于 webpack 构建的,而谷歌又提供了 Workbox 的 webpack 插件 -> workbox-webpack-plugin
workbox-webpack-plugin 使用
安装npm install workbox-webpack-plugin
引入插件并配置这里有两种配置方式,我们可以一起看看:
第一种:GenerateSW
通过配置在项目中引入 service.worker.js适用于:
const WorkboxPlugin = require('workbox-webpack-plugin');
module.exports = {
plugins: [
new WorkboxPlugin.GenerateSW({
// Do not precache images
exclude: [/\.(?:png|jpg|jpeg|svg)$/],
// Define runtime caching rules.
runtimeCaching: [{
// Match any request that ends with .png, .jpg, .jpeg or .svg.
urlPattern: /\.(?:png|jpg|jpeg|svg)$/,
// Apply a cache-first strategy.
handler: 'CacheFirst',
options: {
// Use a custom cache name.
cacheName: 'images',
// Only cache 10 images.
expiration: {
maxEntries: 10,
},
},
}],
})
]
};
第二种:InjectManifest
通过已有的 service-worker.js 文件生成新的 service-worker.js适用于:
new workboxPlugin.InjectManifest({
// 目前的service worker 文件
swSrc: './src/sw.js',
// 打包后生成的service worker文件,一般存到disk目录
swDest: 'sw.js'
})
思路
service-worker.js 编写第一步,通常需要编写 service-worker.js 脚本。这里我们使用 workbox-webpack-plugin 插件,通过各种配置(如缓存策略配置)来自动生成相应的 service-worker.js
注册 Service Worker一般来说,我们可以使用原生代码注册 SW 即可,如
// Check that service workers are supported
if ('serviceWorker' in navigator) {
// Use the window load event to keep the page load performant
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
}
但由 vue 脚手架生成的项目,默认带有register-service-worker 。这个三方库能代替原生代码进行SW注册。在这个库里,抛出了自定义 Service Worker 实例的状态,对麻烦的 Service Worker 更新做了处理,抛出了 updated 自定义事件,供用户自行处理。所以本项目使用默认的 register-service-worker
基础功能实现service-worker.js 脚本生成
思路
打包配置 vue.config.js 里,增加 SW 相应的更新机制和缓存策略。
通过需分析求,我们设置了三种缓存模式将生成的文件命名为 sw.js代码
module.exports = {
...
pwa: {
workboxPluginMode: "GenerateSW",
workboxOptions: {
importWorkboxFrom: "local",
swDest: "sw.js", // 固定的脚本名
cacheId: "pro-v1", // cache key
skipWaiting: true,
clientsClaim: true,
cleanupOutdatedCaches: true,
navigationPreload: true,
runtimeCaching: [
// 缓存配置
// 静态资源默认使用缓存
{
//(策略NetworkFirst 优先用网络,网络失败用缓存)仅缓存首页用到的接口
urlPattern: /^https:\/\/main.domain.com\/xxx/,
handler: "NetworkFirst",
options: {
cacheName: "api",
networkTimeoutSeconds: 10000,
expiration: {
maxAgeSeconds: 60 * 24 * 60 * 60, // 这只最长缓存时间为2个月
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
// (策略StaleWhileRevalidate 优先用缓存,发请求提供下一次更新使用)
urlPattern: /^https:\/\/main.u.com.*/,
handler: "StaleWhileRevalidate",
options: {
cacheName: "ujs",
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
//下载图片(策略 Cache First,只缓存成功的)
urlPattern: /^https:\/\/main.pic.com.*.(?:png|jpg|jpeg|svg|gif|PNG|JPG|JPEG|SVG|GIF)/,
handler: "CacheFirst",
options: {
cacheName: "images",
expiration: {
//maxEntries: 30, 最大的缓存数,超过之后则走 LRU 策略清除最老最少使用缓存
maxAgeSeconds: 60 * 24 * 60 * 60,
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
]
}
}
结果
上述配置后,我们在编译后的 dist 文件夹,就会变成这样
- dist
- css
- js
... // 原有的
- workbox-v4.3.3
- sw.js
我们会有 workbox 文件夹,它是个解决方案集合包,当中包含了核心库和构建工具
我们还有一个 sw.js 文件,这就是通过配置生成的 Service Worker 脚本
注册 Service Worker
我们现在已经有了 Service Worker 脚本,现在需要注册它。
配置
// package.json
"dependencies": {
"register-service-worker": "^1.7.2",
}
注册文件
新建一个 registerServiceWorker.js 文件,在该文件里引入 register-service-worker 插件,并注册 Service Worker 脚本
参考git仓做了如下配置,由于我们不想激活 SW 后刷新才能接管,所以执行了刷新操作(一般情况,激活后,要刷新后 SW 才会接管资源)
import { register } from "register-service-worker";
if (process.env.NODE_ENV === "production") {
// 我们知道 sw.js 位于 dist 里,也就是 BASE_URL 里
register(`${process.env.BASE_URL}sw.js`, {
ready() {
console.log("Service worker is active.");
},
registered() {
console.log("Service worker has been registered.");
},
cached() {
console.log("Content has been cached for offline use.");
},
updatefound() {
console.log("New content is downloading.");
},
updated() {
console.log("New content is available; please refresh.");
window.location.reload(); // 强制刷新以接管请求
},
offline() {
console.log("No internet connection found.");
},
error(error) {
console.error("Error during service worker registration:", error);
},
});
}
注册
在入口文件里引入 registerServiceWorker.js
// index.js
import "./registerServiceWorker";
Bug 分析缓存多余文件
项目中,原本对 u.js 进行了缓存。但在测试环境中,无法正常访问 u.js,针对 u.js 的缓存任务无法停止 (生产环境是可以的)。原因是在Chrome浏览器中,当新的 SW 执行替换操作时,检测到旧SW存在未完成的任务,会取消替换,导致无法安装新SW。
解决方案:不缓存 u.js
// vue.config.js
pwa: {
...
// 移除此配置
// {
// urlPattern: /^https:\/\/main.u.com.*/,
// handler: "StaleWhileRevalidate",
// options: {
// cacheName: "ujs",
// cacheableResponse: {
// statuses: [0, 200],
// },
// },
// },
}
静态资源缓存失败
在站点部署过程中,访问站点,由于项目有灰度功能,就会出现新老 SW 实例并存的情况。这时候一部分资源被 旧SW 接管,当 新SW 安装成功时,执行了强刷动作,此时又有一部分资源被 新SW 接管。这样一来,资源就会出现混乱,会返回错误的缓存。
这时候,要做的就是清除缓存,从头再来
解决方案:在index.html中捕获全局错误,当资源加载失败时,清除 SW 缓存内容,并重新加载页面。
// index.html
<script type="text/javascript">
(function(){
window.addEventListener(
"error",
function(e) {
var url = e.target.href || e.target.src;
if (/\/js\/.*\.js/.test(url) || /\/css\/.*\.css/.test(url)) {
setTimeout(function() {
if (
Object.prototype.toString.call(caches) ===
"[object CacheStorage]"
) {
caches
.keys()
.then(function(keyList) {
return Promise.all(
keyList
.filter(function(key) {
return key !== "images";
})
.map(function(key) {
return caches.delete(key);
})
);
})
.then(tryReload);
}
}, 5000);
}
},
true
);
//刷新策略
function tryReload() {
window.location.reload();
}
})()
script>
TODO其实这里,在 updated 里清除缓存效果应该一样。
进阶需求
由于场景特殊,该网页可能长达一个月不关闭不刷新,那这时候,如果更新版本了,那又没有重新刷新界面,可能就无法及时更新。这时候就有了定时刷新需求:于1:30~2:30时,检查是否有更新,若存在,则主动更新并刷新
// registerServiceWorker.js
import { register } from "register-service-worker";
if (process.env.NODE_ENV === "production") {
...
}
/**
* control {promise} control状态完成后再执行更新sw操作
* 基于updated中的reload方法进行重载页面,主动更新service-worker
*/
export function updateServiceWorker(control) {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready
.then((registration) => {
control.then(() => registration.update());
})
.catch((error) => console.log(error));
}
}
// index.js
import { updateServiceWorker } from "./registerServiceWorker";
function timeCheck(){
//自动修正定时器时间,每隔一小时检查一次,
const now = dayjs();
const timeEnd = now.minute(59).second(59);
const interval = timeEnd.diff(now);
setTimeout(function() {
const now = dayjs();
const start = now
.hour(1)
.minute(30)
.second(0)
.millisecond(0);
const end = start.add(1, "hour");
if (now.isBetween(start, end, "millisecond")) {
updateServiceWorker(Promise.resolve());
}
midnightCheck();
}, interval);
}
timeCheck();
参考
谷歌文档
workbox-webpack-plugin
register-service-worker