• 作者:老汪软件技巧
  • 发表时间: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