前提:多页面vite项目给native提供h5页面,设置离线缓存优化体验
实现service worker
离线缓存以前需要自己编写sw.js
文件内容,比较复杂。 谷歌提供了workbox-*
库来协助写配置。
vite-plugin-pwa就是帮你使用workbox
结合一些模板代码自动生成sw.js
,实现0配置
第一: 我们通过类型文件了解值得关注的配置项
引入很简单
// vite.config.js / vite.config.ts
import { VitePWA } from 'vite-plugin-pwa'
export default {
plugins: [
VitePWA()
]
}
上面简单的配置就可以用,默认缓存了所有js,css,html
如果想做更多配置,推荐看代码里面插件的类型文件,里面写的很清楚
1. 先看VitePWAOptions
VitePWA(userOptions?: Partial): Plugin[]
interface VitePWAOptions {
/**
* Build mode
*
* @default process.env.NODE_ENV or "production"
*/
mode?: 'development' | 'production';
/**
* @default 'generateSW'
*/
strategies?: 'generateSW' | 'injectManifest';
/**
* The workbox object for `generateSW`
*/
workbox: Partial;
/**
* The workbox object for `injectManifest`
*/
injectManifest: Partial;
/**
* Unregister the service worker?
*
* @default false
*/
selfDestroying?: boolean;
}
这里关注以下几个配置,其他的不作关注:
(1) . strategies
: 默认是generateSW
然后去配置workbox
; 如果想要更多自定义的设置,可以选择injectManifest
,那就对应配置injectManifest
以下内容都是针对generateSW
策略进行配置
(2). workbox
: 给generateSW
的配置,配置的所有workbox
,将交给workbox-build
插件中的generateSW
处理,生成最终sw.js
中的配置代码
// vite-plugin-pwa/index.js
function loadWorkboxBuild() {
return require("workbox-build");
}
const { generateSW } = loadWorkboxBuild();
const buildResult = await generateSW(options2.workbox);
(3). selfDestroying
:vite-plugin-pwa
提供这个注销配置,注销代码给我们写好了
// vite-plugin-pwa/index.js
if (options2.selfDestroying) {
const selfDestroyingSW = `
self.addEventListener('install', function(e) {
self.skipWaiting();
});
self.addEventListener('activate', function(e) {
self.registration.unregister()
.then(function() {
return self.clients.matchAll();
})
.then(function(clients) {
clients.forEach(client => client.navigate(client.url))
});
});
`;
await import_fs.promises.writeFile(options2.swDest.replace(/\\/g, "/"), selfDestroyingSW, { encoding: "utf8" });
....
2. 再看workbox的类型:GenerateSWOptions 中的 GeneratePartial 和GlobPartial
workbox: Partial
type GenerateSWOptions = BasePartial & GlobPartial & GeneratePartial & RequiredSWDestPartial & OptionalGlobDirectoryPartial;
GeneratePartial
export interface GeneratePartial {
/**
* An optional ID to be prepended to cache names. This is primarily useful for
* local development where multiple sites may be served from the same
* `http://localhost:port` origin.
*/
cacheId?: string | null;
/**
* Any search parameter names that match against one of the RegExp in this
* array will be removed before looking for a precache match. This is useful
* if your users might request URLs that contain, for example, URL parameters
* used to track the source of the traffic. If not provided, the default value
* is `[/^utm_/, /^fbclid$/]`.
*
*/
ignoreURLParametersMatching?: Array;
/**
* If specified, all
* [navigation requests](https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading#first_what_are_navigation_requests)
* for URLs that aren't precached will be fulfilled with the HTML at the URL
* provided. You must pass in the URL of an HTML document that is listed in
* your precache manifest. This is meant to be used in a Single Page App
* scenario, in which you want all navigations to use common
* [App Shell HTML](https://developers.google.com/web/fundamentals/architecture/app-shell).
* @default null
*/
navigateFallback?: string | null;
runtimeCaching?: Array;
}
这里关注以下几个配置,其他的不作关注:
(1)cacheId
: 定义预缓存的名称
(2)ignoreURLParametersMatching
:默认无法缓存html后面带参数的页面,加上它忽略参数就可以缓存了
(3)navigateFallback
: 未命中缓存导航到的页面,比如可以可以设置为index.html
或者404.html
.
认识它是因为我的多页面项目首次加载报错:WorkboxError non-precached-url index.html
,issue中也有提到。说:默认为null并未生效,需要手动写为null
(4)runtimeCaching
: 运行时缓存,可以自定义配置各种类型的缓存,下面我们会讲到
GlobPartial
export interface GlobPartial {
/**
* A set of patterns matching files to always exclude when generating the
* precache manifest. For more information, see the definition of `ignore` in
* the `glob` [documentation](https://github.com/isaacs/node-glob#options).
* @default ["**\/node_modules\/**\/*"]
*/
globIgnores?: Array;
/**
* Files matching any of these patterns will be included in the precache
* manifest. For more information, see the
* [`glob` primer](https://github.com/isaacs/node-glob#glob-primer).
* @default ["**\/*.{js,css,html}"]
*/
globPatterns?: Array;
}
这里关注两个配置,其他的不作关注:
(1)globPatterns
: 可以看到上面我们说最简单的配置会默认缓存所有的html,js,css,也就是通过它实现的。如果想增加图片缓存可以在里面添加
(2)globIgnores
: 很明显是用来忽略不想缓存的资源
了解了上面 的配置后,我们增加一些基础配置优化效果
// vite.config.js / vite.config.ts
import { VitePWA } from 'vite-plugin-pwa'
export default {
plugins: [
VitePWA({
// 忽略html后面的参数,缓存有参数的html
ignoreURLParametersMatching: [/.*/],
// 自定义缓存名称
cacheId: 'wisbayar-cache',
// 增加图片类缓存
globPatterns: ["**\/*.{js,css,html,png,jpg,svg}"],
// 不设置回退url,依照实际情况配置
navigateFallback: null,
})
]
}
第二 :配置自定义运行时缓存
上面第二点最后配置在全部缓存资源的情况下是足够了,但是随着项目的增大,我期望
- 缓存get请求
- 不预缓存所有资源,访问到哪个页面就缓存哪个页面资源
- 分类单独设置js或者html的缓存个数和时间
- 图片优先缓存,js优先使用网络请求
要实现上面三点,就要用到runtimeCaching
配置了,我们先看看它的类型文件
runtimeCaching?: Array;
export interface RuntimeCaching {
handler: RouteHandler | StrategyName;
method?: HTTPMethod;
options?: {
cacheableResponse?: CacheableResponseOptions;
cacheName?: string | null;
expiration?: ExpirationPluginOptions;
};
urlPattern: RegExp | string | RouteMatchCallback;
}
这里关注以下配置,其他的不作关注:
(1)urlPattern
: 通过正则,字符或者函数形式匹配要缓存的资源类型
(2)cacheName
: 自定义缓存的类型名称
(3)cacheableResponse
: 缓存状态码正确的资源,比如200的
(4)expiration
: 设置缓存的时间,数量。超过数量就删除之前的
(5)method
: 默认是缓存get请求的资源,想缓存post的可以配置
(6)handler
: 取缓存的策略,有五种
type StrategyName = 'CacheFirst' | 'CacheOnly' | 'NetworkFirst' | 'NetworkOnly' | 'StaleWhileRevalidate';
-
NetworkFirst
:网络优先 -
CacheFirst
:缓存优先 -
NetworkOnly
:仅使用正常的网络请求 -
CacheOnly
:仅使用缓存中的资源 -
StaleWhileRevalidate
:从缓存中读取资源的同时发送网络请求更新本地缓存
根据这几个类型,我一般这么设置:get请求缓存用NetworkFirst
,图片变动不大用CacheFirst
,其他js,html用StaleWhileRevalidate
了解了以上配置后,再看看最终的配置代码
VitePWA({
workbox: {
// ignoreURLParametersMatching: [/.*/],
cacheId: 'wisbayar-cache',
globPatterns: [],
// globIgnores: ['static/js/**'],
navigateFallback: null,
runtimeCaching: [
mode !== 'production'
? {
urlPattern: ({ url }) => url.origin === 'https://app-api-0.com',
handler: 'NetworkFirst',
options: {
cacheName: 'wisbayar-api',
cacheableResponse: {
statuses: [200]
}
}
}
: {
urlPattern: ({ url }) => url.origin === 'https://app-api.id',
handler: 'NetworkFirst',
options: {
cacheName: 'wisbayar-api',
cacheableResponse: {
statuses: [200]
}
}
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg)$/,
handler: 'CacheFirst',
options: {
cacheName: 'wisbayar-images',
expiration: {
// 最多30个图
maxEntries: 30
}
}
},
{
urlPattern: /.*\.js.*/,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'wisbayar-js',
expiration: {
maxEntries: 30, // 最多缓存30个,超过的按照LRU原则删除
maxAgeSeconds: 30 * 24 * 60 * 60
},
cacheableResponse: {
statuses: [200]
}
}
},
{
urlPattern: /.*\.css.*/,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'wisbayar-css',
expiration: {
maxEntries: 20,
maxAgeSeconds: 30 * 24 * 60 * 60
},
cacheableResponse: {
statuses: [200]
}
}
},
{
urlPattern: /.*\.html.*/,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'wisbayar-html',
expiration: {
maxEntries: 20,
maxAgeSeconds: 30 * 24 * 60 * 60
},
cacheableResponse: {
statuses: [200]
}
}
}
]
},
// 取消注册
selfDestroying: false
})
说明一下:
(1)上面使用了urlPattern: /.*\.html.*/
正则单独匹配缓存html后,已经可以缓存带参数的html了,ignoreURLParametersMatching
用不上了
(2)globPatterns
设置为空数组,就是不做预缓存,全部使用runtimeCaching
的能力实现进行时缓存,访问到哪个页面就缓存哪个页面的资源,并通过expiration
设置了数量限制
(3)为什么我在接口缓存那里没有直接使用process.env
判断是否与url.origin
相等呢? 发现urlPattern
函数里面不能写变量,生成的sw.js
直接把变量copy过去了
第三 :最后看效果,从浏览器中的观察service workers。
只有配置了service worker的网页(也就是在项目根目录拥有
sw.js
的网页),才能看到以上内容
Cache storage
就是缓存到本地的文件,我这里进行了区分命名,分类缓存wisbaya-js
,wisbaya-css
,wisbaya-images
,wisbaya-html
,以及wisbaya-api(get请求)
, 精细的文件类别缓存可以控制缓存文件数量。
以上缓存是运行时缓存,也就是点击进入某个页面请求成功才缓存当前页面(首次加载不会生成,切换页面就有了)
区别于第一个缓存wisbaya-cache-precache
是预缓存列表,只要访问,vite-plugin-pwa
默认全部将所有js,css,html
缓存起来很多页面都应用了service worker,可以点击
see all registrations
会看到很多曾经访问过的应用了service worker的页面
现在只要断开网络,刷新网页还能看到原页面和数据展示
以上就是通过vite-plugin-pwa
插件按照generateSW
策略配置离线缓存的全部。
更多关于generateSW/injectManifest
两种策略可以查看官网Service Worker 策略和行为
我在类型文件中还看到针对两种策略的webpack类型,配置差不多,下次可以在webpack中试试
export declare type WebpackGenerateSWOptions = BasePartial & WebpackPartial & GeneratePartial & WebpackGenerateSWPartial;
export declare type WebpackInjectManifestOptions = BasePartial & WebpackPartial & InjectPartial & WebpackInjectManifestPartial;