上一篇 全链路 HTTP 编解码:从客户端到服务端的数据流转 讲解 HTTP 请求中的编解码。然而在客户端开发的日常里,除了 HTTP,还有一个更高频的跨端传输场景:与 H5 的数据交换,而 JS Bridge 正是其中不可或缺的桥梁。

本篇文章会沿着交互流程这条主线,只关注“编码 → 传输 → 解码”相关的细节。iOS、Android 与 H5 分别在什么时候序列化、什么时候做字符串转义、什么时候反序列化,以及这些差异会带来哪些坑。

下面以两个经典的开源实现为例,分析底层逻辑与代码实现:

1. 交互流程中的编解码骨架

无论是哪端实现,消息本质上都是 JSON。先把消息结构说清楚:

消息结构

消息分为请求响应两种格式:

// 请求消息:JS → Native 或 Native → JS
{
    handlerName: "getUserInfo",     // 要调用的处理器名称
    data: { userId: "123" },        // 业务参数
    callbackId: "cb_1_1703404800"   // 回调标识,用于匹配响应
}

// 响应消息:处理完成后返回
{
    responseId: "cb_1_1703404800",  // 对应请求的 callbackId
    responseData: { name: "张三" }   // 处理结果
}

接收方通过是否存在 responseId 来区分请求与响应。callbackId / responseId 虽然是流程字段,但它们在编码阶段就已经写入消息体,任何一端漏写或命名不一致,都会导致解码正常却无法匹配回调。

