PWA底层探秘:从Manifest到推送,那些说明书不会写的坑点

你真以为 PWA 就是加个 manifest.json 再上个 service worker 就完事了?早些年我也是这么干的,结果线上出了个事故——用户一直看到的都是旧版页面,清了缓存都没用。那一刻我才意识到,这玩意儿水太深了。

Service Worker 不是黑魔法,但它的生命周期就是个连环套

首先得搞清楚,service worker 是个独立于页面主线程的脚本,它像浏览器里常驻的一个代理。当你在页面里注册它时,浏览器会走一套让人头疼的流程:下载、安装、等待激活、激活、接管页面。每一步都有坑。

安装事件里,你通常会预缓存关键资源。这没问题,但如果某个资源下载失败,整个安装就会失败,sw 不会被激活——现在知道为什么你的 PWA 偶尔离线打不开了吧?我习惯在 install 事件里用 event.waitUntil 包装一个 Promise.all,然后针对每个资源加单独的 catch,确保单个失败不影响整体。但这里有个大忌:waitUntil 里的 Promise 如果一直不 resolve,service worker 会一直停在 installing 状态,超时后浏览器会放弃——我踩过这样的坑,调试时都得在 Chrome DevTools 里手动 skipWaiting 才能恢复。

然后是激活。默认情况下,新的 service worker 会等所有旧页面关闭后才接管——这就是用户看到旧版不更新的根因。解决方法是 self.skipWaiting(),但直接用会带来另一个问题:如果你在新 sw 里改了缓存策略,而仍然打开的旧页面可能还依赖旧缓存结构,酿成版本不一致的惨剧。我的实践是:skipWaiting 尽量早调用,但在 activate 事件里用 self.clients.claim() 强制接管所有页面,并同时用版本化的缓存键(比如 cache-v2),把新旧缓存隔离开。

PWA Service Worker生命周期流程图
PWA Service Worker生命周期流程图

再唠叨一句:DevTools 里的 “Update on reload” 能救开发时的命,但生产环境千万别依赖它,因为用户可不会开调试面板。

缓存策略选不好,你的应用比传统网页还慢

缓存策略说白了就是网络请求和本地缓存的组合方式。常见的有 Cache First、Network First、Stale-While-Revalidate 等。选错了策略,你测出来的 Lighthouse 分数再高,用户还是骂街。

举个例子:你用 Cache First 处理 API 数据,结果用户一直看到过时的价格。用 Network First 且超时回退缓存,但超时设得太长,弱网时白屏半分钟。

我的血泪教训是:对静态资源,用 Cache First 并配合版本号或者 hash。但更新时一定要用 activate 事件清理旧缓存,否则用户的存储空间很快爆满。清理逻辑要精确:遍历所有缓存,只删当前版本不用的,别把动态缓存也清了。我曾经写错正则,把用户的草稿数据全删了……唉。

PWA缓存策略对比表格
PWA缓存策略对比表格

有个真实案例:Pinterest 的 PWA 重构后,加载速度提升了 40%,但一开始他们的缓存策略是 Network Only,根本没发挥离线能力。后来改成 Stale-While-Revalidate 处理图片,先展示占位图,后台更新,核心资源用预缓存。最终数据显示,用户平均会话时长增加 40%,广告点击率提高 50%。这可不是随意拿来的二手数据,是 Pinterest 工程师在技术博客里公布的。

但即便如此,我见过有团队把所有资源无脑预缓存,sw.js 体积暴涨,安装时间超过 3 秒,用户启动时直接卡死。正确的做法是用 Workbox 这样的库做预缓存的自动化管理和资源预加载的精细化控制。不过,Workbox 也不是银弹,它的配置项多得可以出本书,有些默认行为(比如对导航请求的缓存策略)在不理解内部机制时会引发稀奇古怪的问题。

推送通知?你可能会被用户骂得体无完肤

推送通知是 PWA 区别于传统网页的杀手锏,但你如果一上来就弹权限,那就等着掉用户吧。Chrome 的数据显示,48% 的用户会因不当授权请求直接离开网站

我的做法是:双重许可(double opt-in)。先展示一个自定义的提示,解释推送的好处,用户点击“同意”后再触发浏览器的原生权限弹窗。而且这个自定义提示要有明显的好处才出现,比如“订阅降价提醒”或“收货通知”,而不是“我们想给你发通知”。

技术层面的坑更大。推送服务依赖 VAPID 密钥和消息加密,一旦密钥丢失或过期,所有用户都收不到推送。我发现多数开发者根本不知道 VAPID 密钥有有效期,通常是一年,而且不同浏览器处理过期方式不一样。Firefox 会静默失效,Chrome 则会在控制台报错。我们必须搭建监控,定期检查订阅的有效性,必要时重新订阅。

另一个坑:Push API 的 payload 在浏览器端解密时依赖 用户可见的订阅端点(endpoint)。如果服务端保存的 endpoint 失效——比如用户清理了浏览器数据——推送就会失败。解决之道是:服务端要处理 410 Gone 响应,及时清理无效订阅。

还有,iOS 上的 PWA 推送直到 iOS 16.4 才全面支持,但之前的各种限制让开发者头大。即使是现在,manifest 中的 display 设为 standalone 时,iOS 的状态栏交互仍然别扭。你会发现用户很难找到退出全屏的方式——这是苹果的执念。

说到底,PWA 不是一套标准,而是一个持续演进的实践集。我见过太多团队被“一次开发,全平台运行”的口号冲昏头脑,忽视了平台差异和底层原理。Service Worker 是工具,但用好它需要你对 HTTP 缓存、Web 安全、浏览器存储有深刻的理解。

最后再爆个料:Manifest 里的 scope 属性如果不设对,你的应用会被浏览器认为是跨域导航,导致 service worker 不拦截,离线时直接 404。这个小细节,浪费了我整整一个下午。

免责声明:市场有风险,选择需谨慎!此文仅供参考,不作买卖依据。如有侵权请联系删除。
文章名称:PWA底层探秘:从Manifest到推送,那些说明书不会写的坑点
文章链接:https://m.lfdjt.com/info_23_8113.html