js与原生通讯版本演进

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;,原生侧轮询/等待就绪后再批量调用

好了,以后就可以和原生小伙伴愉快的玩耍了...

相关推荐
zhangphil1 分钟前
Android Coil3视频封面抽取封面帧存Disk缓存,Kotlin
android·kotlin
Fly-ping3 分钟前
【前端八股文面试题】【JavaScript篇3】DOM常⻅的操作有哪些?
前端
2301_810970397 分钟前
Wed前端第二次作业
前端·html
猪哥帅过吴彦祖10 分钟前
Flutter SizeTransition:让你的UI动画更加丝滑
android·flutter
不浪brown12 分钟前
全部开源!100+套大屏可视化模版速来领取!(含源码)
前端·数据可视化
iOS大前端海猫14 分钟前
drawRect方法的理解
前端
姑苏洛言29 分钟前
有趣的 npm 库 · json-server
前端
知否技术33 分钟前
Vue3项目中轻松开发自适应的可视化大屏!附源码!
前端·数据可视化
Hilaku35 分钟前
为什么我坚持用git命令行,而不是GUI工具?
前端·javascript·git
用户adminuser37 分钟前
深入理解 JavaScript 中的闭包及其实际应用
前端