网站离线访问Vite实践

317 阅读10分钟

最近客户需求需要实现网站在离线状态下也能够正常访问,因为我们团队用的是vite工具,因此我认识到了vite-plugin-pwa这个插件,下面我会将该插件的一些使用经验分享给大家

话不多说先看最终效果:

可以看到是支持离线访问

好的讲一下实现的基本原理,其实本质上就是通过service worker技术在请求响应的时候拦截了一层,将响应的静态资源或数据缓存到本地,这样下次访问的时候就无需在从服务器获取

ok原理讲完了接下来讲实现流程

首先前提是你使用的是vite工具,然后需要安装vite-plugin-pwa这个包

pnpm i vite-plugin-pwa -D

这个包的其实就是帮助你做了注册service worker、缓存响应结果、请求预缓存等事情,后续只需要通过配置或者自定义js脚本来实现定制化需求,安装完成后,这个时候这个插件给你提供了2套方案来实现这个功能

方案一 generateSW

优点:通过纯配置即可实现离线访问,使用简单

缺点:无法支持post请求结果缓存,只支持get请求结果缓存,在高度定制化场景下可能有限制

方案二 injectManifest

优点:支持post请求结果缓存,在高度定制化场景下较为适用

缺点:流程相对来说较为复杂

以下是方案一的实现流程讲解:

1. 安装以下包

pnpm i workbox-window -D

2. 将以下配置项放在defineConfig的plugins选项中 ,具体选项可按照你的需求进行更改

import { VitePWA } from 'vite-plugin-pwa'

