Appearance
桥接契约版本治理:把 JSBridge 当作版本化 API 表面
回到总览:混合开发与桥接相关模块:WebView H5 JSBridge 交互与安全防护 · 跨端一致性设计:多端架构分层与设计系统
一句话定义
把原生 ↔ H5 的桥接通道当成一套版本化 API 表面来管理:每个通道是一个「契约(名字 + 版本号)」,破坏性变更必须升版本;由宿主在 WebView 全局注入一份「桥接元信息(bridge meta)」,H5 据此做能力探测与降级适配;再用一条 CI 校验把「契约漂移」挡在编译前。
代码索引
对应 Lab:contract-consistency-simulation · ContractConsistencySimulation.java
为什么需要
- 为什么 JSBridge 不能像内部函数调用那样随意改返回字段?
- 一句话答:H5 是独立发布周期的前端,与原生壳的发版节奏不同;原生偷偷改了某个通道的返回结构,线上 H5 不会同步更新,立刻大面积报错。通道必须显式版本化,让两端按契约协商。
- 为什么只有「加可选字段」才算向后兼容,其它改动都不算?
- 一句话答:旧端只取已知字段、忽略未知字段才能继续跑;一旦类型变更、删字段、把可选变必填,旧端解析即崩溃——这些才是破坏性变更,必须升版本号。
- 为什么要在 WebView 全局注入 bridge meta,而不是直接约定「双方都用 v2」?
- 一句话答:同一份 H5 可能同时被多个版本的原生壳加载(老包用户没升级);H5 只能在运行时探测「当前壳支持哪些通道、什么版本」,再决定走哪套适配逻辑,编译期约定无法覆盖多版本并存。
底层机制
1. 契约注册表 + 元信息注入
宿主侧维护一份 ChannelContract(name, version) 契约表(单一事实源);在页面加载前把元信息注入 WebView 全局,供 H5 探测能力。
对应 Lab:contract-consistency-simulation · ContractConsistencySimulation.java
dart
// 原生注册的通道契约表(单一事实源)
final contractTable = [
ChannelContract('getDeviceInfo', 1),
ChannelContract('openGallery', 2), // v2 新增了 compression 参数
ChannelContract('requestPayment', 1),
];
// 注入 WebView 全局,供 H5 探测能力(示意字段,非某仓库实现)
void injectBridgeMeta(WebViewController vc) {
final meta = {
'bridgeVersion': 3,
'appVersion': '5.2.0',
'channels': contractTable
.map((c) => {'name': c.name, 'version': c.version})
.toList(),
};
vc.runJavaScript('window.__BRIDGE_META__ = ${jsonEncode(meta)}');
}关键点:契约表是「单一事实源」,多端都从这里生成/校验;注入的 meta 只是它的运行时投影,解决「同一 H5 跑在多版本壳」的探测问题。
2. H5 能力探测与降级适配
H5 读 meta,对需要的能力查版本;低版本走 normalizers(适配层)把老结构补成新结构;缺失通道则禁用对应功能并提示升级。
javascript
// H5 侧能力探测(示意字段,非某仓库实现)
function channelVersion(name) {
const meta = window.__BRIDGE_META__;
const ch = (meta?.channels || []).find(c => c.name === name);
return ch ? ch.version : 0; // 0 = 当前壳不支持
}
// openGallery v2 才有 compression 参数,老壳走无参路径
function openGalleryAdapted() {
if (channelVersion('openGallery') >= 2) {
return nativeCall('openGallery', { compression: 'high' });
}
return nativeCall('openGallery', {}); // 老壳走适配路径
}3. CI 强校验:契约漂移挡在编译前
提交时跑一条校验:① 注册通道集合必须等于契约表集合(不能注册了却不登记,或登记了却没实现);② 同名通道版本号在所有登记处必须一致。不一致直接 fail 构建。
对应 Lab:contract-consistency-simulation · 场景 6(演示「注册表 vs 契约表」CI 校验的失败判定)
bash
# 契约漂移校验思路(示意,非某仓库实现)
fail=0
# 1. 注册的通道名集合 必须等于 契约表集合
comm -3 <(list_registered | sort) <(list_contracts | sort) && fail=1
# 2. 同名通道版本必须一致
diff <(list_registered_with_version) <(list_contracts_with_version) && fail=1
[ $fail -eq 0 ] && echo "契约一致 ✅" || { echo "契约漂移 ❌"; exit 1; }Android / Flutter / Web / Backend 对照
| 维度 | Android WebView | Flutter WebView | H5 / Web | 类比 Backend |
|---|---|---|---|---|
| 契约登记 | Java 侧维护契约表 | Dart 侧维护契约表 | 读 window.__BRIDGE_META__ 探测 | 契约即 OpenAPI Schema |
| 元信息注入 | evaluateJavascript | runJavaScript | 只读 | API 版本走 Header/URL |
| 版本治理 | CI 校验脚本 | 同左 | normalizers 适配 | 网关 API 兼容性测试 |
桥接版本治理与 REST API 版本治理本质相同(都是版本化 API 表面),差异只在:桥接的「客户端」是 H5、版本信息不走 HTTP Header 而走注入的 meta;CI 校验代替了网关的 API 兼容性测试。
常见场景
1. 给 openGallery 加 compression 参数(非破坏性)
新增参数设为可选:旧 H5 不传也能跑,新 H5 传了新壳才生效。无需升版本,旧端按「忽略未知参数」天然兼容。
对应 Lab:contract-consistency-simulation · 场景 2(加可选字段兼容)
2. 把 getUserProfile 返回值扁平→嵌套(破坏性,必须升版本)
结构变化会让旧 H5 取不到字段。必须升 version,并在 meta 中体现;旧壳用户要么走 normalizers 适配,要么提示升级。
对应 Lab:contract-consistency-simulation · 场景 3(类型变更破坏性)
常见误配、事故后果与排障
- 误配:「我改了返回结构但没升版本,因为只多了一个字段」——实际是重命名/类型改,旧端崩溃。
- 后果:线上旧 H5 大面积解析失败。
- 排障:先看 meta.channels 里该通道版本,确认两端是否一致;类型变更必须升版本。
- 误配:「约定好都用 v2 就行,不用注入 meta」——老壳用户加载新 H5。
- 后果:功能缺失或崩溃。
- 排障:H5 必须在运行时探测 meta,不能硬编码版本。
- 误配:注册了通道却忘了登记进契约表(或版本写错)。
- 后果:契约漂移,靠人工 review 极易漏。
- 排障:CI 强校验拦截(见场景 6)。
与相近概念对比
- 契约版本 vs REST API 版本:机制同源;桥接把「版本信息」放在注入的 meta 而非 HTTP Header,CI 校验替代网关兼容性测试。
- 契约版本 vs 普通白名单:白名单解决「谁能调」(安全),契约版本解决「调的是什么结构」(兼容性),两者正交、都要做。
对应实验
| Lab | 说明 | 源码 |
|---|---|---|
| contract-consistency-simulation | 契约兼容/破坏仿真 + 注册表 vs 契约表 CI 强校验 | ContractConsistencySimulation.java |
复习检查题
为什么给桥接通道「加一个可选字段」是安全的,而「把字段从 String 改成数组」是破坏性变更?
答:旧端只取已知字段、忽略未知字段,所以多一个可选字段它照样能跑;但类型一变,旧端按原类型强转就会
ClassCastException崩溃。判断标准不是「字段数量」,而是「旧端能否在不改代码的情况下继续正确解析」。既然已经约定好「都用 v2」,为什么还要在运行时注入 bridge meta 让 H5 探测?
答:H5 和原生壳的发布节奏不同,同一份 H5 会被多个版本的原生壳加载(老包用户未必升级)。H5 无法在编译期确定自己运行在哪个壳里,只能在运行时探测当前壳支持的通道与版本,再走对应适配逻辑;约定「都用 v2」只适用于强管控的单壳场景。
速记
- 桥接通道 = 版本化 API 表面;破坏性变更必升版本。
- meta 注入解决「同一 H5 跑在多版本壳」的探测问题。
- 兼容性靠「旧端忽略未知字段」;破坏性是「类型 / 必填 / 删字段」。
- CI 校验:注册表 == 契约表、同名版本一致,挡住契约漂移。