PWA 离线化实战:把计算器装进手机
让工程计算器在没网的地方也能用:预缓存的版本纪律、四平台的安装引导矩阵,以及'首屏死窗'的压缩实战。
工程现场的痛点很具体:工地可能没信号,但接触网的计算不能停。所以站内的计算工具很早就做了离线化——装到手机上,断网可用。这篇讲这条 PWA 路上的三个实战课题。
课题一:预缓存的版本纪律
离线化的核心是服务工作线程:首次访问时把应用的全部资源预缓存下来,之后断网也从缓存出。设计不难,难的是更新:缓存清单里的每个资源都带版本参数,任何资源变了,清单的版本、脚本里的版本常量、缓存策略的版本号要三处同步升级——漏一处,就会出现“线上是新功能、手机上是旧版本”的精神分裂,而且用户无感、你也无感。
我们为此立了“版本三连”纪律:改任何前端资源,三个版本点一起动,并在验收清单里加一项——用旧版本标识的缓存请求必须返回新版内容。听起来繁琐,但它防住的那类事故,排查起来远比纪律昂贵。
另一个反直觉的发现:预缓存清单会在每个首次访客的设备上自动拉取全部资源,产生的请求链条和爬虫的行为极其相似——我们曾在反爬分析中把它误判为爬虫指纹。自动化行为的取证,先分清“所有正常用户都会做的事”和“只有机器才做的事”,再下结论。
课题二:四平台的安装引导
“安装到手机”四个字,在四个平台上是四种现实。安卓的浏览器有标准的安装事件,监听到就能弹出原生安装按钮。iOS 从不支持这个事件——按钮永远不会亮,只能引导用户走“分享到主屏幕”的手动路径,而且要识别设备后把按钮文案改成对应的引导语。已安装的独立窗口模式则要全部隐藏。
识别 iPad 有个经典陷阱:它的浏览器标识伪装成桌面系统,要靠“多触点能力”来补判。最终的四平台行为矩阵:安卓原生按钮、iOS 未装显示引导加提示条、iOS 已装隐藏、桌面隐藏只剩同步选项——每一条都在真机验证过,而不是看文档写的。
课题三:首屏死窗的压缩
离线可用的代价是首屏之前要装载脚本。低端网络下,从页面出现到按钮可点的“死窗”一度明显。优化的路径:把非关键脚本延迟加载,让导航和内容先行渲染;关键交互的绑定内联到渲染层里,不再依赖延迟到达的脚本文件。实测死窗压缩了一个量级,三 G 模拟下也压到秒级出按钮。
两个工程细节:其一,内联进模板的脚本要小心引号——渲染层模板是单引号字符串,内联脚本必须全部用双引号,否则一个引号毁掉整页;其二,语法校验要在装了运行时的宿主机上做,容器里没有校验器时,校验命令的失败会被管道悄悄吞掉,给你一个“没问题”的假象。
离线策略:预缓存还是运行时缓存
离线方案有两条路线。运行时缓存:用户访问过什么就存什么,首次离线必丢;预缓存:首次访问就把清单内资源全部拉下来,断网完整可用。工程工具的场景是后者——工地上打开就要用,没有“先联网逛一圈”的前提。预缓存的代价是清单要人工维护:每个新增资源都要登记进清单,漏了它就离线不可用。我们用过一次自动化比对(构建产物与清单差集)来兜底,清单规模控制在几十条,首访拉取量在可接受范围。
更新的可达性
预缓存的另一个哲学问题是更新如何到达用户。机制上,脚本文件更新后浏览器会异步获取新版本,但已装载的旧版本仍服务到下一次刷新——意味着用户至少要多刷新一次才用上新版。我们的纪律是“版本三连”:脚本内版本常量、预缓存清单的资源参数、缓存策略的版本名,三处同步升级;配套一个验收断言——以旧版本标识发起的缓存请求,必须返回新内容。极端情况下(用户长期不刷新)还有兜底:站内提示与手动清缓存的引导路径。
安装矩阵的判定细节
四平台矩阵的判定实现里有几处精密活,单独记录。iOS 的识别:浏览器标识簇里有 iPhone、iPad、iPod 的特征串;而 iPad 的新系统标识伪装成桌面系统,要靠“多触点能力大于一”这个特征补判。已安装的判定:独立窗口模式有两个探针,标准属性与专属属性都要查,双保险覆盖不同版本的实现差异。引导提示条的设计:出现四秒自动消失,角色声明为状态播报——它是指引,不是弹窗,不能挡内容。这一整套判定逻辑上线前在真机矩阵里逐台过了一遍——文档写的平台行为与真机实况永远有出入,真机是唯一的真相。
死窗实测数字
优化的前后对比要给数字才有说服力:直连环境下,从页面出现到核心按钮可点,优化前约零点五秒,三 G 模拟下一点六秒;延迟加载非关键脚本、关键交互内联到渲染层之后,直连压到零点三七秒,三 G 一点六秒——三 G 的下限基本是网络与样式加载的物理底限,纯脚本优化的空间已尽。这组数字的另一个作用是止损:当优化撞到物理底限时,知道该停手了,把精力转去别处。
沉淀
PWA 这条路的所有坑,归结起来都是同一类:状态的位置。缓存的状态在用户设备上,安装的状态在浏览器里,版本的状态散落在三个文件中——它们都不在你的服务器上,你看不见,只能靠纪律、探针和真机矩阵去覆盖。前端工程做到深处,拼的不是框架,是对“状态住在哪里”的清醒。