PWA 与 Service Worker 缓存

225 阅读4分钟
  • TLDR(由 Notion AI 生成)

    PWA 与 Service Worker 缓存相关的内容包括 PWA 背景、Service Worker 和 Caches 的兼容性、Workbox 库的使用、缓存策略、手动唤起 PWA 安装提示、判断用户是否使用 PWA 应用、Service Worker 更新策略以及紧急卸载 Service Worker 的方法。

PWA 背景

Manifest

Manifest 字段:developer.mozilla.org/zh-CN/docs/…

Manifest 兼容性:developer.mozilla.org/en-US/docs/…

PWA(Manifest)兼容性较差,桌面端的 Safari、Firefox 无法使用 PWA,IOS上 Safari 和 Chrome 均无法使用

Service Worker

Service Worker 兼容性:developer.mozilla.org/en-US/docs/…

Caches 兼容性:developer.mozilla.org/en-US/docs/…

Service Worker 和 Caches 都可以在大部分浏览器中运行,具有较高的兼容性,可以在大部分浏览器上进行资源+接口缓存

PWA 技术相关

Workbox

使用 Workbox 可以简化 Service Worker 的编写。Workbox 包括两部分,Workbox 缓存库和编译时静态资源处理。Workbox 缓存库提供了通用的缓存函数和缓存策略,编译时静态资源处理主要用于汇总编译出的静态资源,并注入 Service Worker 源文件中实现 precache。

缓存策略

Workbox PreCache

对于打包产物的静态资源,使用预缓存的形式,在 Service Worker install 时资源就将缓存到 Caches 中。

Workbox precache 的缓存策略是 CacheFirst,所以尽量不对产物中的 HTML 文件做预缓存,避免出现更新不及时问题。HTML 文件可以走运行时缓存 NetworkFirst 策略。

静态资源缓存

所有的图片、字体、JS、CSS 静态资源都采用 StaleWhileRevalidate 策略缓存,即使用时优先使用缓存,并在后台同步请求资源更新本地缓存,当资源更新时将在下一次进入页面使用缓存时生效。缓存过期时间为 30 天。

接口缓存

考虑到缓存服务端容易出现状态同步问题,因此一般针对服务端接口做缓存。对于一些特殊接口,如接口幂等、更新频率低、请求资源大,一般采用 StaleWhileRevalidate 或者 NetworkFirst 缓存策略。

缓存带来的问题

缓存 HTML 文件可能会导致用户无法第一时间访问到最新资源,导致无法使用最新功能,带来一些问题,如开发时无法第一时间访问本地环境、测试时无法正确访问到测试环境等。

因此对 HTML 文件使用 NetworkFirst 策略进行缓存,确保用户正常使用时可以请求到最新的 HTML 资源,同时用户网络环境差、离线时也可以使用本地缓存打开页面。

手动唤起PWA安装提示

可以通过阻止并缓存 BeforeInstallPrompt 事件以实现在 js 中手动唤起 PWA 安装提示。

但是这种方法存在问题:

  1. BeforeInstallPrompt 事件的prompt()方法只能调用一次,无法实现常驻按钮点击下载。但是在目前支持 BeforeInstallPrompt 事件的浏览器中,关闭 PWA 安装弹窗后,浏览器会再次触发 BeforeInstallPrompt 事件,因此可以在全局监听 BeforeInstallPrompt 事件并缓存,使用事件时永远使用最新的 BeforeInstallPrompt 事件(开发时极有可能因为闭包导致使用了旧的事件而无法唤起安装提示) 。市面上多数产品(抖音、小红书)也是通过这种方式多次唤起安装提示
  2. BeforeInstallPrompt 事件存在兼容性问题,在 Firefox 和 Safari 中无法手动唤起 PWA 安装提示。developer.mozilla.org/zh-CN/docs/…
const installPwa = async () => {
  if (!window.deferredPrompt) {
    // 延迟提示不存在。
    return;
  }

  // 显示安装提示。
  const promptEvent = window.deferredPrompt;
  promptEvent.prompt();
  const result = await promptEvent.userChoice;
};

window.addEventListener('beforeinstallprompt', (event) => {
  // 阻止 Chrome 67 及更早版本中默认的安装提示
  event.preventDefault();

  // 保存事件,以便稍后触发
  window.deferredPrompt = event;
});

判断用户是否正在使用PWA应用

浏览器没有提供精确判断 PWA 应用内的 api,只能根据当前 display-mode 是否是 standalone 来做模糊判断。

const isPwa = () => {
  // iOS设备
  if ('standalone' in navigator && navigator.standalone) {
    return true;
  }
  // 安卓和其他设备
  if (window.matchMedia('(display-mode: standalone)').matches) {
    return true;
  }
  return false;
}

Service Worker更新策略

默认情况下 Service Worker 的请求和安装是后台执行的,当新版本的 Service Worker 请求之后并不会立即启用,和 VSCode 一样,使用旧版本时在后台请求新版本,新版本请求后会仍使用旧版本,直到用户重启 VSCode。

在 Service Worker 中可以使用skipWaiting跳过等待,使当前页面立即使用新版本的 Service Worker。

self.skipWaiting();

在 Service Worker 更新时,新的 Service Worker 并不会接管已打开的旧版本 Service Worker 标签页,可以使用clients.claim()强制接管所有已打开的标签页。

self.addEventListener("activate", () => self.clients.claim());

紧急卸载Service Worker

如果因为缓存导致出现线上问题,如新功能无法使用等,则将register.js下线并替换为以下代码卸载 Service Worker

if ("serviceWorker" in navigator) {
  navigator.serviceWorker
    .getRegistration(window.location.host)
    .then((res) => {
      if (res && res.unregister) {
        res.unregister();
      }
    });
}

部署该段代码后,所有用户在刷新页面后都将注销 Service Worker。