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" , "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' ; 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; } } 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 ; }); }); });
判断当前应用是否以 PWA 独立模式运行 beforeinstallprompt
事件无法判断应用是否已经被添加到桌面上,但你可以使用其他方法检测 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' ); }
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. 最终实现效果如下图所示:
目前很多app 都会禁止生成PWA, 比例微信 facebook 飞书等。
如果需要,可以添加 Web Push 通知功能。使用 Push API
和 Notification 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' , }, name: 'My App' , themeColor: '#4DBA87' , msTileColor: '#000000' , manifestOptions: { background_color: '#42B883' }, workboxOptions: { } } }; pwa: isNodeProd ? { name: VEST_CONTENT.pwaName, workboxPluginMode: 'InjectManifest' , workboxOptions: { swSrc: './public/service-worker.js' , swDest: 'service-worker.js' }, manifestOptions: { background_color: 'rgba(12, 12, 12, 1)' , theme_color: 'rgba(12, 12, 12, 1)' , orientation: 'any' , description: VEST_CONTENT.pwaName, display: 'standalone' , icons: [ { src: '/zy-abroad-mobile-adfree-web/img/logo.png' , sizes: '512x512' , type: 'image/png' } ], screenshots: [ { src: '/zy-abroad-mobile-adfree-web/img/wide-screen-pc.png' , sizes: '1281x913' , type: 'image/png' , form_factor: 'wide' }, { src: '/zy-abroad-mobile-adfree-web/img/wide-screen.png' , sizes: '386x830' , type: 'image/png' } ], name: VEST_CONTENT.pwaName, short_name: VEST_CONTENT.pwaName, start_url: VEST_CONTENT.pwaUrl, id: VEST_CONTENT.pwaUrl, lang: 'en-US' , dir: 'ltr' , related_applications: [ { platform: 'web' } ], prefer_related_applications: false , categories: ['books' , 'education' , 'magazines' ] } } : { manifestOptions: { icons : [] } }
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 的部分功能。
在 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
文件。
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 就可以了。
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)); };