做 JS Bridge 时,很多人会先讨论底层通信方式:iOS 用 WKScriptMessageHandler,Android 用注入对象,老版本用 URL Scheme。底层通道当然重要,但更关键的是协议。
通道只是“消息怎么送过去”,协议决定“消息长什么样、怎么解释、怎么升级、怎么处理错误”。如果协议没有设计好,底层通道再稳定,业务接入也会越来越混乱。
一个 Bridge 协议至少要覆盖几类问题:
H5 如何描述一次调用客户端如何识别方法和参数客户端如何返回成功或失败异步回调如何和请求对应客户端如何主动通知 H5不同版本如何兼容权限和安全边界如何表达
协议设计清楚以后,Bridge SDK、客户端实现、业务页面接入和后续调试都会简单很多。
方法名应该有命名空间
Bridge 方法不能随便命名成 share、login、open。随着能力变多,这类名字会越来越模糊。
更好的方式是使用命名空间:
user.getLoginInfouser.openLoginshare.openPanelnavigation.openPagedevice.getNetworkStatustracking.reportEventimage.choose
命名空间的好处是职责清晰。看到 share.openPanel 就知道它属于分享能力,看到 navigation.openPage 就知道它属于页面跳转能力。
方法名也应该保持语义稳定。不要把一个方法不断塞入新含义。例如 openPage 只负责打开页面,不应该后来又承担登录校验、埋点上报、参数修复等职责。否则一个方法会越来越难维护。
请求协议要保持结构稳定
一次 H5 到 Native 的调用可以设计成统一结构:
{ "id": "bridge_10001", "method": "navigation.openPage", "params": { "url": "app://car/detail?id=123", "animated": true }, "meta": { "bridgeVersion": "1.2.0", "timestamp": 1553230000000, "source": "h5" }}
这里不要把所有字段都混在一起。比较清晰的划分是:
| 字段 | 作用 |
|---|---|
id | 请求唯一标识,用于异步回调 |
method | 要调用的客户端能力 |
params | 业务参数 |
meta | Bridge 版本、时间戳、来源等元信息 |
params 是业务可变部分,id、method、meta 是协议层稳定部分。这样协议层和业务层边界比较清楚。
响应协议要区分业务失败和系统失败
客户端返回结果时,不能只返回一个 success: true。实际业务里失败类型很多:
- 用户取消登录或分享。
- 客户端不支持该方法。
- 参数校验失败。
- 权限不足。
- 原生能力执行异常。
- Bridge 调用超时。
响应结构可以统一成:
{ "id": "bridge_10001", "code": 0, "message": "ok", "data": { "result": true }, "meta": { "nativeVersion": "8.5.0" }}
错误时:
{ "id": "bridge_10001", "code": 4003, "message": "method not supported", "data": null}
错误码要有分层:
| 错误类型 | 示例 |
|---|---|
| 协议错误 | 参数格式错误、缺少 method |
| 能力错误 | method 不存在、版本不支持 |
| 权限错误 | 域名不可信、用户未授权 |
| 用户行为 | 用户取消登录、取消分享 |
| 系统异常 | 客户端执行异常、超时 |
这样业务层可以根据错误类型做不同处理。用户取消分享不应该当成系统异常;客户端版本不支持则应该走降级。
参数要能校验和演进
Bridge 参数不能完全靠约定。客户端和 H5 之间一旦版本不一致,很容易出现参数缺失或类型错误。
每个方法最好都有参数协议:
type OpenPageParams = { url: string; animated?: boolean; replace?: boolean;};
客户端也要做校验:
method = navigation.openPage -> url 必填 -> animated 可选,默认 true -> replace 可选,低版本不支持时忽略
参数新增时尽量只加可选字段,不要改变已有字段含义。比如 animated 原来表示是否开启动画,就不要后来改成动画类型。需要表达新语义时,新增字段比复用旧字段更安全。
协议演进要遵守几个原则:
- 新增可选字段通常安全。
- 删除字段风险很高。
- 改变字段类型风险很高。
- 改变字段语义风险最高。
- 新能力优先新增 method,不要污染旧 method。
这和后端 API 设计很像。Bridge 协议本质上也是 H5 和客户端之间的 API。
事件协议要和调用协议分开
H5 调 Native 是 request/response 模式;Native 主动通知 H5 是 event 模式。两者不要混成一套。
调用协议:
{ "id": "bridge_10001", "method": "user.openLogin", "params": {}}
事件协议:
{ "event": "user.loginChange", "payload": { "isLogin": true, "userId": "123" }, "meta": { "timestamp": 1553230000000 }}
事件没有 requestId,因为它不是某次调用的响应。业务页面通过 bridge.on 监听:
bridge.on("user.loginChange", payload => { updateUser(payload);});
事件也需要命名空间,例如:
app.resumeapp.pauseuser.loginChangenetwork.changenavigation.back
这样可以避免客户端随意执行 H5 全局函数,也避免页面暴露太多临时 callback。
权限最好进入协议层
Bridge 能力越多,权限越重要。不是所有 H5 页面都应该能调用所有客户端能力。
可以把权限分成几层:
公开能力:打开页面、获取基础环境登录能力:获取用户信息、调起登录敏感能力:定位、相册、设备信息业务能力:特定业务域名才能调用
协议层可以在 meta 中携带来源:
{ "method": "device.getLocation", "params": {}, "meta": { "url": "https://m.example.com/activity", "host": "m.example.com" }}
客户端根据当前 WebView URL 和方法白名单判断是否允许调用:
m.example.com -> share.openPanel -> user.openLogin -> navigation.openPagetrusted.example.com -> device.getLocation -> image.choose
注意,来源不能完全相信 H5 自己传入的参数。客户端应该从 WebView 当前 URL 获取真实来源,再做校验。
Bridge SDK 要提供能力探测
协议设计里很容易忽略一个问题:H5 如何知道客户端支持哪些方法?
如果 H5 直接调用新方法,而用户客户端版本较低,就会失败。更好的方式是提供能力探测:
const supported = await bridge.canIUse("share.openPanel", "2.0.0");if (supported) { await bridge.call("share.openPanel", params);} else { showWebShareFallback();}
客户端可以暴露一份能力表:
{ "bridgeVersion": "2.3.0", "methods": { "share.openPanel": "1.0.0", "navigation.openPage": "1.0.0", "image.choose": "2.1.0" }}
Bridge SDK 缓存这份能力表,业务调用前可以判断。
这比简单判断 App 版本更好。因为不同端、不同灰度批次、不同容器可能支持的 Bridge 能力并不完全一致。
调试信息也应该标准化
Bridge 问题通常很难排查:H5 说已经调用了,客户端说没有收到,或者客户端说回调了,H5 说没拿到。
所以协议里要保留调试信息:
request idmethodparamssend timenative receive timenative response timeresponse codeerror message
开发环境可以提供 Bridge 调试面板,记录所有调用:
[bridge_10001] share.openPanel pending[bridge_10001] share.openPanel success 128ms[bridge_10002] image.choose failed method_not_supported
线上也可以对失败率较高的方法做采样上报。Bridge 是跨端链路,缺少日志时很难定位问题。
小结
JS Bridge 的长期维护成本主要由协议决定,而不是由底层通信方式决定。
一套清晰的 Bridge 协议应该包括:
- 有命名空间的方法名。
- 稳定的 request/response 结构。
- 可分层处理的错误码。
- 可演进的参数协议。
- 独立的事件协议。
- 方法白名单和权限校验。
- 能力探测和版本判断。
- 标准化调试日志。
底层通道可以随着 WebView 能力升级而变化,但协议最好保持稳定。只要协议稳定,H5、客户端和业务页面都能围绕同一套契约协作。