最近客户需求需要实现网站在离线状态下也能够正常访问,因为我们团队用的是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.json的compilerOptions.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. 将以下配置项放在defineConfig的plugins选项中 ,具体选项可按照你的需求进行更改
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.json的compilerOptions.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的环境下才可正常使用
好的,所有的相关使用流程已经说完啦,希望能够帮助到你!