Vite和Vite-PWA
Vite,是比Webpack开发体验更好的打包工具。而PWA,是渐进式应用,让网站可以像应用一样,从桌面、主屏幕等入口直接进入,并拥有接近于原生应用快速加载,推送等能力。两者的结合,能让我们获得开发体验和用户体验的双重提升。
不过,Vite本身并不支持构建PWA,但在加入vite-pwa
这个助手后,用Vite开发PWA,会变得无比舒适。
Vite PWA提供了下列功能,能够让我们高效地构建PWA应用
- 生成
manifest
的配置文件,并使index.html
能引用到。 - 管理Service worker注册和更新的行为,并提供它们与前端框架交互的方法(如React,Vue)
- 通过配置文件生成缓存策略代码(
generateSW
) - 将用户通过直接编码写出的缓存策略代码交给Vite编译和打包(
injectManifest
)
在这篇文章中,将会用一个非常简单的Vite + React的例子,直观地展示如何配置Vite和Vite-PWA并实现一个PWA应用
初始化项目
bash
npm create vite@latest vite-pwa-demo -- --template vanilla-ts
cd vite-pwa-demo
npm i -D vite-plugin-pwa
之后在项目根目录下创建一个vite.config.js
并且填入以下代码。
ts
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [VitePWA({
// 插件配置项
})]
});
使用 vite-pwa 配置 manifest.json
manifest.json
是PWA的清单文件,它定义了PWA的行为和属性。 manifest.json
最少要包括这些字段
name
或short_name
,用于指定PWA的名字。当PWA被添加到桌面上,和其它原生App一样,这个名字会显示在App图标下方。在小屏幕设备上,会在无法完全展示name
时,展示short_name
description
一段描述PWA的文字icons
,PWA图标,至少需要包含192x192
和512x512
两个尺寸的icontheme_color
主题颜色
将这些字段加入到vite-pwa的配置中。
ts
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [VitePWA({
manifest: {
"name": 'PWA Demo',
"description": "A PWA demo built with Vite and vite pwa",
"theme_color": "#242424",
// 为了方便,使用svg图标
icons: [
{
"src": "/vite.svg",
"sizes": "192x192",
"type": "image/svg+xml"
},
{
"src": "/vite.svg",
"sizes": "512x512",
"type": "image/svg+xml"
}
]
},
devOptions: {
// 如果想在`vite dev`命令下调试PWA, 必须启用它
enabled: true,
// Vite在dev模式下会使用浏览器原生的ESModule,将type设置为`"module"`与原先的保持一致
type: "module"
}
})]
});
另外,在index.html
的<head>
中,需要加一些内容让它能支持PWA。
html
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite PWA</title>
<meta name="description" content="" />
<link rel="apple-touch-icon" href="/vite.svg" sizes="180x180" />
<!-- 必须和manifest中保持一致 -->
<meta name="theme-color" content="#242424" />
</head>
为了拥有更好的编辑器体验。将src/vite-env.d.ts
修改成这样:
ts
/// <reference types="vite/client" />
/// <reference types="vite-plugin-pwa/client" />
之后使用npm run dev
启动项目并用浏览器打开,地址栏右侧出现了安装PWA的按钮。
点击安装后,PWA就像原生应用一样,打开了一个独立的窗口。之后我们可以通过移动设备的主屏幕,或桌面设备的桌面,来直接进入PWA。
使用Workbox管理PWA缓存
PWA的能力,不仅是让网站像应用一样拥有从主屏幕直达的高效可访问。更有通过Service Worker缓存任何请求的能力,让应用的开启速度和响应速度接近于原生应用,并能离线使用一些功能
然而使用Service Worker的底层缓存API制定缓存策略,实现起来比较麻烦。Workbox
库就是为了解决这个问题而生的。它对浏览器提供的缓存API进行了封装。workbox
有这些包,提供了各式的功能:
workbox-routing
提供缓存的路由功能workbox-strategies
提供某个路由中资源的缓存策略workbox-prefeching
提供在安装应用时的预缓存能力worbox-build
通过配置项直接生成缓存策略代码的生成器
pwa-vite
在配置项中,提供了两种生成缓存策略的方式。 其中strageties: 'generateSW'
会使用workbox-build
通过配置来生成策略。
另一种strategies: 'injectManifest'
则会使用Vite打包一个service worker的入口文件,让开发者在代码中自行编写缓存策略。
缓存策略及选择
仅限网络(Network only)
"仅限网络"不会对资源做任何缓存,只请求网络上的最新数据。适用于对数据时效性最高的场景,如付款和结账等请求。非GET请求一般不是幂等的,通常不应被缓存。
网络优先(Network first)
"网络优先"会尽量提供最新鲜的内容,当网络不稳定或出现故障时会提供缓存中的过时内容。可以用于展示数据时效性高,但旧数据的回退也能被接受的场景。比如订单状态,价格和费率等(这两个需要告知用户时效性)
重新验证过时(Stale-while-validate)
"重新验证过时"会立即返回缓存的内容,但马上更新缓存。通过Service Worker与渲染主进程的通信,可以将这种更新通知到UI并重新获取数据。
这种缓存方式通常被用于新闻流,商品详情页面等。
借用Service Worker与主进程的通信能力,可以做到缓存被更新时让应用页面自动获取数据并重新渲染视图。
缓存优先,回退到网络
"缓存优先"仅会在内容未命中缓存时请求网络并更新缓存,否则会直接返回缓存。适用于非关键内容用于提升性能。比如用户头像等。
仅缓存
"仅缓存"会在缓存命中时返回缓存,在不命中时直接抛出错误。缓存的内容需要预先设置。通常,构建工具的构建产物(运行vite build
的产物),即PWA自身的资源会使用这种缓存方式。
使用workbox-precaching
库的precacheAndRoute(self.__WB_MANIFEST)
就会在Service Worker安装时,将构建产物加入缓存。
从配置生成缓存策略代码:strageties: 'generateSW'
vite-pwa
会默认使用generateSW
来生成service worker。这种方式能满足大部分PWA简单的需求。在没有配置的情况下,会在service worker安装时缓存构建产物中的大部分内容。 使用这些命令,构建项目并安装PWA。打开PWA后使用Dev tools的network 面板,刷新页面,能看到大部分的资源已经被service worker缓存了。
shell
npm run build;
npm run preview;
实际上
generateSW
配置模式下生成的service worker代码,会在注册和更新时,调用上文提到的precacheAndRoute(self.__WB_MANIFEST)
,将构建产物加入到缓存中并使用"仅缓存"策略来服务这些资源的请求。
对缓存的需求,不仅是前端构建产物,CDN上的内容,也可以进行缓存。修改例子,在header中加入这样一段代码引入jQuery
重新build并preview
html
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.js"></script>
观察network,发现jQuery通过网络被加载了进来。但它的加载时间比起其它的请求长达378ms,也就是说渲染的过程为了等待网络慢了这么多时间。
接下来,修改vite-pwa
中workbox
的配置,将jQuery也加入缓存。
js
export default defineConfig({
plugins: [VitePWA({
// ...vite-pwa的其它配置,比如manifest等
// workbox
workbox: {
runtimeCaching: [{
handler: 'CacheFirst',
urlPattern: 'https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.js',
options: {
cacheName: 'jQuery',
// 这是必须的
cacheableResponse: {
statuses: [0, 200]
}
}
}]
},
})]
});
重启项目。第二次加载后,network能观察到jQuery也被Service worker缓存。从之前的378ms到现在的1ms,提升速度巨大。
跨域会带来不透明请求,对于workbox,不透明请求只会在
network first
和stale-while-validate
策略下被缓存。如果需要在其它模式下也缓存不透明请求,需要cacheableResponse的statuses
字段来指定返回的http状态码来确定是否缓存。关于workbox处理不透明请求的更多详情可以看这里
直接编写缓存策略:strategies: 'injectManifest'
generateSW
虽然能够通过配置项快速地生成用于缓存资源的service worker代码。但也剥夺了开发者使用Service Worker提供的其它能力,如原生应用拥有的消息推送,定期同步功能等。 injectManifest
提供了另一种选择,让开发者能直接编写service worker的代码,在其中描述缓存策略。
要采用这种模式,需要在vite-pwa的配置中,显式地声明,strategies: 'injectManifest'
。此时,vite-pwa默认会将public/sw.js
作为service worker文件的入口。
但大多数情况下Vite项目的源代码都会存放在src目录中。因此还要指定srcDir
和filename
字段,以自定义Service worker的代码入口。
js
export default defineConfig({
plugins: [VitePWA({
// ...其它配置项
strategies: 'injectManifest',
srcDir: 'src',
filename: 'sw.ts',
})]
});
此外,还需安装workbox相关依赖。
shell
npm i -D workbox-strategies workbox-precaching workbox-routing workbox-cacheable-response
接着在src
目录下创建sw.ts
,并加入以下代码。
ts
/// <reference lib="webworker" />
declare const self: ServiceWorkerGlobalScope;
import { precacheAndRoute, cleanupOutdatedCaches } from "workbox-precaching";
import { CacheFirst } from "workbox-strategies";
import { registerRoute } from "workbox-routing";
import { CacheableResponsePlugin } from "workbox-cacheable-response";
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting();
}
});
precacheAndRoute(self.__WB_MANIFEST);
cleanupOutdatedCaches();
registerRoute(
"https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.js",
new CacheFirst({
cacheName: "jQuery",
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
],
}),
"GET"
);
第8-12行,为了支持下文"自动更新"相关内容的Service Worker更新功能。
第13行和14行必须存在。它们会缓存新静态资源,并清除旧静态资源。
15行到27行,我们为CDN上的jQuery注册了一个Cache First的缓存。为了能满足跨域,CacheableResponsePlugin
是必不可少的。
比起用generateSW
来生成Service Worker,injectManifest
能直接写Service Worker的代码,代码量更大。但让开发者获得了Service Worker的完整控制权。可以围绕缓存的生效与失效做很多事。比如stale-while-validate
策略下,在缓存更新时,通过message
API,通知渲染进程重新请求数据并更新视图,亦或是使用推送和周期同步API实现许多原生App的功能。
PWA的更新
当应用的代码更新时,构建出的Service Worker内容也会发生变化。此时就需要更新PWA。PWA的更新是通过Service Worker来完成的。
询问更新(prompt)
当检测到应用需要更新,也就是需要安装一个新的Service Worker时,仅当用户同意,才会安装。这种方式接近于原生应用,询问用户是否安装新的安装包,但PWA在用户设备上的更新速度远快于原生应用。
vite-pwa
默认使用的就是这种更新方式,我们不需要特地修改vite-pwa的配置。但需要将询问的逻辑,写在前端代码中。为了方便理解,例子使用最简单的window.confirm
来询问用户。
ts
import { registerSW } from 'virtual:pwa-register';
const updateSW = registerSW({
onNeedRefresh() {
const confirmed = window.confirm(
"A new version is available! Reload to update?"
);
if (confirmed) {
updateSW(true);
}
},
onOfflineReady() {},
});
运行效果如图所示。
自动更新(autoUpdate)
这种方式更新会在用户打开应用时,检测Service Worker是否发生变化(是否需要更新),如果发生变化,就会自动更新Service Worker并刷新页面。它的配置非常简单。
js
export default defineConfig({
plugins: [VitePWA({
// ...其它配置
registerType: 'autoUpdate'
})]
});
之后在代码的入口(main.ts
),靠前的位置,渲染页面之前,加入下列代码。
js
import { registerSW } from 'virtual:pwa-register';
registerSW({ immediate: true })
当应用发现有更新时,应用会自动安装新的Service Worker,更新缓存并刷新页面。
这种安装方式,完全体现了Web应用快速迭代的灵活性,不需要用户同意,自动更新应用。
现在的前端开发会使用像Vue, React等框架来提升开发效率。
vite-pwa
提供了和这些框架联动的API,我们无需再对registerSW
方法进行封装。根据文档指引,就能将PWA的生命周期和更新,与框架进行联动。
总结
比起原生应用,PWA同时做到了应用的高可达性,又具有Web网站的快速迭代能力,当应用不复杂时,也能提供原生应用的体验。即使不要求用户安装PWA到桌面,Service Worker的缓存功能,也能让应用加载速度,从网络请求的数个几百毫秒过程,提升到数个从本地加载的几毫秒过程。无论怎样,对用户体验会带来明显的正向提升。
而Vite,配合Vite PWA,能够充分利用Service Worker提供的种种能力,让Service Worker的开发,乘上了现代前端工程化的快车。