PWA 落地实践

[[toc]]

谷歌发的协议,苹果目前不支持,但是可以根据 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"
}

然后在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;
}

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);
}
});
}

原生用法

if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('service-worker.js')
.then(() => { console.log('Service Worker Registered'); });
}

其中 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;
}
}

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;
});
});
});

判断是否加桌

beforeinstallprompt 事件本身不能直接判断当前应用是否已经被添加到桌面上。它的作用是捕获用户的 PWA 安装提示(通常是在浏览器判断 PWA 可以安装的时候触发)。如果你想判断应用是否已经被添加到桌面,通常需要结合其他方法来实现。

判断是否已被添加到桌面的常见方法:

  1. 使用 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');
    }
  2. navigator.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');
    }

这两种方法可以帮助你判断应用是否已被添加到桌面上运行。

5. 最终实现效果如下图所示:

image-20240908175350231

目前很多app 都会禁止生成PWA, 比例微信 facebook 飞书等。

如果需要,可以添加 Web Push 通知功能。使用 Push APINotification API 来实现推送通知。


@vue/cli-plugin-pwa

https://github.com/vuejs/vue-cli/tree/dev/packages/vue/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 选项,像是缓存策略等
}
}
};

Workbox 提供的两种模式

  1. GenerateSW (默认模式):
    • 在默认情况下,@vue/cli-plugin-pwa 使用 GenerateSW 模式。这种模式会自动生成一个完整的 Service Worker 文件,并根据你在 vue.config.js 中配置的选项(如缓存策略、预缓存文件等)来管理应用的缓存。开发者无需手动编写 Service Worker,插件会为你处理大部分的工作。
  2. InjectManifest (自定义模式):
    • 如果你需要完全控制 Service Worker 的逻辑,比如自定义缓存策略、处理后台同步、管理复杂的推送通知等,可以使用 InjectManifest 模式。
    • InjectManifest 模式下,你需要手动编写一个 Service Worker 文件(通常命名为 src/my-service-worker.js 或其他路径),然后 Workbox 会在构建过程中将一些基础的 Workbox 运行时代码注入到你编写的 Service Worker 中。这意味着你可以编写自己的 Service Worker 逻辑,同时利用 Workbox 的部分功能。

InjectManifest 模式下,@vue/cli-plugin-pwa 不会自动生成 service-worker.js 文件。相反,它允许你自己编写自定义的 service-worker.js 文件,然后通过 Workbox 将预缓存逻辑注入到你的自定义 Service Worker 中。

InjectManifest 模式的工作原理

构建时,Workbox 会自动注入额外的代码,以实现预缓存和其他功能

  • 预缓存的文件列表
  • Workbox Runtime 代码注入
  • 使用的 Workbox 功能

self.__WB_MANIFEST 里面包括所有的静态资源

  1. 自定义 Service Worker 文件
    • 你需要手动编写 service-worker.js 文件。这个文件可以包含你自己定义的缓存策略、事件监听器等逻辑。
  2. Workbox 注入预缓存逻辑: 插件会继承很多功能
    • InjectManifest 模式会将 Workbox 的预缓存逻辑注入到你自定义的 service-worker.js 文件中。通过配置,你可以定义哪些文件需要预缓存(例如静态资源),但不会完全覆盖你的自定义逻辑。
  3. 手动注册 Service Worker
    • 在这种模式下,你仍然需要手动注册 Service Worker,确保浏览器能够找到并激活你的 service-worker.js 文件。

Web Share API

延伸其他API

Web Share API 和 PWA 一样是一项由古哥提出的草案,现还未被纳入 W3C。通过 Web Share API,用户可以方便地将内容或数据分享到应用中。

不过,现在只有安卓 Chrome 55 以上支持 Web Share API。另外,要使用分享功能,还要满足以下几点:

  • 网站必须基于 HTTPS
  • 注册 Origin Trial,并将生成的 token 加入页面 meta 中
  • 提供 text 或 url 中的一项,且值必须为字符串
  • 分享事件必须由用户事件触发

满足了这些剩下的就很简单了,只需监听用户事件,然后将需要分享的内容传递给 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));
};