PWA 落地实践
谷歌发的协议,苹果目前不支持,但是可以根据 manifest.json 文件能读出来部分配置
项目需求,需要在html5页面中实现,添加PC端桌面或者移动端桌面按钮的功能。经过调研,通过渐进式web应用pwa(Progressive Web Apps)实现添加到主屏幕中(Add to Home Screen,简称 A2HS),是现代智能手机浏览器中的一项功能,能够快捷的web页面添加到主屏幕中,通过快捷方式,单击快速访问。
# PWA 中两个角色
# 1. Service Worker
- 功能:Service Worker 是一个在后台运行的脚本,主要用于控制网络请求、管理缓存、实现离线功能、推送通知等。它在用户关闭页面后依然可以运行,帮助 PWA 提供类似原生应用的体验,如离线支持和后台数据同步。
- 工作原理:Service Worker 拦截网络请求,可以决定是使用缓存的数据,还是从网络获取更新的内容,确保应用能够在无网络连接时也正常工作。
# 2. Web App Manifest
- 功能:Web App Manifest 是一个 JSON 文件,定义了 PWA 的基本元数据,如应用的名称、图标、启动 URL、主题颜色等。它用于配置 PWA 的外观和行为,特别是在用户将应用添加到主屏幕时,使其像原生应用一样运行。
- 作用:通过 Web App Manifest,用户可以在设备上“安装”你的应用(例如,添加到手机的主屏幕上),并获得类似于原生应用的启动体验。
有这个文件,在pc端已经支持加桌功能,只不过没有离线,推送能功能。
# 原生PWA 配置
PWA 必须在 HTTPS 环境下运行,因为服务工作线程(Service Worker)等功能需要安全的环境。
# 1. manifest文件
manifest.json 内容如下:
{
"background_color": "rgba(12, 12, 12, 1)",
"theme_color": "rgba(12, 12, 12, 1)",
"orientation": "any",
"description": "web程序的一般描述",
"display": "standalone", // fullscreen(全屏 没有电量条) 、standalone、browser
"icons": [
{
"src": "/img/logo.png",
"sizes": "512x512",
"type": "image/png"
}
],
"screenshots": [
{
"src": "/img/wide-screen-pc.png",
"sizes": "1442x927",
"type": "image/png"
},
{
"src": "/img/wide-screen.png",
"sizes": "386x830",
"type": "image/png",
"form_factor": "wide"
}
],
"name": "桌面PWA",
"short_name": "应用名称简称",
"start_url": "/discover",
"id": "/discover"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
然后在head 中添加这个
<link rel="manifest" href="/manifest.json">
# 2. 添加自定义加桌按钮
<button class="add_button_theme" style="display: block">
Add To HomeScreen
</button>
.add_button_theme {
display: none;
position: fixed;
bottom: 20px;
left: 50%;
z-index: 100;
transform: translateX(-50%);
background: #378ef5;
color: #fff;
text-decoration: none;
padding: 10px 20px;
font-size: 15px;
line-height: 20px;
border-radius: 20px;
box-shadow: 0 4px 16px rgb(0 0 0 / 30%);
border: none;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3. 注册Service Worker
推荐使用 register-service-worker 插件,会集成一个钩子函数。
import { register } from 'register-service-worker';
if ('serviceWorker' in window.navigator) {
register(`/service-worker.js`, {
ready() {
console.log(
'App is being served from cache by a service worker.\n' + 'For more details, visit https://goo.gl/AFskqB'
);
},
registered() {
console.log('Service worker has been registered.');
},
cached() {
console.log('Content has been cached for offline use.');
},
updatefound() {
console.log('New content is downloading.');
},
updated() {
console.log('New content is available; please refresh.');
},
offline() {
console.log('No internet connection found. App is running in offline mode.');
},
error(error) {
console.error('Error during service worker registration:', error);
}
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
原生用法
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('service-worker.js')
.then(() => { console.log('Service Worker Registered'); });
}
2
3
4
5
6
其中 service-worker.js 文件内容如下,可以在离线状态调取对应数据
一共有三个方法 install 、activate、fetch
install
事件在 Service Worker 首次被安装时触发。通常在此事件中,开发者会预缓存一些静态资源,以确保应用可以在离线时使用。
典型操作:打开缓存并添加文件,如 HTML、CSS、JavaScript 和图像文件。
activate
事件在 Service Worker 被激活时触发,通常用于清理旧的缓存,或者更新缓存内容。当新的 Service Worker 取代旧的 Service Worker 时,这个事件会被触发。
典型操作:删除不再需要的旧缓存,确保新的缓存能够生效。
fetch
事件在页面发起网络请求时触发,开发者可以拦截这些请求,并根据策略(如缓存优先、网络优先等)返回缓存的资源或从网络获取资源。这个事件可以帮助实现离线功能。
典型操作:检查缓存中是否有请求的资源,如果有则返回缓存内容,否则通过网络请求资源。
const CURRENT_CACHE_NAME = 'm_web_dev_v1.1'; // 存储的key
const URLS = [];
const cacheFiles = [];
self.addEventListener('install', async () => {
console.log(12123454544545, cacheFiles);
console.log('install');
const cache = await caches.open(CURRENT_CACHE_NAME);
await cache.addAll(URLS);
await self.skipWaiting();
});
self.addEventListener('activate', async () => {
console.log('activate');
const keys = await caches.keys();
keys.forEach(key => {
if (key !== CURRENT_CACHE_NAME) { // 新版本发布后 清除之前老版本
caches.delete(key);
}
});
});
self.addEventListener('fetch', async function (e) {
const req = e.request;
const url = req.url;
if (
url.endsWith('.js') ||
url.endsWith('.css') ||
url.endsWith('.png') ||
url.endsWith('.jpg') ||
url.endsWith('.svg')
) {
e.respondWith(cacheFirst(req));
} else {
e.respondWith(networkFirst(req));
}
});
// 缓存优先
async function cacheFirst(req) {
console.log('cacheFirst', req);
const cache = await caches.open(CURRENT_CACHE_NAME);
const cached = await cache.match(req);
if (cached) {
return cached;
} else {
const fresh = await fetch(req);
cache.put(req, fresh.clone());
return fresh;
}
}
// 网络优先
// eslint-disable-next-line no-unused-vars
async function networkFirst(req) {
console.log('networkFirst request', req);
const cache = await caches.open(CURRENT_CACHE_NAME);
try {
const fresh = await fetch(req);
console.log('networkFirst success', fresh);
cache.put(req, fresh.clone());
return fresh;
} catch (e) {
console.log('networkFirst fail', e);
const cached = await cache.match(req);
return cached;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# 4. 加载事件beforeinstallprompt
let deferredPrompt;
const addBtn = document.querySelector('.add_button_theme');
addBtn.style.display = 'none';
window.addEventListener('beforeinstallprompt', e => {
e.preventDefault();
deferredPrompt = e;
addBtn.style.display = 'block';
addBtn.addEventListener('click', () => {
addBtn.style.display = 'none';
deferredPrompt.prompt();
deferredPrompt.userChoice.then(choiceResult => {
if (choiceResult.outcome === 'accepted') {
console.log('取消安装');
} else {
console.log('安装成功');
}
deferredPrompt = null;
});
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 判断是否加桌
beforeinstallprompt
事件本身不能直接判断当前应用是否已经被添加到桌面上。它的作用是捕获用户的 PWA 安装提示(通常是在浏览器判断 PWA 可以安装的时候触发)。如果你想判断应用是否已经被添加到桌面,通常需要结合其他方法来实现。
判断是否已被添加到桌面的常见方法:
使用
window.matchMedia()
可以通过检查显示模式来推测应用是否在桌面上运行。例如:if (window.matchMedia('(display-mode: standalone)').matches) { console.log('The app is running as a standalone PWA'); } else { console.log('The app is not running as a standalone PWA'); }
1
2
3
4
5navigator.standalone
(仅 iOS Safari 支持) 在 iOS Safari 中,你可以使用navigator.standalone
来检查是否运行在独立模式中:if (window.navigator.standalone) { console.log('The app is running as a standalone PWA on iOS'); } else { console.log('The app is not running as a standalone PWA on iOS'); }
1
2
3
4
5
这两种方法可以帮助你判断应用是否已被添加到桌面上运行。
# 5. 最终实现效果如下图所示:
目前很多app 都会禁止生成PWA, 比例微信 facebook 飞书等。
如果需要,可以添加 Web Push 通知功能。使用 Push API
和 Notification API
来实现推送通知。
# @vue/cli-plugin-pwa
https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa
@vue/cli-plugin-pwa
是 Vue CLI 提供的一个插件,专门用于为 Vue 应用添加 PWA(Progressive Web App)功能。这个插件集成了 Web App Manifest、Service Worker 等核心功能,并提供了一些开箱即用的配置,方便开发者将 Vue 应用转换为 PWA。
主要功能:
自动生成 manifest.json
文件,包含应用名称、图标、启动 URL、主题颜色等信息。
可以通过 vue.config.js
配置来自定义 manifest 的内容。
配置 vue.config.js
文件:
module.exports = {
pwa: {
workboxPluginMode: 'InjectManifest',
workboxOptions: {
swSrc: './public/service-worker.js',
swDest: 'service-worker.js',
cacheId: 'my-app', // 给 Service Worker 的缓存命名 每次部署时,如果你希望确保缓存能够正确管理和更新,cacheId 应该在每个部署版本中保持唯一
},
name: 'My App',
themeColor: '#4DBA87',
msTileColor: '#000000',
manifestOptions: {
background_color: '#42B883'
},
workboxOptions: {
// Workbox 选项,像是缓存策略等
}
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Workbox 提供的两种模式
GenerateSW
(默认模式):- 在默认情况下,
@vue/cli-plugin-pwa
使用GenerateSW
模式。这种模式会自动生成一个完整的 Service Worker 文件,并根据你在vue.config.js
中配置的选项(如缓存策略、预缓存文件等)来管理应用的缓存。开发者无需手动编写 Service Worker,插件会为你处理大部分的工作。
- 在默认情况下,
InjectManifest
(自定义模式):- 如果你需要完全控制 Service Worker 的逻辑,比如自定义缓存策略、处理后台同步、管理复杂的推送通知等,可以使用
InjectManifest
模式。 - 在
InjectManifest
模式下,你需要手动编写一个 Service Worker 文件(通常命名为src/my-service-worker.js
或其他路径),然后 Workbox 会在构建过程中将一些基础的 Workbox 运行时代码注入到你编写的 Service Worker 中。这意味着你可以编写自己的 Service Worker 逻辑,同时利用 Workbox 的部分功能。
- 如果你需要完全控制 Service Worker 的逻辑,比如自定义缓存策略、处理后台同步、管理复杂的推送通知等,可以使用
在 InjectManifest
模式下,@vue/cli-plugin-pwa
不会自动生成 service-worker.js
文件。相反,它允许你自己编写自定义的 service-worker.js
文件,然后通过 Workbox 将预缓存逻辑注入到你的自定义 Service Worker 中。
# InjectManifest
模式的工作原理
构建时,Workbox 会自动注入额外的代码,以实现预缓存和其他功能
- 预缓存的文件列表
- Workbox Runtime 代码注入
- 使用的 Workbox 功能
self.__WB_MANIFEST 里面包括所有的静态资源
- 自定义 Service Worker 文件:
- 你需要手动编写
service-worker.js
文件。这个文件可以包含你自己定义的缓存策略、事件监听器等逻辑。
- 你需要手动编写
- Workbox 注入预缓存逻辑: 插件会继承很多功能
InjectManifest
模式会将 Workbox 的预缓存逻辑注入到你自定义的service-worker.js
文件中。通过配置,你可以定义哪些文件需要预缓存(例如静态资源),但不会完全覆盖你的自定义逻辑。
- 手动注册 Service Worker:
- 在这种模式下,你仍然需要手动注册 Service Worker,确保浏览器能够找到并激活你的
service-worker.js
文件。
- 在这种模式下,你仍然需要手动注册 Service Worker,确保浏览器能够找到并激活你的
# Web Share API
延伸其他API
Web Share API 和 PWA 一样是一项由古哥提出的草案 (opens new window),现还未被纳入 W3C。通过 Web Share API,用户可以方便地将内容或数据分享到应用中。
不过,现在只有安卓 Chrome 55 以上支持 Web Share API。另外,要使用分享功能,还要满足以下几点:
- 网站必须基于 HTTPS
- 注册 Origin Trial (opens new window),并将生成的 token 加入页面 meta 中
- 提供 text 或 url 中的一项,且值必须为字符串
- 分享事件 (opens new window)必须由用户事件触发
满足了这些剩下的就很简单了,只需监听用户事件 (opens new window),然后将需要分享的内容传递给 Web Share API 就可以了。
// CommonService.js
export const isSupportShareAPI = () => !!navigator.share;
export const sharePage = () => {
navigator
.share({
title: document.title,
text: document.title,
url: window.location.href
})
.then(() => console.info('Successful share.'))
.catch(error => console.log('Error sharing:', error));
};
2
3
4
5
6
7
8
9
10
11
12
13