VitePWA({
  // Web 应用清单配置 https://github1s.com/antfu/vite-plugin-pwa/blob/HEAD/src/types.ts#L117
  manifest: {
    name: 'My Awesome App',
    short_name: 'MyApp',
    description: 'My Awesome App description',
    theme_color: '#ffffff',
    icons: [
      {
        src: 'pwa-192x192.png',
        sizes: '192x192',
        type: 'image/png'
      },
      {
        src: 'pwa-512x512.png',
        sizes: '512x512',
        type: 'image/png'
      }
    ]
  },
  // 输出的 Web 应用清单文件的文件名 默认: manifest.webmanifest
  manifestFilename: 'manifest.webmanifest',
  // 是否预缓存清单中的图标 默认: true
  includeManifestIcons: true,
  // 是否压缩manifest文件 默认: true
  minify: true,
  // 是否给 <link rel="manifest"> 标签添加 crossorigin="use-credentials" 属性 默认: false
  useCredentials: true,

  // 配置开发环境下的选项
  devOptions: {
    // 是否在开发模式下启用服务工作线程 默认: false
    enabled: false,
    // 服务工作线程类型,使用模块化,默认: 'classic'
    type: 'module',
    // 指定生成的服务工作线程存储位置。默认为 resolve(viteConfig.root, 'dev-dist')
    resolveTempFolder: () => 'dev-dist',
    // 是否抑制 workbox-build 工具生成的警告信息,如果启用,那么 globPatterns 将会被更改为 [*.js],同时在 dev-dist 文件夹中会创建一个新的空的 suppress-warnings.js 文件。这样做的目的是为了确保在生成服务工作线程时,不会因为警告而导致输出结果的不一致性。
    suppressWarnings: false
  },
  // 环境 默认: production
  mode: 'production',
  // service worker 脚本文件名 默认: sw.js (注意:strategies:generateSW 时该选项失效)
  filename: 'sw.js',
  // Service Worker 脚本输出目录 默认: dist
  outDir: 'dist',
  //service worker控制的当前目录,只能拦截及其子路径下的请求,并为其提供缓存策略等服务 默认: vite.base
  scope: '/',
  // 如果不使用自定义脚本文件,插件会自动注入,service worker的注入方式 默认: auto
  injectRegister: 'auto',
  // 触发服务工作线程的更新方式 autoUpdate:自动 prompt:手动,客户端根据钩子函数调用update() 这个可以直接看官网例子 https://github1s.com/antfu/vite-plugin-pwa/blob/HEAD/examples/vue-router/src/ReloadPrompt.vue 默认值: prompt
  registerType: 'autoUpdate',
  // 是否自毁服务工作线程
  selfDestroying: false,
  // 配置 PWA 资源生成和注入的参数(实验属性)
  pwaAssets: {
    // 指定一组预先定义好的配置选项,默认: minimal-2023,如果启用了 config 选项,则会忽略此选项
    preset: 'minimal-2023'
  },

  // Service Worker脚本 创建策略,默认: generateSW
  // generateSW: 根据配置生成Service Worker脚本文件,一般不需要高度定制Service Worker的情况直接选择generateSW就行。
  // injectManifest: 自定义Service Worker脚本文件,注入资源列表到脚本中,自己控制缓存策略,一般injectManifest适用于复杂一点的缓存策略
  strategies: 'generateSW',

  // 在 strategies:generateSW 下配置workbox
  workbox: {
    // 是否生成 sourcemap 源代码映射 默认: true
    sourcemap: true,
    // 默认无法缓存html后面带参数的页面,加上它忽略参数就可以缓存了
    ignoreURLParametersMatching: [/.*/],
    // 是否应该尝试识别并删除由较旧、不兼容的版本创建的任何预缓存,在新版本部署时自动清理不再需要的缓存内容 默认: false
    cleanupOutdatedCaches: true,
    // 需要预缓存的静态资源 (首次进入网站缓存的资源)
    globPatterns: [
      '**/*.{ico,html,js,css,webp,jpg,jpeg,png,gif,svg,ttf,woff,woff2,otf,eot,mp3,wav,ogg,mp4,webm,json,bmp,psd,tiff,tga,eps}'
    ],
    // 忽略不想预缓存的资源
    // globIgnores: [],
    // 运行时缓存配置 (用于接口数据的缓存)
    runtimeCaching: [
      // 接口缓存
      {
        // urlPattern: /api/,
        urlPattern: (e) => {
          console.log('接口缓存路由规则 =>>', e)
          return e.url.pathname.includes('/api')
        },
        // 请求类型 默认: GET
        method: 'GET',
        // Service Worker缓存响应策略 示例:https://juejin.cn/post/7039258299086143524
        handler: 'NetworkFirst',
        options: {
          // 缓存名
          cacheName: 'get-request',
          // 网络超时时间设置为3s
          networkTimeoutSeconds: 3,
          // 是否启用范围请求,服务工作线程会在请求中包含 Range 头,以支持客户端请求资源的部分内容,对大型资源友好
          rangeRequests: true,

          // 后台同步功能配置,在离线状态下将失败的请求加入队列,然后在恢复在线状态时重新尝试发送这些请求
          backgroundSync: {
            // 队列名
            name: 'get-request-fail-task',
            // 队列配置
            options: {
              // 在浏览器不支持后台同步时是否应该使用传统的同步机制
              forceSyncFallback: true,
              // 队列最大保留时间 默认: 24小时
              maxRetentionTime: 24 * 60 * 60
            }
          }
        }
      }
    ]
  }
})

3.tsconfig.jsoncompilerOptions.lib选项中添加webworker选项,在compilerOptions.types选项中添加vite-plugin-pwa/client选项

4. 在主入口文件的第一行执行这个函数,注意一定要在第一行

// 下方代码如果不使用PWA的话需要注释掉,否则打包编译会报错
async function setupServiceWorker() {
  try {
    // 引入该模块需要安装 workbox-window 依赖
    const { registerSW } = await import('virtual:pwa-register')

    // 当检测到sw.ts文件有更新时,会注册新的 Service Worker,并重新执行sw.ts脚本
    registerSW({
      // 立即注册
      immediate: true,
      onRegisteredSW() {
        console.log('Service Worker 注册成功')
      },
      onRegisterError() {
        console.error('Service Worker 注册失败')
      }
    })
  } catch (error) {
    console.error('动态加载 Service Worker 注册模块失败:', error)
  }
}

