/* * WebViewJavascriptBridge.js * * H5 端桥协议代码,与 Native BridgeBus(@MainActor BridgeProtocol 实现) * 一一对应。详见 docs/H5-Native-Implementation-Design.md §3.2 / §3.3。 * * ⚠️ 本文件的**语义**必须与原工程 msext 的 `WebViewJavascriptBridge_JS.m` * (marcuswestin 原版,daoqi/msext/Class/WebViewJavascriptBridge/)保持一致。 * 传输层可以现代化(msext 用 iframe + _fetchQueue 拉取,我们用 * WKScriptMessageHandler 直推),但**派发时机与消息字段必须逐项对齐**, * 否则 H5 侧同一份代码在两个壳里的执行顺序不同 → 行为不一致。 * * 已对齐的 msext 语义(每一条都曾经不一致,是排查「子游戏定位拿不到」时发现的): * * 1. **异步派发**:msext `dispatchMessagesWithTimeoutSafety = true`,且 * `disableJavscriptAlertBoxSafetyTimeout` 在整个 msext 工程里**从未被调用**, * 所以 Native → H5 的每一条消息都走 `setTimeout(_doDispatch)`,在**新的 * macrotask** 上执行。早前本文件是同步派发(在 evaluateJavaScript 内直接 * 调 handler),H5 handler 相对自身 pending 脚本/微任务的顺序与 msext 不同。 * 2. **WVJBCallbacks 用 `setTimeout(..., 0)` flush**(msext `_callWVJBCallbacks`), * 不是同步 flush —— 决定 H5 的 `registerHandler` 何时生效。 * 3. **H5 → Native 的 response 消息带 `handlerName`**(msext `_doSend({handlerName, * responseId, responseData})`)。 * 4. `_disableJavascriptAlertBoxSafetyTimeout` 作为 H5 侧 handler 注册(msext 有)。 * 5. 缺省安装 `window.onerror`(msext 在 H5 未装时兜底装一个)。 * 6. 「收到 Native 消息但没有对应 handler」用 `console.log`(msext 原文),不是 warn。 * * 协议(Native ↔ H5 双向 JSON 消息): * - H5 → Native:window.webkit.messageHandlers.WVJBHandler.postMessage({...}) * - Native → H5:window.WebViewJavascriptBridge._handleMessageFromObjC('') * - 单条消息: * { handlerName, data?, callbackId? } // 主动调用对端 handler * { responseId, responseData? } // 响应对端早前 callHandler 的 callbackId */ ;(function () { if (window.WebViewJavascriptBridge) { return; } // msext 同款:H5 没装 onerror 时兜底装一个 if (!window.onerror) { window.onerror = function (msg, url, line) { console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line); }; } // ── 内部状态 ───────────────────────────────────────────── var messageHandlers = {}; // H5 注册的 handler:name → fn(data, responseCallback) var responseCallbacks = {}; // 等待 Native 响应的 JS 回调:callbackId → fn(responseData) var nextCallbackId = 1; // msext `dispatchMessagesWithTimeoutSafety`:默认 true,仅在 Native 下发 // `_disableJavascriptAlertBoxSafetyTimeout` 时置 false(msext 从不下发)。 var dispatchMessagesWithTimeoutSafety = true; // ── 对外 API ───────────────────────────────────────────── var bridge = window.WebViewJavascriptBridge = { /** * H5 注册 handler,等待 Native 主动 callHandler。重名覆盖。 */ registerHandler: function (handlerName, handler) { messageHandlers[handlerName] = handler; }, /** * H5 主动调 Native handler。 * msext `callHandler` 支持 2 参形式(第二个参数是函数时视为 responseCallback)。 */ callHandler: function (handlerName, data, responseCallback) { if (arguments.length === 2 && typeof data === 'function') { responseCallback = data; data = null; } _doSend({ handlerName: handlerName, data: data }, responseCallback); }, disableJavscriptAlertBoxSafetyTimeout: function () { dispatchMessagesWithTimeoutSafety = false; }, /** * Native → H5 入口。BridgeBus.sendToJS 会注入: * window.WebViewJavascriptBridge._handleMessageFromObjC('') * base64 包装是为了避免 JSON 内容里的单引号 / 反斜杠扰乱 evaluateJavaScript * 的字符串字面量(msext 走的是逐个 escape,等价)。 * * ⚠️ 无返回值:msext 的派发是 setTimeout 异步的,拿不到「H5 有没有跑到 * handler」的同步结果。原生侧不要依赖它的返回值做诊断。 */ _handleMessageFromObjC: function (base64String) { _dispatchMessageFromObjC(base64String); } }; // ── 内部 ───────────────────────────────────────────────── function _postMessageToNative(message) { try { window.webkit.messageHandlers.WVJBHandler.postMessage(message); } catch (e) { console.log('[WVJB] post to native failed (WVJBHandler 通道未注册?):', e); } } function _doSend(message, responseCallback) { if (typeof responseCallback === 'function') { var callbackId = 'cb_' + (nextCallbackId++) + '_' + new Date().getTime(); responseCallbacks[callbackId] = responseCallback; message['callbackId'] = callbackId; } _postMessageToNative(message); } /// msext `_dispatchMessageFromObjC` 逐句等价:默认走 setTimeout(新 macrotask)。 function _dispatchMessageFromObjC(base64String) { if (dispatchMessagesWithTimeoutSafety) { setTimeout(_doDispatchMessageFromObjC); } else { _doDispatchMessageFromObjC(); } function _doDispatchMessageFromObjC() { var message; try { message = JSON.parse(_decodeBase64UTF8(base64String)); } catch (e) { console.log('[WVJB] handle message from native failed:', e); return; } var responseCallback; if (message.responseId) { responseCallback = responseCallbacks[message.responseId]; if (!responseCallback) { return; } responseCallback(message.responseData); delete responseCallbacks[message.responseId]; return; } if (message.callbackId) { var callbackResponseId = message.callbackId; responseCallback = function (responseData) { // msext 同款:response 消息里**带 handlerName** _doSend({ handlerName: message.handlerName, responseId: callbackResponseId, responseData: responseData }); }; } var handler = messageHandlers[message.handlerName]; if (!handler) { console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message); // 诊断上报(不改 msext 语义,只是额外往自家通道发一条原生侧忽略的消息) _postMessageToNative({ diagDispatched: message.handlerName, hadHandler: false }); } else { var threw = null; try { handler(message.data, responseCallback); } catch (e) { threw = String(e); } _postMessageToNative({ diagDispatched: message.handlerName, hadHandler: true, threw: threw }); // msext 不吞异常(handler 抛错会冒到 setTimeout 的全局 onerror); // 这里把它原样重抛,保持行为一致,只是先记录了一笔。 if (threw !== null) { throw new Error(threw); } } } } /** * 标准 base64 → UTF-8 字符串。 * 直接 atob() 只能处理 latin1;这里用 decodeURIComponent + escape 处理多字节字符。 */ function _decodeBase64UTF8(b64) { return decodeURIComponent(Array.prototype.map.call(atob(b64), function (c) { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }).join('')); } // msext 同款:把 Native 可下发的 safety-timeout 开关注册成 H5 侧 handler bridge.registerHandler("_disableJavascriptAlertBoxSafetyTimeout", bridge.disableJavscriptAlertBoxSafetyTimeout); // ── 告知原生「桥已就绪」────────────────────────────────── // 等价 msext 的 `__bridge_loaded__` iframe 回调 → `injectJavascriptFile` → // flush `startupMessageQueue`。原生在收到本消息前发出的 callHandler 必须排队, // 不能丢(msext 有 startupMessageQueue,早前我们是直接丢弃)。 _postMessageToNative({ bridgeReady: true }); // ── marcuswestin 旧式握手兼容 ───────────────────────────── // msext `_callWVJBCallbacks` 用 setTimeout(..., 0),**不是同步 flush**: // 这决定 H5 的 registerHandler 相对页面其它顶层脚本的生效时机。 setTimeout(function () { var callbacks = window.WVJBCallbacks; if (!callbacks) { return; } delete window.WVJBCallbacks; for (var i = 0; i < callbacks.length; i++) { try { callbacks[i](bridge); } catch (e) { console.log('[WVJB] pending WVJBCallback threw:', e); } } }, 0); })();