vue3.5.12 vite 5.4.8使用service worker 缓存 vite-plugin-pwa

1、检查到有新版本 弹框提示 确认更新 取消不更新

vite.config.ts

...
// 引入 VitePWA 插件
import { VitePWA } from "vite-plugin-pwa"
export default defineConfig((mode): any => {
  return {
   
    ...
    plugins: [
      VitePWA({
        // 注册 Service Worker 的方式
        // registerType: "prompt", // 自动更新 Service Worker 并刷新页面(避免用户使用旧版本)
        registerType: "autoUpdate", // 自动更新 Service Worker 并刷新页面(避免用户使用旧版本)
        // 要包含到 PWA 的额外静态资源
        includeAssets: [
          "favicon.svg", // 网站的图标
          "robots.txt", // robots.txt 文件,用于搜索引擎优化
          "apple-touch-icon.png", // iOS 特定的桌面图标
          "**/*.png",
          "**/*.jpg",
          "**/*.jpeg",
          "**/*.svg",
          "**/*.woff2",
          "**/*.woff",
          "**/*.ttf",
          "**/*.otf"
        ],
        // // 用于精细地控制服务工作者中缓存和离线功能
        workbox: {
          globPatterns: ["**/*.{js,css,html,png,jpg,gif,svg,woff,ttf,otf,woff2}"],
          runtimeCaching: [
            {
              urlPattern: /\.(png|jpg|jpeg|svg|gif|woff|ttf|otf|woff2)$/, // 匹配图片文件请求
              // handler: 'NetworkOnly', // 始终只通过网络请求获取数据,不会去使用缓存中的内容
              handler: "CacheFirst" // 优先从缓存中查找响应,如果缓存中存在相应资源,就直接使用缓存内容返回给客户端,这样能实现快速响应,提升页面加载速度
            },
            {
              urlPattern: /\.(js|css|html)$/, // 匹配图片文件请求
              // handler: 'NetworkOnly', // 始终只通过网络请求获取数据,不会去使用缓存中的内容
              handler: "StaleWhileRevalidate" // 虽然一开始返回给客户端的是缓存中的旧资源,但它会在后台持续尝试更新缓存,只要网络请求成功,缓存中的资源就能尽快更新为最新版本,相对来说能让客户端在较短时间内获取到更新后的资源,保证资源的新鲜度在一个合理的时间范围内不断更新,更适用于那些内容有一定更新频率但又允许短暂使用旧内容的场景。
            }
        },
        // 配置 Web App Manifest
        manifest: {
          name: "web App", // 应用的全称,显示在安装提示或加载界面
          short_name: "App", // 应用的短名称,显示在桌面图标下方
          description: "web App built with Vite", // 应用的描述信息
          theme_color: "#3367D6", // PWA 的主题颜色,用于地址栏、通知栏等
          background_color: "#ffffff", // 启动页面的背景颜色
          display: "standalone" // 显示模式:standalone 模式模拟原生应用
        },

        // 开发环境的特定配置
        devOptions: {
          enabled: true, // 启用 PWA 功能,方便开发测试
          type: "classic", // 使用标准的 Service Worker 格式
          navigateFallback: "index.html" // 未命中的路由返回的默认页面
        }
      })
    ],
   ...
})

main.ts

import { registerSW } from 'virtual:pwa-register';

const updateSW = registerSW({
  onNeedRefresh() {
    console.log('新版本可用,请刷新页面以更新。');
    const userConfirmed = confirm('检测到新版本,是否立即刷新以应用更新?');
    if (userConfirmed) {
      updateSW(); // 激活新版本
    }
  },
  onOfflineReady() {
    console.log('应用已准备好离线使用。');
  }
});

2.1、在下一路由切换时更新缓存版本(vite-plugin-pwa 虚拟模块 virtual:pwa-register/vue 版本)

注意:页面刷新会造成 needRefresh 为false,所以在安装完缓存后,点击路由前,刷新页面了,再去点击路由,是不会更新缓存的,如果先避免这个问题,可以使用2.2中的原生PAI编写的方法

router.ts

import { useRegisterSW } from "virtual:pwa-register/vue"
const {
  needRefresh, // 是否需要刷新
  // offlineReady, // 是否已准备好离线使用
  updateServiceWorker // 手动更新 Service Worker
} = useRegisterSW()
// 后置守卫,保证每次路由跳转结束时关闭进度条
router.afterEach(() => {
  // 更新pwa缓存
  if (needRefresh.value) {
    updateServiceWorker()
  }
})

vite.config.ts

...
// 引入 VitePWA 插件
import { VitePWA } from "vite-plugin-pwa"
export default defineConfig((mode): any => {
  return {
   
    ...
    plugins: [
      VitePWA({
        // 注册 Service Worker 的方式
        registerType: "prompt", // 自动更新 Service Worker 并刷新页面(避免用户使用旧版本)
        // registerType: "autoUpdate", // 自动更新 Service Worker 并刷新页面(避免用户使用旧版本)
        // 要包含到 PWA 的额外静态资源
        includeAssets: [
          "favicon.svg", // 网站的图标
          "robots.txt", // robots.txt 文件,用于搜索引擎优化
          "apple-touch-icon.png", // iOS 特定的桌面图标
          "**/*.png",
          "**/*.jpg",
          "**/*.jpeg",
          "**/*.svg",
          "**/*.woff2",
          "**/*.woff",
          "**/*.ttf",
          "**/*.otf"
        ],
        // // 用于精细地控制服务工作者中缓存和离线功能
        workbox: {
          globPatterns: ["**/*.{js,css,html,png,jpg,gif,svg,woff,ttf,otf,woff2}"],
          runtimeCaching: [
            {
              urlPattern: /\.(png|jpg|jpeg|svg|gif|woff|ttf|otf|woff2)$/, // 匹配图片文件请求
              // handler: 'NetworkOnly', // 始终只通过网络请求获取数据,不会去使用缓存中的内容
              handler: "CacheFirst" // 优先从缓存中查找响应,如果缓存中存在相应资源,就直接使用缓存内容返回给客户端,这样能实现快速响应,提升页面加载速度
            },
            {
              urlPattern: /\.(js|css|html)$/, // 匹配图片文件请求
              // handler: 'NetworkOnly', // 始终只通过网络请求获取数据,不会去使用缓存中的内容
              handler: "StaleWhileRevalidate" // 虽然一开始返回给客户端的是缓存中的旧资源,但它会在后台持续尝试更新缓存,只要网络请求成功,缓存中的资源就能尽快更新为最新版本,相对来说能让客户端在较短时间内获取到更新后的资源,保证资源的新鲜度在一个合理的时间范围内不断更新,更适用于那些内容有一定更新频率但又允许短暂使用旧内容的场景。
            }
          skipWaiting: false, // 不跳过等待
          clientsClaim: true // 激活后是否主动接管页面
        },
        // 配置 Web App Manifest
        manifest: {
          name: "web App", // 应用的全称,显示在安装提示或加载界面
          short_name: "App", // 应用的短名称,显示在桌面图标下方
          description: "web App built with Vite", // 应用的描述信息
          theme_color: "#3367D6", // PWA 的主题颜色,用于地址栏、通知栏等
          background_color: "#ffffff", // 启动页面的背景颜色
          display: "standalone" // 显示模式:standalone 模式模拟原生应用
        },

        // 开发环境的特定配置
        devOptions: {
          enabled: true, // 启用 PWA 功能,方便开发测试
          type: "classic", // 使用标准的 Service Worker 格式
          navigateFallback: "index.html" // 未命中的路由返回的默认页面
        }
      })
    ],
   ...
})

2.2、在下一路由切换时更新缓存版本(操作原生service worker API 版本)

/utils/pwa.ts

let channel: any = {}
// 检查浏览器是否支持 BroadcastChannel API
if (typeof BroadcastChannel !== "undefined") {
  // 创建一个广播频道
  channel = new BroadcastChannel("pwa-update-channel")

  // 在其他标签页中监听广播消息,触发刷新
  channel?.addEventListener("message", (event) => {
    if (event.data === "refresh") {
      window.location.reload() // 刷新页面
    }
  })
} else {
  console.warn("BroadcastChannel API is not supported in this browser.")
}
export function initPwa() {
  localStorage.setItem("newWorkerState", "installed")
  if ("serviceWorker" in navigator) {
    const registerFilePath = import.meta.env.MODE == "dev" ? "/dev-sw.js?dev-sw" : "/sw.js"
    // 注册service worker
    navigator.serviceWorker.register(registerFilePath).then((registration) => {
      console.log("updatePwa ----- Service Worker registered with scope3:", registration) // 监听 waiting 状态的 Service Worker
      registration.addEventListener("updatefound", () => {
        const newWorker = registration.installing
        if (newWorker) {
          newWorker.addEventListener("statechange", () => {
            if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
              // 新版本已安装,但未激活
              if (localStorage.getItem("newWorkerState") !== "installed") {
                localStorage.setItem("newWorkerState", "installed")
              }
            }
            if (newWorker.state === "activated") {
              pwaActivate()
            }
          })
        }
      })
    })
    pwaActiveWatch()
  }
}
export function pwaPostMessage() {
  if ("serviceWorker" in navigator) {
    // 获取当前service worker
    navigator.serviceWorker.getRegistration().then((registration: any) => {
      console.log("pwaPostMessage --- Service Worker registered with scope:", registration) // 监听 waiting 状态的 Service Worker
      if (localStorage.getItem("newWorkerState") === "installed") {
        // 手动激活新 Service Worker
        const newWorker = registration.waiting
        newWorker && newWorker.postMessage({ type: "SKIP_WAITING" })
        localStorage.setItem("newWorkerState", "")
      }
    })
  }
}

// 需要vite-plugin-pwa插件配置 registerType: "autoUpdate" 是才可触发此事件
export function pwaActiveWatch() {
  if ("serviceWorker" in navigator) {
    navigator.serviceWorker.addEventListener("controllerchange", () => {
      console.log("检测到新版本,页面将重新加载!")
      pwaActivate()
    })
  }
}

// 激活广播
export function pwaActivate() {
  localStorage.setItem("testNewWorker1", "activated1")
  // 新版本激活刷新页面
  // 广播消息,通知其他标签页刷新
  try {
    channel?.postMessage("refresh")
  } catch (error) {
    localStorage.setItem("testNewWorker1", "error")
  }
  localStorage.setItem("testNewWorker2", "activated2")
  window.location.reload()
  localStorage.setItem("testNewWorker3", "activated3")
}


main.ts

...
import { initPwa } from "@/utils/pwa"

initPwa()
...

router.ts

// 后置守卫,保证每次路由跳转结束时关闭进度条
router.afterEach(() => {
  // 更新pwa缓存
  pwaPostMessage()
})

vite.config.ts

...
// 引入 VitePWA 插件
import { VitePWA } from "vite-plugin-pwa"
export default defineConfig((mode): any => {
  return {
   
    ...
    plugins: [
      VitePWA({
        // 注册 Service Worker 的方式
        registerType: "prompt", // 自动更新 Service Worker 并刷新页面(避免用户使用旧版本)
        // registerType: "autoUpdate", // 自动更新 Service Worker 并刷新页面(避免用户使用旧版本)
        // 要包含到 PWA 的额外静态资源
        includeAssets: [
          "favicon.svg", // 网站的图标
          "robots.txt", // robots.txt 文件,用于搜索引擎优化
          "apple-touch-icon.png", // iOS 特定的桌面图标
          "**/*.png",
          "**/*.jpg",
          "**/*.jpeg",
          "**/*.svg",
          "**/*.woff2",
          "**/*.woff",
          "**/*.ttf",
          "**/*.otf"
        ],
        // // 用于精细地控制服务工作者中缓存和离线功能
        workbox: {
          globPatterns: ["**/*.{js,css,html,png,jpg,gif,svg,woff,ttf,otf,woff2}"],
          runtimeCaching: [
            {
              urlPattern: /\.(png|jpg|jpeg|svg|gif|woff|ttf|otf|woff2)$/, // 匹配图片文件请求
              // handler: 'NetworkOnly', // 始终只通过网络请求获取数据,不会去使用缓存中的内容
              handler: "CacheFirst" // 优先从缓存中查找响应,如果缓存中存在相应资源,就直接使用缓存内容返回给客户端,这样能实现快速响应,提升页面加载速度
            },
            {
              urlPattern: /\.(js|css|html)$/, // 匹配图片文件请求
              // handler: 'NetworkOnly', // 始终只通过网络请求获取数据,不会去使用缓存中的内容
              handler: "StaleWhileRevalidate" // 虽然一开始返回给客户端的是缓存中的旧资源,但它会在后台持续尝试更新缓存,只要网络请求成功,缓存中的资源就能尽快更新为最新版本,相对来说能让客户端在较短时间内获取到更新后的资源,保证资源的新鲜度在一个合理的时间范围内不断更新,更适用于那些内容有一定更新频率但又允许短暂使用旧内容的场景。
            }
          skipWaiting: false, // 不跳过等待
          clientsClaim: true // 激活后是否主动接管页面
        },
        // 配置 Web App Manifest
        manifest: {
          name: "web App", // 应用的全称,显示在安装提示或加载界面
          short_name: "App", // 应用的短名称,显示在桌面图标下方
          description: "web App built with Vite", // 应用的描述信息
          theme_color: "#3367D6", // PWA 的主题颜色,用于地址栏、通知栏等
          background_color: "#ffffff", // 启动页面的背景颜色
          display: "standalone" // 显示模式:standalone 模式模拟原生应用
        },

        // 开发环境的特定配置
        devOptions: {
          enabled: true, // 启用 PWA 功能,方便开发测试
          type: "classic", // 使用标准的 Service Worker 格式
          navigateFallback: "index.html" // 未命中的路由返回的默认页面
        }
      })
    ],
   ...
})

3、在检测更新下载缓存资源的时候,弹出加载资源的提示框,到缓存完成立即更新

/utils/pwa.ts

let channel: any = {}
// 检查浏览器是否支持 BroadcastChannel API
if (typeof BroadcastChannel !== "undefined") {
  // 创建一个广播频道
  channel = new BroadcastChannel("pwa-update-channel")

  // 在其他标签页中监听广播消息,触发刷新
  channel?.addEventListener("message", (event) => {
    if (event.data === "refresh") {
      window.location.reload() // 刷新页面
    }
  })
} else {
  console.warn("BroadcastChannel API is not supported in this browser.")
}

// 派发PWA下载开始事件
function dispatchDownloadStartEvent() {
  window.dispatchEvent(new CustomEvent("pwa-download-start"))
  localStorage.setItem("testNewWorkerDispatchDownloadStartEvent", "down")
}

// 派发PWA下载完成事件
function dispatchDownloadCompleteEvent() {
  window.dispatchEvent(new CustomEvent("pwa-download-complete"))
  localStorage.setItem("testNewWorkerDispatchDownloadCompleteEvent", "complete")
}

export function initPwa() {
  if (!process.env.VUE_APP_PWA) {
    return
  }

  // 检查是否有下载标记,如果有则派发下载开始事件
  if (sessionStorage.getItem("pwa_downloading") === "true") {
    dispatchDownloadStartEvent()
  }

  localStorage.setItem("newWorkerState", "installed")
  if ("serviceWorker" in navigator) {
    const registerFilePath = import.meta.env.MODE == "dev" ? "/dev-sw.js?dev-sw" : "/sw.js"
    // 注册service worker
    navigator.serviceWorker.register(registerFilePath).then((registration) => {
      console.log("updatePwa ----- Service Worker registered with scope3:", registration) // 监听 waiting 状态的 Service Worker
      registration.addEventListener("updatefound", () => {
        const newWorker = registration.installing
        if (newWorker) {
          // 当新的 Service Worker 开始安装时,触发下载开始事件
          dispatchDownloadStartEvent()
          sessionStorage.setItem("pwa_downloading", "true")

          newWorker.addEventListener("statechange", () => {
            if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
              // 新版本已安装,但未激活
              if (localStorage.getItem("newWorkerState") !== "installed") {
                localStorage.setItem("newWorkerState", "installed")
              }
            }
            if (newWorker.state === "activated") {
              // 当 Service Worker 激活完成,触发下载完成事件
              dispatchDownloadCompleteEvent()
              sessionStorage.setItem("pwa_downloading", "false")
              pwaActivate()
            }
          })
        }
      })
    })
    pwaActiveWatch()
  }
}
export function pwaPostMessage() {
  if (!process.env.VUE_APP_PWA) {
    return
  }
  if ("serviceWorker" in navigator) {
    // 获取当前service worker
    navigator.serviceWorker.getRegistration().then((registration: any) => {
      console.log("pwaPostMessage --- Service Worker registered with scope:", registration) // 监听 waiting 状态的 Service Worker
      if (localStorage.getItem("newWorkerState") === "installed") {
        // 手动激活新 Service Worker
        const newWorker = registration?.waiting
        newWorker && newWorker.postMessage({ type: "SKIP_WAITING" })
        localStorage.setItem("newWorkerState", "")
      }
    })
  }
}

// 需要vite-plugin-pwa插件配置 registerType: "autoUpdate" 是才可触发此事件
export function pwaActiveWatch() {
  if (!process.env.VUE_APP_PWA) {
    return
  }
  if ("serviceWorker" in navigator) {
    navigator.serviceWorker.addEventListener("controllerchange", () => {
      console.log("检测到新版本,页面将重新加载!")
      pwaActivate()
    })
  }
}

// 激活广播
export function pwaActivate() {
  if (!process.env.VUE_APP_PWA) {
    return
  }
  localStorage.setItem("testNewWorker1", "activated1")
  // 新版本激活刷新页面
  // 广播消息,通知其他标签页刷新
  try {
    channel?.postMessage("refresh")
  } catch (error) {
    localStorage.setItem("testNewWorker1", "error")
  }
  localStorage.setItem("testNewWorker2", "activated2")

  // 在刷新页面前设置下载完成标志
  dispatchDownloadCompleteEvent()
  sessionStorage.setItem("pwa_downloading", "false")

  window.location.reload()
  localStorage.setItem("testNewWorker3", "activated3")
}


main.ts

...
import { initPwa } from "@/utils/pwa"

initPwa()
...

router.ts

// 后置守卫,保证每次路由跳转结束时关闭进度条
router.afterEach(() => {
  // 更新pwa缓存
  pwaPostMessage()
})

vite.config.ts

...
// 引入 VitePWA 插件
import { VitePWA } from "vite-plugin-pwa"
export default defineConfig((mode): any => {
  return {
   
    ...
    plugins: [
       VitePWA({
        // 注册 Service Worker 的方式
        registerType: "prompt", // 自动更新 Service Worker 并刷新页面(避免用户使用旧版本)autoUpdate
        // registerType: "autoUpdate", // 自动更新 Service Worker 并刷新页面(避免用户使用旧版本)
        // 要包含到 PWA 的额外静态资源
        includeAssets: [
          "favicon.svg", // 网站的图标
          "robots.txt", // robots.txt 文件,用于搜索引擎优化
          "apple-touch-icon.png", // iOS 特定的桌面图标
          "**/*.png",
          "**/*.jpg",
          "**/*.jpeg",
          "**/*.svg",
          "**/*.woff2",
          "**/*.woff",
          "**/*.ttf",
          "**/*.otf"
        ],
        // // 用于精细地控制服务工作者中缓存和离线功能
        workbox: {
          globPatterns: ["**/*.{js,css,html,png,jpg,gif,svg,woff,ttf,otf,woff2}"],
          runtimeCaching: [
            {
              urlPattern: /\.(png|jpg|jpeg|svg|gif|woff|ttf|otf|woff2)$/, // 匹配图片文件请求
              // handler: 'NetworkOnly', // 始终只通过网络请求获取数据,不会去使用缓存中的内容
              handler: "CacheFirst" // 优先从缓存中查找响应,如果缓存中存在相应资源,就直接使用缓存内容返回给客户端,这样能实现快速响应,提升页面加载速度
            },
            {
              urlPattern: /\.(js|css|html)$/, // 匹配图片文件请求
              // handler: 'NetworkOnly', // 始终只通过网络请求获取数据,不会去使用缓存中的内容
              handler: "StaleWhileRevalidate" // 虽然一开始返回给客户端的是缓存中的旧资源,但它会在后台持续尝试更新缓存,只要网络请求成功,缓存中的资源就能尽快更新为最新版本,相对来说能让客户端在较短时间内获取到更新后的资源,保证资源的新鲜度在一个合理的时间范围内不断更新,更适用于那些内容有一定更新频率但又允许短暂使用旧内容的场景。
            }
            // {
            //   urlPattern: /.*\.(?:js|css|html|json)/, // 缓存静态资源
            //   handler: "CacheFirst", // 使用缓存优先策略
            //   options: {
            //     cacheName: "static-cache", // 设置缓存名称
            //     expiration: {
            //       maxEntries: 2000, // 最大缓存条目数
            //       maxAgeSeconds: 60 * 60 * 24 * 7 // 缓存过期时间(30天)
            //     }
            //   }
            // }
          ],
          skipWaiting: true, // 不跳过等待
          clientsClaim: true // 激活后是否主动接管页面
        },
        // 配置 Web App Manifest
        manifest: {
          name: "web App", // 应用的全称,显示在安装提示或加载界面
          short_name: "App", // 应用的短名称,显示在桌面图标下方
          description: "web App built with Vite", // 应用的描述信息
          theme_color: "#3367D6", // PWA 的主题颜色,用于地址栏、通知栏等
          background_color: "#ffffff", // 启动页面的背景颜色
          display: "standalone" // 显示模式:standalone 模式模拟原生应用
        },

        // 开发环境的特定配置
        devOptions: {
          enabled: isRunPWA, // 启用 PWA 功能,方便开发测试
          type: "classic", // 使用标准的 Service Worker 格式
          navigateFallback: "index.html" // 未命中的路由返回的默认页面
        }
      }),
   ]
   ...
})

提示弹框vue 文件

<template>
  <CommonDialog
    v-model:commonVisible="dialogVisible"
    :dialogProps="{
      title: '系统更新中',
      width: '400px',
      closeOnClickModal: false,
      closeOnPressEscape: false,
      showClose: false
    }"
  >
    <div class="pwa-download-content">
      <div class="icon-wrapper">
        <el-icon class="loading-icon"><Loading /></el-icon>
      </div>
      <div class="message">
        <p>系统资源正在下载中,预计1~2分钟。请耐心等待...</p>
        <p>此过程不可中断,请勿关闭或刷新页面</p>
      </div>
    </div>
    <template #footer>
      <div class="download-footer">
        <p class="footer-tip">资源下载完成后会自动关闭此窗口</p>
      </div>
    </template>
  </CommonDialog>
</template>

<script setup lang="ts">
import { ref, onMounted, watch } from "vue"
import { useRoute, useRouter } from "vue-router"

const dialogVisible = ref(false)
const route = useRoute()
const router = useRouter()

// 检查是否有下载标记
const checkDownloadStatus = () => {
  const isDownloading = sessionStorage.getItem("pwa_downloading")
  dialogVisible.value = isDownloading === "true"
}

// 监听路由变化,确保在任何页面都显示弹框
watch(
  () => route.path,
  () => {
    checkDownloadStatus()
  }
)

onMounted(() => {
  checkDownloadStatus()

  // 监听自定义事件,用于打开/关闭对话框
  window.addEventListener("pwa-download-start", () => {
    sessionStorage.setItem("pwa_downloading", "true")
    dialogVisible.value = true
  })

  window.addEventListener("pwa-download-complete", () => {
    sessionStorage.setItem("pwa_downloading", "false")
    dialogVisible.value = false
  })
})
</script>

<style scoped lang="scss">
.pwa-download-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px 0;

  .icon-wrapper {
    margin-bottom: 20px;

    .loading-icon {
      font-size: 48px;
      color: #409eff;
      animation: rotate 2s linear infinite;
    }
  }

  .message {
    text-align: center;

    p {
      margin: 8px 0;
      color: #606266;
      font-size: 14px;
      line-height: 1.5;
    }
  }
}

.download-footer {
  width: 100%;
  text-align: center;

  .footer-tip {
    color: #909399;
    font-size: 12px;
  }
}

@keyframes rotate {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
</style>

4、如何卸载pwa

是的没有看错就是卸载,我们不否认 pwa的强大,但是 PC端使用的pwa 缺少了web 刷新页面立即更新的优点, 造成我们重要版本更新不及时的问题。建议还是用在app内嵌的H5 更加的完美。

main.ts

import { cleanupPWA } from "@/utils/cleanPwa"

// 只在生产环境且有新版本时清理 PWA 缓存
const currentVersion = process.env.VUE_APP_DATETIME
const lastVersion = localStorage.getItem("app_version")
if (import.meta.env.PROD && currentVersion && lastVersion !== currentVersion) {
  cleanupPWA().then(() => {
    console.log("PWA 缓存清理完成")
    localStorage.setItem("app_version", currentVersion)
  })
}

// 初始化init
// initPwa()

cleanPwa.ts

export async function cleanupPWA() {
  try {
    console.log("=== PWA 清理开始 ===")

    // 检查是否已经在清理中或已清理完成
    const cleaningStatus = sessionStorage.getItem("pwa_cleaning_status")
    if (cleaningStatus === "completed") {
      console.log("本次会话已完成 PWA 清理,无需重复执行")
      return {
        success: true,
        message: "本次会话已完成 PWA 清理"
      }
    }

    if (cleaningStatus === "in_progress") {
      console.log("PWA 清理正在进行中,请勿重复执行")
      return {
        success: true,
        message: "PWA 清理正在进行中"
      }
    }

    // 标记清理状态为进行中
    sessionStorage.setItem("pwa_cleaning_status", "in_progress")

    // 1. 只注销 PWA 相关的 Service Worker
    if ("serviceWorker" in navigator) {
      const registrations = await navigator.serviceWorker.getRegistrations()
      for (const registration of registrations) {
        // 只注销 PWA 相关的 Service Worker
        if (registration.scope.includes("/sw.js") || registration.scope.includes("/dev-sw.js")) {
          await registration.unregister()
        }
      }
      console.log("PWA Service Workers 已成功注销")
    }

    // 2. 只清除 PWA 相关的缓存
    if ("caches" in window) {
      const cacheKeys = await caches.keys()
      const pwaCacheKeys = cacheKeys.filter(
        (key) =>
          key.includes("workbox-") || // Workbox 缓存
          key.includes("sw-") || // Service Worker 缓存
          key.includes("pwa-") // 其他 PWA 相关缓存
      )

      await Promise.all(pwaCacheKeys.map((key) => caches.delete(key)))
      console.log("PWA 缓存已成功清除")
    }

    // 3. 清除 PWA 相关的存储数据
    const clearStorageData = () => {
      // 清除 localStorage 中的 PWA 相关数据
      const pwaKeys = [
        "newWorkerState",
        "testNewWorker1",
        "testNewWorker2",
        "testNewWorker3",
        "testNewWorkerDispatchDownloadStartEvent",
        "testNewWorkerDispatchDownloadCompleteEvent"
      ]
      pwaKeys.forEach((key) => localStorage.removeItem(key))

      // 清除 sessionStorage 中的 PWA 相关数据
      sessionStorage.removeItem("pwa_downloading")

      // 只清除 PWA 相关的 IndexedDB 数据库
      const deleteDatabase = (dbName) => {
        return new Promise((resolve) => {
          const request = indexedDB.deleteDatabase(dbName)
          request.onsuccess = () => resolve(true)
          request.onerror = () => resolve(false)
        })
      }

      // 只删除 PWA 相关的数据库
      return Promise.all([
        deleteDatabase("workbox-precache"),
        deleteDatabase("workbox-runtime"),
        deleteDatabase("workbox-background-sync")
      ])
    }

    await clearStorageData()
    console.log("PWA 相关的存储数据已清除")

    // 4. 移除 manifest 链接
    const manifestLink = document.querySelector('link[rel="manifest"]')
    if (manifestLink) {
      manifestLink.remove()
    }

    // 5. 添加 no-cache 头部,仅针对 Service Worker
    const meta = document.createElement("meta")
    meta.httpEquiv = "Service-Worker-Allowed"
    meta.content = "none"
    document.head.appendChild(meta)

    console.log("PWA 清理完成")

    // 标记清理完成
    sessionStorage.setItem("pwa_cleaning_status", "completed")

    // 只在首次清理完成时刷新页面
    if (!sessionStorage.getItem("pwa_refresh_done")) {
      sessionStorage.setItem("pwa_refresh_done", "true")
      setTimeout(() => {
        window.location.reload()
      }, 100)
    }

    return {
      success: true,
      message: "PWA 清理完成"
    }
  } catch (error) {
    console.error("清理 PWA 时发生错误:", error)
    return {
      success: false,
      message: "清理 PWA 时发生错误"
    }
  }
}

router

 // 后置守卫,保证每次路由跳转结束时关闭进度条
  router.afterEach(() => {
    // 更新pwa缓存
    // pwaPostMessage()
  })

vite.config.ts

import { VitePWA } from "vite-plugin-pwa"
export default defineConfig((mode): any => {
  return {
    ...
    plugins: [
		 VitePWA({
	        // 禁用 PWA 功能
	        // disable: true,
	        // 注册 Service Worker 的方式
	        registerType: "prompt", // 自动更新 Service Worker 并刷新页面(避免用户使用旧版本)autoUpdate
	        // 配置 Web App Manifest
	        manifest: false, // 禁用 manifest
	        workbox: {
	          // 清空全局匹配模式,不缓存任何静态资源
	          globPatterns: [],
	          // 配置运行时缓存策略
	          runtimeCaching: [
	            {
	              urlPattern: /\.(gif)$/i,
	              handler: "NetworkOnly", // 只使用网络请求,不使用缓存
	              options: {
	                cacheName: "static-assets"
	              }
	            }
	          ],
	          navigateFallback: null,
	          skipWaiting: true, // 新 Service Worker 安装后立即激活
	          clientsClaim: true, // 立即控制所有客户端
	          navigationPreload: false, // 关闭导航预加载 去掉service worker的 代理请求
	          cleanupOutdatedCaches: true // 自动清理过期缓存
	        },
	        // 开发环境的特定配置
	        devOptions: {
	          enabled: isRunPWA // 禁用开发环境的 PWA
	        }
	      }),
      ]
   ...
})

看完以后肯定会疑问,为什么这么复杂? 因为 上线的项目已被缓存到客户端电脑的本地。 如果第二次直接关闭PWA ,那么线上的PWA不更新, 最新版本的静态资源就会一直不更新。 所以需要先处理资源不更新的问题 发一版,第三次发版再删除PWA!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值