Vite开发PWA指南

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最少要包括这些字段

  • nameshort_name,用于指定PWA的名字。当PWA被添加到桌面上,和其它原生App一样,这个名字会显示在App图标下方。在小屏幕设备上,会在无法完全展示name时,展示short_name
  • description一段描述PWA的文字
  • icons,PWA图标,至少需要包含192x192512x512两个尺寸的icon
  • theme_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-pwaworkbox的配置,将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 firststale-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目录中。因此还要指定srcDirfilename字段,以自定义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策略下,在缓存更新时,通过messageAPI,通知渲染进程重新请求数据并更新视图,亦或是使用推送和周期同步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的开发,乘上了现代前端工程化的快车。

相关推荐
无尽的大道4 小时前
深入理解 Java 阻塞队列:使用场景、原理与性能优化
java·开发语言·性能优化
loey_ln4 小时前
webpack配置和打包性能优化
前端·webpack·性能优化
郭梧悠15 小时前
HarmonyOS(57) UI性能优化
ui·性能优化·harmonyos
奈斯ing16 小时前
【Oracle篇】SQL性能优化实战案例(从15秒优化到0.08秒)(第七篇,总共七篇)
运维·数据库·sql·oracle·性能优化
青云交2 天前
大数据新视界 -- Impala 性能优化:分布式环境中的优化新视野(下)(28 / 30)
大数据·性能优化·资源管理·impala·优化策略·分布式环境·数据布局
hummhumm2 天前
第 24 章 -Golang 性能优化
java·开发语言·前端·后端·python·性能优化·golang
白茶等风121382 天前
准备阶段 Profiler性能分析工具的使用(一)
unity·性能优化
敲代码的彭于晏2 天前
除了localStorage、sessionStorage,了解Cache Storage吗?
前端·浏览器·pwa
激流丶2 天前
【Redis 探秘】Redis 性能优化技巧
redis·性能优化·bootstrap