Appearance
桥接边界与错误协议
回到总览:混合开发与桥接相关模块:WebView H5 JSBridge 交互与安全防护
一句话定义
桥接工程化最容易翻车的不是「能不能通」,而是三类边界处理:① 大整数精度(snowflake id 超过 JS number 精度,必须字符串化);② 返回栈协商(goBack 先问 H5,H5 有历史就消费一条、无历史/超时才交还原生关页);③ 错误信封(桥错误用 {__isBridgeError:true} 包裹,让 H5 区分「原生报错」与「正常返回」)。
代码索引
对应 Lab:bridge-boundary-error-protocol-simulation · BridgeBoundaryErrorProtocolSimulation.java
为什么需要
- 为什么更大的 ID(如雪花 id)不能直接走 JSON 传给 H5?
- 一句话答:JS 的
number是双精度浮点,只有 53 位尾数;超过 2⁵³ 的整数(典型雪花 id)被解析后会丢精度,前端拿到错误的 id。必须双重编码成字符串传。
- 一句话答:JS 的
- 为什么原生
goBack不能直接关页,要先问 H5?- 一句话答:H5 内部还有自己的路由栈(如 Vue Router);原生一刀切关页会让用户在 H5 内部还有历史时误退出 App。必须先让 H5 协商:有历史就消费一条,没历史才关。
- 为什么桥返回要用独立的错误信封,而不是塞个
error字段?- 一句话答:塞字段不够彻底,容易被业务逻辑当成普通数据漏判;用独立错误信封(
__isBridgeError),H5 渲染层一眼识别,绝不会把异常当业务数据渲染。
- 一句话答:塞字段不够彻底,容易被业务逻辑当成普通数据漏判;用独立错误信封(
底层机制
1. 大整数精度:双重编码成字符串
跨 JS 边界的数字默认走 number。大整数必须字符串化,让 JS 当字符串解析而非当 number。
对应 Lab:bridge-boundary-error-protocol-simulation · 场景 1 · BridgeBoundaryErrorProtocolSimulation.java
java
// 大整数双重 encode:让 JS 当 string 解析,不当 number 丢精度
long snowflakeId = 1872039485720394857L; // > 2^53
String safe = "\"" + snowflakeId + "\""; // 等价于 JSON.encode(JSON.encode(id))
// H5 侧用 parseJsonPreservingId 解析,保留完整精度2. 返回栈协商:goBack 先问 H5
goBack 触发时,原生先问 H5 __tryGoBack;H5 有路由历史就消费一条(页面不关),无历史或超时未答才交还原生关页。原生侧还要兼容不同 WebView 实现对返回值的类型差异(bool / string / int)。
3. 错误信封:区分原生异常与正常返回
桥错误双向用 {__isBridgeError:true} 包裹;H5 判断是否含该标记来分流「异常」与「正常数据」。
java
// 错误信封:区分原生异常与正常返回
static String normalReturn(String data) {
return "{data:" + data + "}";
}
static String errorReturn(String code) {
return "{__isBridgeError:true, code:\"" + code + "\"}";
}Android / Flutter / Web / Backend 对照
| 维度 | Android / 原生 | Flutter / Dart | H5 / Web | 后端类比 |
|---|---|---|---|---|
| 大整数 | long 原生精度足够 | int 精度足够 | number 仅 53 位尾数 | 同 JS,需字符串化 |
| 返回栈 | onBackPressed 协商 | PopScope / 路由栈协商 | history.back() / Router | 前端路由守卫 |
| 错误协议 | 异常对象 | Result / 异常 | 错误信封 | gRPC status / HTTP 错误码 |
常见场景
1. 列表项 id 用雪花 id,详情页拿到错误 id
服务端返回雪花 id,若直接 JSON 数字传 H5,详情请求会带错 id。必须字符串化。
2. H5 内多层跳转后点返回
goBack 先问 H5,H5 逐层退出路由栈;栈空才真正关页。
常见误配、事故后果与排障
- 误配:ID 直接 JSON 数字传。
- 后果:雪花 id 末尾几位错乱,详情页张冠李戴。
- 排障:跨 JS 边界的大整数一律字符串化。
- 误配:原生
goBack直接关页。- 后果:H5 内部还有历史时误退 App,用户抱怨「退不回去」。
- 排障:先
__tryGoBack协商,带超时兜底。
- 误配:桥返回里塞
error字段区分。- 后果:业务逻辑漏判,把异常当数据渲染。
- 排障:用独立错误信封,渲染层强制分流。
与相近概念对比
- 错误信封 vs HTTP 状态码:HTTP 状态码在传输层就分流;桥接没有现成状态码通道,所以用信封在 payload 内显式标记。
- 返回栈协商 vs 原生返回拦截:原生拦截只知「要返回了」,不知 H5 内部栈深度;协商让 H5 自报是否有历史,决策更准。
对应实验
| Lab | 说明 | 源码 |
|---|---|---|
| bridge-boundary-error-protocol-simulation | 大整数精度 / 返回栈协商 / 错误信封三者仿真 | BridgeBoundaryErrorProtocolSimulation.java |
复习检查题
为什么雪花 id(snowflake id)跨桥传给 H5 时必须字符串化,而不能直接当 JSON 数字传?
答:JS 的
number是双精度浮点,尾数只有 53 位;雪花 id 通常超过 2⁵³,被 JSON 解析成number时会丢失低位精度,前端拿到的 id 与后端不一致,导致详情页/关联查询张冠李戴。双重编码成字符串可让 JS 当字符串原样保留。为什么
goBack要先问 H5,而不是直接由原生关页?答:H5 内部有自己的路由栈(如 Vue Router),用户在 H5 里可能还有多层历史;原生一刀切关页会把「H5 内部返回」误判成「退出 App」。先让 H5 协商——有历史就消费一条、没历史或超时未答才交还原生关页——返回语义才正确。
速记
- 大 ID 用字符串传,避免 JS
number精度丢失(> 2⁵³)。 goBack先协商:H5 有历史消费一条,无历史/超时才关页。- 错误信封
__isBridgeError:让 H5 一眼区分「原生异常」与「正常数据」。 - 跨 WebView 实现对返回值类型(
bool/string/int)都要兼容。