神代綺凛の随波逐流

为 Vue 项目添加 PWA 支持

当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »

前段时间摸鱼终于摸到了 Service Worker 这块,顺理成章的接触了 PWA,于是用这篇文章记录下如何为一个 vue 应用添加 PWA 支持,包括中途遇到的一些坑点和一些技巧

Head Pic:【オリジナル】「Recital」/「RH」的插画 [pixiv]

Vue & PWA

如果您的目的不是为现有的 vue 项目添加 PWA 支持,那么更推荐尝试 Lavas

注:PWA 应用要求必须全程 https,且在已安装的 PWA 应用中无法发送 http 请求

为已有项目添加 PWA 支持

1. 安装 PWA 插件

如果你已经在使用@vue/cli,那么可以直接在可视化界面中安装 PWA 插件

否则,可以通过vue add @vue/pwa命令来安装

该插件会使用谷歌的 PWA 框架 Workbox

2. 配置 PWA 插件

需要创建或修改项目中的vue.config.js,配置项以及示例在此处

我想多提几句的配置项有三个:

workboxPluginMode

可选配置项,默认为GenerateSW

GenerateSWInjectManifest如何选择:

workboxOptions

配置项,请对应workboxPluginMode来参考

通过配置可以做到的一些常用操作:

  1. 将指定(或指定文件夹中的)文件添加到 precache manifest 中,或从中排除,支持使用正则表达式
  2. 自动跳过 Service Worker 的等待阶段
  3. 添加离线 Google Analytics 支持
  4. 运行时缓存runtimeCaching,Workbox 的强大所在,阅读这些内容会使你更好地了解如何配置以及具体能做什么:

iconPaths

该设置项可以自定义在页面<head>中添加的一些图标的<link><meta>中指定的文件路径

public/icons中有安装插件时生成的默认图标

其有一个坑点,就是你无法设置不去添加某些<link><meta>,也就是强制性的

这主要会影响到maskIcon,是 Macbook 的 Touch Bar 上的图标,由于要求必须是 svg,个人开发的小应用一般懒得去制作这个图标,但又无法不去添加这个<link>

几个可能对你有帮助的 icon 制作工具:

3. 配置manifest.json

位于public/manifest.json,安装插件时自动生成,参考 Web App Manifest 进行配置

引导用户添加 PWA 应用

在应用中可以自行通过提示等方式引导用户手动添加 PWA 应用,以下列举目前我所知道的添加方式

Chrome 专有方式

对于 PC 或 Android 的 Chrome 浏览器都可以实现点击一个按钮来添加 PWA 应用,其原理是拦截了beforeinstallprompt这一事件,并在自己需要的时候触发

例如我们可以在项目的根 vue 实例中(以下示例省略了挂载及渲染等操作)

new Vue({
    data: {
        deferredPrompt: false
    },
    created() {
        window.addEventListener('beforeinstallprompt', e => {
            e.preventDefault();
            this.deferredPrompt = e;
        });
    },
    methods: {
        installPWA() {
            if (this.deferredPrompt) {
                this.deferredPrompt.prompt();
                this.deferredPrompt = false;
            }
        }
    }
});

然后就可以像这样自由地在任何 template 中使用了

<button @click="$root.installPWA" :disabled="$root.deferredPrompt===false">添加到主屏幕</button>

手动添加方式

iOS ≥ 11.3 可以在 Safari 中打开,点击浏览器底部的分享按钮,选择“添加到主屏幕”

PC 与 Android 的 Chrome 可通过右上角菜单添加(此处以 m.weibo.cn 为例)

PC Android

Service Worker 的更新

这是开发 PWA 应用时需要考虑的一个重要问题

由于 Service Worker 的更新机制(扩展阅读:【Service Worker】生命周期那些事儿),直接单纯的刷新页面并没有结束当前 session,因此依然是旧的 SW 在接管页面,新的 SW 仍旧是 waiting 状态

想要实现在不结束 session 的情况下更新 SW,必须使用 skipWaiting,目前有两种常见的处理方法

注:以下方法中提到的registerServiceWorker.js是由 PWA 插件在src目录中自动生成的,其作用是注册 SW 以及提供其生命周期钩子,具体可以看该 npm 包 register-service-worker

方法一:直接 skipWaiting,并引导用户刷新

这种方法非常暴力且简单,你只需要在步骤2提到的workboxOptions中将skipWaiting设置为true就行了,然后在registerServiceWorker.js中的updated()函数里做一些操作,例如弹出一个对话框来提示用户点击某个按钮以刷新页面

该方法对仅 precache 应用是没有任何影响的

但由于 skipWaiting 后新 SW 会立即接管页面,因此如果你更新了 SW 在处理 runtimeCaching 之类的运行时操作的行为而用户又没有刷新页面,就有可能会出现问题

即除非你能保证同一个页面在两个版本的 SW 相继处理的情况下依然能够正常工作,否则不要使用这个方法

方法二:等待用户同意再 skipWaiting 并刷新

该方法可以解决方法一的局限性,我们可以先弹出一个对话框询问用户是否要更新,用户同意后再 skipWaiting 并刷新

关于这种方法,我只描述大致的思路和做法,因为我没有实际完整地实践过,因为比较复杂麻烦(好的下面我就开始云了)

我们需要在workboxOptions中将skipWaiting设置为false,或者不设置,因为默认值为false

此处,官方文档中提到,当skipWaitingfalse的时候,生成的 SW 会加入以下代码

self.addEventListener('message', (event) => {
    if (event.data && event.data.type === 'SKIP_WAITING') {
        self.skipWaiting();
    }
});

其作用是当 SW 接收到{type:'SKIP_WAITING'}的消息后,SW 就会 skipWaiting

但实际情况是,最终生成的 SW 中并没有这一段代码(也并没有放置在其他 js 中),我猜测这可能是因为加入代码的这一特性是 Workbox 4 才加入的,而插件生成的 SW 引用的是 Workbox 3 的缘故……

对于这个问题有两种可能的解决方法:

  1. workboxPluginMode设置为InjectManifest,然后自己指定一个 SW 里面加上该代码内容
  2. 从谷歌那里下载最新的 Workbox 放置在项目内,并配置workboxOptions中的importWorkboxFromdisable,然后在importScripts中指定本地workbox-sw.js的路径

接着在registerServiceWorker.js中我们可以如下所示在updated()函数中加入一些内容,询问用户是否愿意重载页面以更新应用,若用户同意则向 waiting 状态的 SW 发送{type:'SKIP_WAITING'}消息,并在新 SW 控制页面后立即刷新

updated(reg) {
    // 当控制页面的 SW 改变时刷新
    navigator.serviceWorker.addEventListener('controllerchange', () => {
        window.location.reload();
    });
    // 接下来询问用户是否更新并重载应用
    // 用户同意则执行以下语句
    reg.waiting.postMessage({ type: 'SKIP_WAITING' });
}