为 Vue 项目添加 PWA 支持
当前页面是本站的「Baidu MIP」版。查看和发表评论请点击:完整版 »
前段时间摸鱼终于摸到了 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
GenerateSW
与InjectManifest
如何选择:
- 如果你只是想简单地将项目 PWA 化,选择
GenerateSW
,插件会自动帮你生成包含 precache manifest 的service-worker.js
- 如果你有较高的定制需求,需要在已有 Service Worker 的基础上将项目 PWA 化,则选择
InjectManifest
,插件会在你指定的service-worker.js
的基础上加入 precache manifest
workboxOptions
配置项,请对应workboxPluginMode
来参考
- GenerateSW
- InjectManifest(必须指定
swSrc
)
通过配置可以做到的一些常用操作:
- 将指定(或指定文件夹中的)文件添加到 precache manifest 中,或从中排除,支持使用正则表达式
- 自动跳过 Service Worker 的等待阶段
- 添加离线 Google Analytics 支持
- 运行时缓存
runtimeCaching
,Workbox 的强大所在,阅读这些内容会使你更好地了解如何配置以及具体能做什么:
iconPaths
该设置项可以自定义在页面<head>
中添加的一些图标的<link>
或<meta>
中指定的文件路径
public/icons
中有安装插件时生成的默认图标
其有一个坑点,就是你无法设置不去添加某些<link>
或<meta>
,也就是强制性的
这主要会影响到maskIcon
,是 Macbook 的 Touch Bar 上的图标,由于要求必须是 svg,个人开发的小应用一般懒得去制作这个图标,但又无法不去添加这个<link>
几个可能对你有帮助的 icon 制作工具:
- https://lolicon.dev/#/image/png2ico
- https://www.websiteplanet.com/webtools/favicon-generator/
- https://realfavicongenerator.net
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
此处,官方文档中提到,当skipWaiting
为false
的时候,生成的 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 的缘故……
对于这个问题有两种可能的解决方法:
- 将
workboxPluginMode
设置为InjectManifest
,然后自己指定一个 SW 里面加上该代码内容 - 从谷歌那里下载最新的 Workbox 放置在项目内,并配置
workboxOptions
中的importWorkboxFrom
为disable
,然后在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' });
}