在实现里,iOS 用 NSDictionary 填充这些字段(WebViewJavascriptBridgeBase.m#L45-L62),Android 则用 WVJBMessage 类定义字段(WVJBWebView.java#L114-L120)。

交互流程中的编解码节点

  • JS 侧构造对象 → JSON.stringify(编码)。
  • Native 侧拿到字符串 → NSJSONSerialization / JSONObject(解码)。
  • Native 侧构造响应 → JSON 序列化,iOS 额外做 JS 字符串转义(编码)。
  • JS 侧 JSON.parse → 得到对象(解码)。

下面这张序列图展示了 JS Bridge 的双向通信流程。读图时只需要把注意力放在两次 JSON 编码与两次 JSON 解码上,其余机制只是把消息送达。

JS Bridge 双向通信流程

2. JS → Native 的编解码

这条链路里,H5 负责编码,Native 负责解码。

H5 侧编码

iOS 与 Android 的注入脚本虽然不同,但编码动作是一致的:先把消息序列化成 JSON 字符串(iOS 见 WebViewJavascriptBridge_JS.m#L72-L76,Android 见 WebViewJavascriptBridge.js#L12-L24)。

// iOS 端注入的 JS:把消息队列序列化成 JSON 字符串
function _fetchQueue() {
    var messageQueueString = JSON.stringify(sendMessageQueue);
    sendMessageQueue = [];
    return messageQueueString;
}

// Android 端注入的 JS:直接序列化单条消息
function _doSend(message, responseCallback) {
    // ...
    var msg = JSON.stringify(message || {});
    if (window.WVJBInterface) {
        WVJBInterface.notice(msg);
    } else {
        prompt("_wvjbxx", msg);
    }
}

JSON.stringify 有几个容易踩坑的点:

  • undefined 和函数不会被序列化,字段会直接消失。
  • 大整数在 JS 里是 Number,可能丢精度,跨端时建议用字符串传输。
  • NaNInfinity 会被序列化成 null

iOS 端解码

iOS 端通过 evaluateJavaScript 调用 _fetchQueue() 拉取队列(WKWebViewJavascriptBridge.m#L94-L101,命令字符串由 WebViewJavascriptBridgeBase.m#L155-L157 生成)。拿到 JSON 字符串后再用 NSJSONSerialization 反序列化(WebViewJavascriptBridgeBase.m#L205-L207):

// 获取 JS 端的消息队列
NSString *messageQueueString = [self _evaluateJavascript:@"WebViewJavascriptBridge._fetchQueue();"];

// 反序列化
- (NSArray *)_deserializeMessageJSON:(NSString *)messageJSON {
    return [NSJSONSerialization JSONObjectWithData:
        [messageJSON dataUsingEncoding:NSUTF8StringEncoding]
        options:0 error:nil];
}

Android 端解码

Android 端通过 JavascriptInterfaceprompt 拿到字符串消息(WVJBWebView.java#L371-L379WVJBWebView.java#L626-L680)。这两条通道只是传输差异,拿到的都是 JSON 字符串,后续再交给 JSONObject 解析并分发(WVJBWebView.java#L241-L275):

// 方式一:通过 JavascriptInterface
@JavascriptInterface
public void notice(String message) {
    handleMessage(message);
}

// 方式二:通过拦截 prompt
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
    if (message.equals("_wvjbxx")) {
        handleMessage(defaultValue);
        result.confirm("");
        return true;
    }
    return super.onJsPrompt(view, url, message, defaultValue, result);
}

3. Native → JS 的编解码

这条链路里,Native 负责编码,H5 负责解码。关键在于 Native 侧如何把对象安全地塞进 JS 代码中。

iOS 端编码

发送消息时,通过 NSJSONSerialization 将字典序列化为 JSON 字符串。序列化方法见 WebViewJavascriptBridgeBase.m#L201-L203,派发方法见 WebViewJavascriptBridgeBase.m#L178-L198

// 序列化方法(简化)
- (NSString *)_serializeMessage:(id)message {
    return [[NSString alloc] initWithData:
        [NSJSONSerialization dataWithJSONObject:message options:0 error:nil]
        encoding:NSUTF8StringEncoding];
}

// 派发消息到 JS
- (void)_dispatchMessage:(NSDictionary *)message {
    NSString *messageJSON = [self _serializeMessage:message];

    // 转义特殊字符,防止 JS 执行出错
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\'"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];

    NSString *js = [NSString stringWithFormat:
        @"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    [self _evaluateJavascript:js];
}

这里的转义非常关键,因为 JSON 字符串被包在 JavaScript 的单引号字符串里,必须避免破坏字符串边界:

原始字符 转义后 说明
\ \\ 反斜杠本身需要转义
" \" 双引号,防止破坏字符串边界
' \' 单引号,因为外层用单引号包裹
\n \\n 换行符
\r \\r 回车符
\f \\f 换页符
\u2028 \\u2028 行分隔符(JS 中会被解释为换行)
\u2029 \\u2029 段落分隔符(同上)

特别注意 \u2028\u2029。它们在 JSON 中合法,但在 JS 字符串字面量中会被当作换行处理,导致语法错误。

Android 端编码

Android 端使用 JSONObject 进行序列化(序列化见 WVJBWebView.java#L278-L300,派发见 WVJBWebView.java#L235-L238):

private JSONObject message2JSONObject(WVJBMessage message) {
    JSONObject jo = new JSONObject();
    if (message.callbackId != null) {
        jo.put("callbackId", message.callbackId);
    }
    if (message.data != null) {
        jo.put("data", message.data);
    }
    // ... 其他字段
    return jo;
}

private void dispatchMessage(WVJBMessage message) {
    String messageJSON = message2JSONObject(message).toString();
    // 执行 JS
    evaluateJavascript("WebViewJavascriptBridge._handleMessageFromJava(" + messageJSON + ")");
}

由于 JSON 直接作为对象字面量拼进 JS 调用中,不需要像 iOS 一样把 JSON 放进单引号字符串,因此少了一层字符串转义。

H5 侧解码

iOS 端传进来的是字符串,因此 JS 侧需要 JSON.parseWebViewJavascriptBridge_JS.m#L78-L113):

function _dispatchMessageFromNative(messageJSON) {
    var message = JSON.parse(messageJSON);
    // 处理消息...
}

Android 端如果直接传对象,JS 侧可以跳过 JSON.parse,这取决于 Native 侧是否以字符串形式传参(WVJBWebView.java#L235-L238WebViewJavascriptBridge.js#L44-L84)。

4. 编解码语义差异与坑点

字段一致性

三端约定的字段名必须一致,包括 handlerNamedatacallbackIdresponseIdresponseData。前缀差异只影响调试,不影响解析,但字段名一旦变了,就会出现“解码正常却找不到回调”的现象。

JSON 语义差异

场景 JavaScript iOS(NSJSONSerialization) Android(JSONObject)
null null NSNull JSONObject.NULL
undefined undefined(不会被序列化) - -
布尔值 true / false @YES / @NO true / false
大整数 可能丢失精度 NSNumber long

特别注意 undefined。JavaScript 中对象的 undefined 值在 JSON.stringify 时会被忽略,这可能导致字段丢失。

解析兜底

任何一端的 JSON.parse 失败都会中断后续流程,建议加一层 try-catch:

function _dispatchMessageFromNative(messageJSON) {
    try {
        var message = JSON.parse(messageJSON);
        // ...
    } catch (e) {
        console.error("Bridge message parse error:", e);
    }
}

5. 编解码排查清单

遇到“回调不执行”“数据丢失”“中文乱码”时,按下面顺序排查最省时:

  1. 编码阶段:检查 JSON.stringify / NSJSONSerialization / JSONObject 是否成功。
  2. 转义阶段:确认 iOS 端的特殊字符转义是否完整,尤其是 \u2028 / \u2029
  3. 解码阶段:检查 JSON.parse 是否抛异常,Native 端反序列化是否失败。
  4. 字段阶段:核对 callbackId / responseId 是否一致。

6. 总结

JS Bridge 的编码链路并不神秘,本质就是 JSON 的序列化与反序列化,差别在于传输通道与字符串转义。只要把每一次编码与解码的位置标清楚,问题通常都能快速定位。