弱网神器PWA
PWA简介
全称Progressive Web App,中文意思是渐进式Web App,我习惯称升级版web网页。PWA与传统 Web App,Native App 的对比如下:
是否可安装 |
是否可链接访问 |
用户体验 |
用户黏性 |
|
传统 Web |
无法安装 |
可链接访问 |
体验一般 |
黏性差 |
Native App |
可安装 |
不可链接访问 |
体验好 |
黏性强 |
PWA |
可安装 |
可链接访问 |
体验好 |
黏性强 |
亮点:
- 隐藏浏览器默认功能,导航栏、工具栏等,给用户沉浸式体验。
- 添加到桌面(与简单主屏幕链接或书签不同),还可以在断网的情况下使用。
- 能够推送,甚至离线推送(这里的离线是指用户关闭网页,并非指断网)。
在国内也有很多 PWA 站点,比如饿了么和新浪微博的移动版。
核心技术
PWA是一系列技术的组合,核心主要三块,Web App Manifest,Service Worker和Web Push,这几项技术还在不断进化,所以叫它渐进式web,简单介绍一下这三个东西:
Web App Manifest
为了跟Native App 一样的沉浸式体验,Web App Manifest 允许开发者配置隐藏浏览器多余的 UI,这也是 Native App 相比 Web App 用户黏性更好的原因之一。具体使用方式其实就是编写一个manifest.json,在里面列出应用名称、图标、启动方式、背景颜色、主题颜色等等。详细配置参数见MDN,还可以通过Web App Manifest Generator可视化配置。
目前PWA应用在安卓手机上表现比较好,如果安卓安装了谷歌浏览器的话,对PWA的体验会更充分,安卓还能主动询问是否添加到主屏幕。ios的话我试了谷歌浏览器没有找到添加到屏幕的入口,Safri上是有的,但是一些Manifest配置项需要兼容。
Service Worker
Service Worker 是PWA的心脏,发动机,主要特点如下:
- 一个特殊的 worker 线程,独立于当前网页主线程,有自己的执行上下文。
- 可拦截并代理请求和处理返回,可以操作本地缓存,如 CacheStorage,IndexedDB 等
- 一旦被安装,就永远存在,除非显示取消注册或者更新。
- 能接受服务器推送的离线消息。
注册
注册SW很简单,如下所示。此时受sw控制的范围是sw文件所在的目录下的所有页面,也可以通过register的第二个参数配置scope自定义作用域,但是最大不能超过sw所在目录范围,否则报错。例如sw.js存在/a/b目录下,那么scope最多能配置成/a,不能配置成/d。
生命周期
Service Worker在第一次被注册后,再次访问页面时,在内部都有一系列的工作流程,下图是 Service Worker 工作流程图。
更新原理
前面说了sw注册一次后变永久存在,刷新页面或者关闭后重新打开生命周期钩子都不会重新执行,除非手动注销或者sw文件有更新,浏览器会自动对sw文件做diff对比,发现sw文件名或者内容有变化会重新进行注册、安装,当检测到当前的页面被激活态的SW控制着的话,会进入waiting状态但不会立马激活新,需要等所有的终端都关闭之后,再重新打开页面才能激活新的SW。
Service Worker 在全局提供了一个 skipWaiting() 方法,可以跳过等待。但页面已经注册了sw并且正在访问,这时如果sw文件更新了并且跳过等待,那页面提取的部分数据将由旧 Service Worker 处理,而新 Service Worker 处理后来提取的数据。如果预期到缓存数据不一致的现象会导致问题,则不要使用 skipWaiting() 跳过 waiting 状态。
终端概念
在手机端或者 PC 端浏览器,每新打开一个已经激活了 SW 的页面,那 SW 所控制的终端就新增一个,每关闭一个包含已经激活了 SW 页面的时候,则 SW 所控制的终端就减少一个,如上图打开了三个浏览器标签,则当前 SW 控制了三个终端。
缓存与请求拦截
PWA之所以能够做到离线访问,是因为充分利用了浏览器缓存能力,例如cacheStorage、IndexDB。具体看下怎么结合SW使用cacheStorage:
let dataCacheName = 'new-data-v1' let cacheName = 'first-pwa-app-1' let filesToCache = [ '/', '/index.html', '/script/index.js', '/style/index.css', ... ] self.addEventListener('install', function (e) { console.log('SW Install') e.waitUntil( caches.open(cacheName).then(function (cache) { console.log('SW precaching') return cache.addAll(filesToCache) }) ) self.skipWaiting() })
self.addEventListener('fetch', function (e) { console.log('SW Fetch', e.request.url) // 如果数据相关的请求,需要请求更新缓存 let dataUrl = '/mockData/' if (e.request.url.indexOf(dataUrl) > -1) { e.respondWith( caches.open(dataCacheName).then(function (cache) { return fetch(e.request).then(function (response){ cache.put(e.request.url, response.clone()) return response }).catch(function () { return caches.match(e.request) }) }) ) } else { e.respondWith( caches.match(e.request).then(function (response) { return response || fetch(e.request) }) ) } })
消息推送
桌面通知
在 iOS 和 Android 移动设备中,Native App 推送通知很常见,容易引导用户重新访问应用。Web App 一直缺少推送通知的能力,Notification是 HTML5 新增的一套展示桌面通知的 API。
Notification.requestPermission().then(permission => { // 通过 permission 判断用户的选择结果 if (permission === 'granted') { console.log('用户已授权,可展示通知'); const title = 'Notification Title' const options = { body: 'Simple piece of body text.\nSecond line of body text :)' } const notification = new Notification(title, options) } else if (Notification.permission === 'denied') { console.log('用户已禁止'); } else { console.log('用户尚未授权,需首先向用户申请通知权限') } })
例如:https://pwa-push-1fdb6.web.app/
如果在手机上大概是这个样子:
在JS主线程还可以这样发起通知:
navigator.serviceWorker.getRegistration().then(function (registration) { registration.showNotification('你好', {/* options */}).then(function () { // 通知展现成功 }) .catch(function (e) { // 通知展现未授权 }) })
在sw文件内则是这样发起通知:
self.registration.showNotification(title, {...options}).then(function () { console.log(11) // 通知展现成功 }) .catch(function (e) { // 通知展现未授权 })
消息推送
Notification可以在PC或者手机端创建通知,但是当浏览器没有打开,Service Worker 处于休眠状态时就没办法给用户推送。先了网络推送的基本流程:
- UA,即浏览器;
- Push Service,即推送服务器,用于管理推送订阅、消息推送等功能的第三方服务器。该服务器是浏览器决定的;但是在本地使用web-push库就可以了
- Application Server,即网站应用的后端服务。
要想手机或PC收到推送满足下面条件:
1. PC端的Chrome要可以FQ,也就是能够使用谷歌相关的服务。
2. 手机端的Chrome要内置了Chrome服务(GMS),实验国内的华为,vivo,小米系列基本是没有内置Chrome服务的,能够连接Google Play则代表可以。
web-push是基于谷歌的FCM(云消息机制)实现的推送,而FCM包含在GMS里面,但是谷歌禁止国外手机使用谷歌服务。
VAPID
出于用户隐私考虑,在应用和推送服务器之间没有进行强身份验证,解决方案是对 Web Push 使用自主应用服务器标识(VAPID)协议,VAPID 允许应用服务器向推送服务器标识身份,推送服务器知道哪个应用服务器订阅了用户,并确保它也是向用户推送信息的服务器。使用 VAPID 服务过程分为以下几个步骤:
- 应用服务器创建一对公钥/私钥,并将公钥提供给 Web App 客户端
- 当用户尝试订阅推送服务时,将公钥添加到 subscribe() 订阅方法中,公钥将被发送到推送服务保存。
- 应用服务器想要推送消息时,发送包含公钥和已经签名的 JSON Web 令牌到推送服务提供的 API,推送服务验证通过后,将信息推送至 Web App 客户端。
利用web-push生成公钥/私钥
npm install web-push -g
web-push generate-vapid-keys
得到如下:
=======================================
Public Key: BLjmecELgzCq4S-fJyRx9j03wvR0yjSs6O13L6qABrj7CadS8689Lvi2iErzG8SeaPSX_ezoyD2O0MMkGZcj4c0
Private Key: wNY2Jw8Zcw2wjfsiVzIxQB6K-ZoOkn-MS7fXxoo8w0Y =======================================
客户端订阅推送服务:
async function subscribe () { // 判断兼容性 if (window.PushManager == null && navigator.serviceWorker == null) { return } // 注册 service-worker.js 获取 ServiceWorkerRegistration 对象 let registration = await navigator.serviceWorker.register('/service-worker', {scope: '/'}) // 发起推送订阅 let pushSubscription = await registration.pushManager.subscribe({ userVisibleOnly:true, applicationServerKey: base64ToUint8Array('Public Key') }) // 将 pushSubscription 发送给应用后端服务器 await distributePushResource(pushSubscription) }
后端进行推送:
const webpush = require('web-push') const vapidKeys = { publicKey: 'BLjmecELgzCq4S-fJyRx9j03wvR0yjSs6O13L6qABrj7CadS8689Lvi2iErzG8SeaPSX_ezoyD2O0MMkGZcj4c0', privateKey: 'wNY2Jw8Zcw2wjfsiVzIxQB6K-ZoOkn-MS7fXxoo8w0Y' } webpush.setVapidDetails( 'mailto:your-email@provider.com', vapidKeys.publicKey, vapidKeys.privateKey ) module.exports = function pushMessage (pushSubscription, message) { return webpush.sendNotification(pushSubscription, message) }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!