setupServiceWorker()

ok这个时候应该就可以正常离线访问了,请注意无论是方案一方案二在第一次访问时都必须有网,并且需要在生产环境https的环境下才可正常使用

以下是方案二的实现流程讲解:

1. 安装以下包

pnpm i workbox-cacheable-response workbox-core workbox-precaching workbox-routing workbox-strategies workbox-window -D

2. 将以下配置项放在defineConfigplugins选项中 ,具体选项可按照你的需求进行更改

import { VitePWA } from 'vite-plugin-pwa'

VitePWA({
  // manifest清单配置
  manifest: {
    name: 'My Awesome App',
    short_name: 'MyApp',
    description: 'My Awesome App description',
    theme_color: '#ffffff',
    icons: [
      {
        src: 'pwa-192x192.png',
        sizes: '192x192',
        type: 'image/png'
      },
      {
        src: 'pwa-512x512.png',
        sizes: '512x512',
        type: 'image/png'
      }
    ]
  },
  // 输出的 manifest清单 文件名 默认: manifest.webmanifest
  manifestFilename: 'manifest.webmanifest',
  // 是否压缩manifest文件 默认: true
  minify: true,
  // 是否预缓存manifest清单中的图标 默认: true
  includeManifestIcons: true,
  // 是否给 <link rel="manifest"> 标签添加 crossorigin="use-credentials" 属性 默认: false
  useCredentials: true,

  // 配置开发环境下的选项
  devOptions: {
    // 是否在开发模式下启用服务工作线程 默认: false
    enabled: false,
    // 服务工作线程类型,如果strategies:injectManifest必须使用模块化,默认: 'classic'
    type: 'module',
    // 指定生成的服务工作线程存储位置。默认为 resolve(viteConfig.root, 'dev-dist')
    resolveTempFolder: () => 'dev-sw-dist'
  },

  // Service Worker脚本 创建策略,默认: generateSW
  // generateSW: 根据配置生成Service Worker脚本文件,一般不需要高度定制Service Worker的情况直接选择generateSW就行 (注意:该策略只支持 GET 请求缓存)
  // injectManifest: 使用自定义Service Worker脚本文件,注入资源列表到脚本中,自己控制缓存策略,一般适用于复杂一点的缓存策略
  strategies: 'injectManifest',

  // strategies:injectManifest 时 自定义 Service Worker 脚本存放目录 默认: public
  srcDir: 'src/sw',
  // strategies:injectManifest 时 自定义 service worker 脚本文件名 默认: sw.js
  filename: 'sw.ts',
  // 编译后的 Service Worker 脚本输出目录 默认: dist
  outDir: 'dist',
  // service worker的注入方式,插件会自动注入 默认: auto
  injectRegister: 'auto',
  // service worker的更新方式 autoUpdate:自动 prompt:手动 默认值: prompt
  registerType: 'autoUpdate',
  // 是否自毁service worker
  selfDestroying: false,
  // strategies:injectManifest 时配置
  injectManifest: {
    // 构建时输出格式 默认: iife
    rollupFormat: 'es',
    // 兼容目标 默认: modules
    target: 'modules',
    // 是否添加源映射
    sourcemap: false,
    // 是否压缩 默认: true
    minify: true,
    //  globPatterns根据此目录来匹配文件
    globDirectory: 'dist',
    // 预缓存的静态资源 (首次进入网站)
    globPatterns: [
      '**/*.{ico,html,js,css,webp,jpg,jpeg,png,gif,svg,ttf,woff,woff2,otf,eot,mp3,wav,ogg,mp4,webm,json,bmp,psd,tiff,tga,eps}'
    ],
    // 生成的预缓存清单注入在Service Worker脚本的位置 默认: self.__WB_MANIFEST
    injectionPoint: 'self.__WB_MANIFEST',
    // 预缓存的文件的最大大小 默认: 2097152 字节(2MB)
    maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
    // 需要额外添加的预缓存清单的资源
    additionalManifestEntries: [],
    // 自定义修改和设置预缓存清单的内容
    manifestTransforms: [
      entries => {

          const manifest = entries.map(entry => {
              // 这里可以对预缓存清单的每一项进行修改
              return entry
          })

          return { manifest }

      }
    ]
  }
})

