H5 × 原生的日常:把"先给我个接口"做成通用 WebView JSBridge
- H5:这边要用相机/扫码/下载/剪贴板,给我个接口?
- 原生:行,把协议发一下,参数怎么传?回调怎么约?
- H5:这周要上线,先用
window.location
或注入一段脚本顶一顶行不? - 原生:行......但很快发现:回调全局乱飞、并发一多就撞车、版本不一致、排障一整天。
问题不在"谁做错了",而在缺少一套可复用的通信约定与工具化落地。本文将把"口头沟通"沉淀为通用的 JS-Native 通信模型:用 postMessage + callbackId
做请求-响应闭环,封装成统一门面,让协作从"每次聊"升级为"一次约定,长期受益"。
你将收获:
- 一套通用通信模型:URL Scheme → postMessage → 回调ID闭环
- 一组可复制的最小代码片段(Web 与原生两侧)
- 工程化要点:类型白名单、错误/并发处理、日志与性能优化
一代目:URL Scheme(能跑但难维护)
- JS → Native:修改
window.location
为自定义 scheme,如myapp://getUserInfo?id=1
- Native → JS:
evaluateJavaScript('window.cb(...)')
痛点:协议约定松散、参数编码繁琐、全局回调污染、并发难、排障难。
二代目:postMessage(规范的单向通道)
JS 在 WebView 内向原生发送结构化消息:
js
window.ReactNativeWebView.postMessage(
JSON.stringify({ type: 'getUserInfo', data: { userId: '1001' } })
);
单向通道已经优雅许多,但还缺少"请求-响应"的对应关系以及并发管理。
三代目:请求-响应 + 回调 ID(工程化标准)
核心思想:每次调用生成唯一 callbackId
,请求带上它,原生处理完成后带着相同 callbackId
回传给 JS,精确触发对应回调。
建议消息结构:
ts
interface BridgeMessage {
type: string; // 消息类型(受控白名单)
data?: any; // 业务参数
callbackId?: string; // 回调ID(请求生成,响应透传)
}
时序示意(简化):
sequenceDiagram
participant H5 as Web(JS)
participant Native as Host(WebView)
H5->>Native: postMessage {type, data, callbackId}
Native-->>Native: handleMessage(route by type)
Native->>H5: injectJavaScript execute(callbackId,'success',data)
大脑:回调管理器(Web 侧,支持回调与 Promise)
js
// 建议统一挂载到 window 下,命名避免与业务冲突
window.__bridgeCallbacks__ = {
callbacks: {},
register(type, successCallback, errorCallback) {
const callbackId = `${type}_${Date.now()}`;
if (!successCallback && !errorCallback) {
const promise = new Promise((resolve, reject) => {
this.callbacks[callbackId] = { success: resolve, error: reject };
});
return { promise, callbackId };
}
this.callbacks[callbackId] = { success: successCallback, error: errorCallback };
return callbackId;
},
execute(callbackId, status, data, deleteCallback = true) {
const cb = this.callbacks[callbackId];
if (!cb) return;
status === 'success' ? cb.success?.(data) : cb.error?.(data);
if (deleteCallback) delete this.callbacks[callbackId];
},
};
要点:
- 同一管理器同时支持回调式与 Promise 式 API
- 执行后删除以避免内存泄漏(必要时可传
deleteCallback=false
保留)
门面:统一的 JS API(Web 侧)
对业务暴露一个干净的门面对象,例如 window.native
:
js
// 回调式
window.native = window.native || {};
window.native.getUserInfo = function (successCallback, errorCallback) {
const { register } = window.__bridgeCallbacks__;
const callbackId = register('getUserInfo', successCallback, errorCallback);
window.ReactNativeWebView.postMessage(
JSON.stringify({ type: 'getUserInfo', callbackId })
);
};
// Promise 式
window.native.getUserInfoAsync = async function () {
const { register } = window.__bridgeCallbacks__;
const { promise, callbackId } = register('getUserInfoAsync');
window.ReactNativeWebView.postMessage(
JSON.stringify({ type: 'getUserInfo', callbackId })
);
return promise;
};
同理可扩展到剪贴板、缓存、相机/相册、扫码、文件下载、设备信息、打印等能力。
闭环:原生侧分发与回传(以 React Native 为例)
ts
// 消息入口(RN 侧 onMessage)
function handleMessage(event) {
try {
const { type, data, callbackId } = JSON.parse(event.nativeEvent.data);
const handler = getHandler(type);
if (!handler) return;
handler(data)
.then((result) => sendToWeb(callbackId, 'success', result))
.catch((err) => sendToWeb(callbackId, 'error', { message: String(err) }));
} catch (err) {
console.warn('invalid message', err);
}
}
// 回传工具(RN 侧)
function sendToWeb(callbackId, status, payload, deleteCallback = true) {
const script = `window.__bridgeCallbacks__.execute('${callbackId}', '${status}', ${JSON.stringify(payload)}, ${deleteCallback});`;
webViewRef.current?.injectJavaScript(script);
}
// handler 示例
const handlers = {
getUserInfo: async () => ({ name: 'Zoro', id: '1001' }),
};
const getHandler = (type) => handlers[type];
Promise 化:业务调用更丝滑
js
try {
const userInfo = await window.native.getUserInfoAsync();
console.log('用户信息', userInfo);
} catch (err) {
console.error('获取失败', err);
}
工程化要点(强烈建议)
- 类型白名单 :原生侧仅处理受控的
type
(集中枚举或常量表),默认拒绝未知类型。 - 参数校验:对 URL、文件路径等关键参数做格式校验与 try/catch,错误通过错误通道回传。
- 并发安全 :
callbackId
全局唯一(建议带上type
+ 时间戳/随机数);避免全局回调污染。 - 内存管理:回调执行后默认删除;长链路场景可按需保留。
- 可观测性 :
- 统一日志前缀(如
[Bridge]
),记录type / callbackId / 时延 / 结果
- 注意生产隐私脱敏,避免打出敏感字段
- 统一日志前缀(如
- 性能优化 :
- 精简消息体,避免传大对象/大 Base64
- 高频/批量操作原生侧聚合处理后一次回传,或分批
- 图片/二进制优先走本地路径或缓存引用,必要时再 Base64
- 就绪探针 :H5 注入
window.__bridge_ready__ = true;
,原生侧轮询/等待就绪后再批量调用
好了,以后就可以和原生小伙伴愉快的玩耍了...