把网页嵌进 App 的卖点很诱人:改一改页面,所有用户秒级拿到,不用过商店。到了生产环境,只要网页层需要原生侧尚未提供的东西,或者跑在旧二进制上的用户无法安全地跑新页面,这份「速度」就成了幻象。你最快的层被拴在最慢的层上,而你没法掌控这条链。

快车道与慢车道

原生发布按商店的节奏走。苹果 App Store 审核中位数大约 24 到 72 小时,但真正要命的是长尾:新开发者账号、敏感类目、节假日积压、被标记的构建,都能把一次发布拖到数周。Google Play 在众多渠道上同理。而用户若关了自动更新,会在旧版本上停留数周乃至数月。反观网页层,走 OTA 秒级更新——这正是混合架构的经典承诺。问题是两条车道并不独立。网页调用原生桥,而桥是一份只存在于特定原生构建里的契约。

旧二进制,新预期

当你发布一个依赖 3.2 版才新增的原生方法的网页时,每一个还停留在 3.0 或 3.1 的用户都会撞上「桥缺失」——面板空白、按钮失灵,若调用包裹不当还会硬崩。你不能简单地把网页推上去就完事,因为旧二进制的安装基数是一个你无法抹掉的事实。网页层必须为你早就不关心了的旧版本背负兼容垫片,或者把功能藏在一个「原生版本检查」后面——而版本号还得靠宿主愿不愿意暴露的信号去推断。无论哪种,网页团队都在写代码为商店的发布节奏买单。

你做不了的回滚

坏掉的网页 bundle 是这套耦合里最噩梦的版本。纯网站出事,重部署上个 commit,几分钟搞定。混合 App 里,如果坏页面是从你的服务器下发的,理论上你可以服务端回退——但前提是无碍于目前在场的每一个原生版本。如果这次坏发布假设了一个新原生契约,回退网页对那些跑在最新二进制上的用户毫无帮助,他们反而彻底没页面可用。而如果网页资源是打进原生 App 里的,唯一的回滚就是再提一次商店。根本没有按用户、按版本、独立的发布通道。你要么全有、要么全无,全看商店脸色。

为何会复利放大

这正是 #12(无远程熔断)与 #23(热更新合规)只在边缘打补丁的陷阱。没有受控发布层,每一次网页改动都变成对整张「安装基数矩阵」的押注:哪些原生版本在跑、它们暴露哪些桥、哪些用户开了自动更新。团队的应对是少发、把嵌入式网页冻进漫长的 QA 周期(反而葬送了嵌它的初衷),或者默默接受「部分用户看到坏页面,直到他们更新」。混合架构的优势——速度——恰恰在最需要它的时候蒸发,比如大促期间一个搞崩结账的 bug。

runtime 解法

宿主自有 runtime 用一条受控发布通道把两条车道解耦。它钉死「哪个网页版本跑在哪个原生版本上」,按安装下发正确的 bundle,并支持分级灰度与 runtime 级的即时回滚——而不是听命于商店。新原生能力通过 runtime 强制执行的版本化桥契约来协商,于是「需要 3.2」的页面在 3.0 上干脆不激活,而不是崩给你看。网页层保住速度,原生层保住安全,方向盘也回到你手里。

维度WebView宿主自有 runtime
网页更新秒级 OTA秒级 OTA
原生依赖假设桥「可能有也可能没有」强制版本化桥契约
旧二进制网页须垫片或崩按安装钉死正确 bundle
回滚商店提交或全有全无runtime 级分级回滚
发布掌控分裂、不可驾驭单一受控通道

本系列的主线始终是「控制权」。WebView 给你两条你无法同步的发布车道、一个你无法触发的回滚。runtime 让你锁定版本、协商契约、按自己的节奏回滚。对一套承载别人页面在你的 App 里交易的 catalog 来说,这中间的差距,就是「几分钟修好一个 bug」与「干等商店」的差距。

参考来源