3.tsconfig.jsoncompilerOptions.lib选项中添加webworker选项,在compilerOptions.types选项中添加vite-plugin-pwa/client选项

4. 在主入口文件的第一行执行这个函数,注意一定要在第一行

// 下方代码如果不使用PWA的话需要注释掉,否则打包编译会报错
async function setupServiceWorker() {
  try {
    // 引入该模块需要安装 workbox-window 依赖
    const { registerSW } = await import('virtual:pwa-register')

    // 当检测到sw.ts文件有更新时,会注册新的 Service Worker,并重新执行sw.ts脚本
    registerSW({
      // 立即注册
      immediate: true,
      onRegisteredSW() {
        console.log('Service Worker 注册成功')
      },
      onRegisterError() {
        console.error('Service Worker 注册失败')
      }
    })
  } catch (error) {
    console.error('动态加载 Service Worker 注册模块失败:', error)
  }
}

setupServiceWorker()

5.src目录下新建sw目录并新建sw.ts文件,然后将以下代码复制粘贴,这块代码的目的其实就是激活新的Service Worker服务、手动清理旧缓存、手动注册配置缓存机制,但是请注意关于post请求结果缓存似乎不支持注册配置,因此需要自己拦截响应做缓存手动处理相关逻辑

//  控制 Service Worker 脚本文件
import { CacheableResponsePlugin } from "workbox-cacheable-response"
import { clientsClaim } from "workbox-core"
import { cleanupOutdatedCaches, precacheAndRoute } from "workbox-precaching"
import { registerRoute } from "workbox-routing"
import { NetworkFirst } from "workbox-strategies"

declare let self: ServiceWorkerGlobalScope

// 开发环境禁止打印日志信息(实在太多了...)
self.__WB_DISABLE_DEV_LOGS = true

// registerType: 'autoUpdate'时需添加此代码,自动刷新页面更新ServiceWorker
// 使新的 Service Worker 立即激活,而不需要等待之前的版本停止控制页面
self.skipWaiting()
// Service Worker 激活后立即获取控制权,而不必等待页面刷新
clientsClaim()

// 清理旧的缓存
cleanupOutdatedCaches()

// 使用预缓存和路由功能,将在 Service Worker 安装时预缓存指定的资源(页面首次加载用到)
precacheAndRoute(self.__WB_MANIFEST)

// 注册运行时get请求缓存策略
registerRoute(
    // 控制请求缓存的条件
    e => {

        return e.request.method === "GET"

    },
    // 缓存策略 NetworkFirst: 网络优先
    new NetworkFirst({
        // 缓存名
        cacheName: "get-request",
        // 网络超时时间
        networkTimeoutSeconds: 3,
        // 插件配置
        plugins: [
            new CacheableResponsePlugin({
                // 缓存条件 状态码
                statuses: [0, 200]
                // 缓存条件 请求头
                // headers: {}
            })
        ]
        // // 请求时需要额外添加的自定义参数
        // fetchOptions: {},
        // // 匹配选项
        // matchOptions: {}
    }),
    "GET"
)

ok这个时候应该就可以正常离线访问了,请注意无论是方案一方案二在第一次访问时都必须有网,并且需要在生产环境https的环境下才可正常使用

好的,所有的相关使用流程已经说完啦,希望能够帮助到你!