/* * WebViewJavascriptBridge.js * * H5 端桥协议代码,与 Native BridgeBus(@MainActor BridgeProtocol 实现) * 一一对应。详见 docs/H5-Native-Implementation-Design.md §3.2 / §3.3。 * * 在 BridgedWebView 构造时通过 WKUserScript(.atDocumentStart)注入到所有 * 加载的 H5 页面。H5 业务代码运行时,window.WebViewJavascriptBridge 已就绪。 * * 协议(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; } // ── 内部状态 ───────────────────────────────────────────── var messageHandlers = {}; // H5 注册的 handler:name → fn(data, responseCallback) var responseCallbacks = {}; // 等待 Native 响应的 JS 回调:callbackId → fn(responseData) var nextCallbackId = 1; // ── 对外 API ───────────────────────────────────────────── var bridge = window.WebViewJavascriptBridge = { /** * H5 注册 handler,等待 Native 主动 callHandler。重名覆盖。 * @param {string} handlerName * @param {function(data, responseCallback)} handler */ registerHandler: function (handlerName, handler) { messageHandlers[handlerName] = handler; }, /** * H5 主动调 Native handler。 * @param {string} handlerName * @param {*} data 任意 JSON 可序列化值 * @param {function(responseData)} [responseCallback] */ callHandler: function (handlerName, data, responseCallback) { var callbackId = null; if (typeof responseCallback === 'function') { callbackId = 'cb_' + (nextCallbackId++) + '_' + Date.now(); responseCallbacks[callbackId] = responseCallback; } var message = { handlerName: handlerName }; if (data !== undefined && data !== null) { message.data = data; } if (callbackId) { message.callbackId = callbackId; } _postMessageToNative(message); }, /** * Native → H5 入口。BridgeBus.sendToJS 会注入: * window.WebViewJavascriptBridge._handleMessageFromObjC('') * base64 包装是为了避免 JSON 内容里的单引号 / 反斜杠扰乱 evaluateJavaScript 的字符串字面量。 */ _handleMessageFromObjC: function (base64String) { try { var json = _decodeBase64UTF8(base64String); var message = JSON.parse(json); _dispatchFromNative(message); } catch (e) { console.error('[WVJB] handle message from native failed:', e); } } }; // ── 内部 ───────────────────────────────────────────────── function _postMessageToNative(message) { try { window.webkit.messageHandlers.WVJBHandler.postMessage(message); } catch (e) { console.error('[WVJB] post to native failed (WVJBHandler 通道未注册?):', e); } } function _dispatchFromNative(message) { // Native 响应 H5 早前 callHandler if (message.responseId) { var cb = responseCallbacks[message.responseId]; if (cb) { delete responseCallbacks[message.responseId]; try { cb(message.responseData); } catch (e) { console.error('[WVJB] response callback threw:', e); } } return; } // Native 主动调用 H5 handler var handler = messageHandlers[message.handlerName]; if (!handler) { console.warn('[WVJB] no H5 handler registered for:', message.handlerName); return; } var responseCallback; if (message.callbackId) { var responseId = message.callbackId; responseCallback = function (responseData) { _postMessageToNative({ responseId: responseId, responseData: responseData }); }; } try { handler(message.data, responseCallback); } catch (e) { console.error('[WVJB] H5 handler "' + message.handlerName + '" threw:', e); } } /** * 标准 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('')); } // ── marcuswestin 旧式握手兼容 ───────────────────────────── // 部分 H5 代码这样初始化 bridge: // function setupWebViewJavascriptBridge(callback) { // if (window.WebViewJavascriptBridge) return callback(WebViewJavascriptBridge); // if (window.WVJBCallbacks) return window.WVJBCallbacks.push(callback); // window.WVJBCallbacks = [callback]; // /* iframe trick - WKWebView 模式不需要 */ // } // setupWebViewJavascriptBridge(function (bridge) { bridge.registerHandler(...) }); // // 我们在 documentStart 就注入 bridge,正常路径下 H5 拿到的 window.WebViewJavascriptBridge // 已存在,会立即同步 callback(bridge)。但若 H5 代码本身用了 WVJBCallbacks 队列模式 // (Array.push 在 bridge 之前),需要在此处 flush 一次。 if (Array.isArray(window.WVJBCallbacks)) { var pending = window.WVJBCallbacks; delete window.WVJBCallbacks; for (var i = 0; i < pending.length; i++) { try { pending[i](bridge); } catch (e) { console.error('[WVJB] pending WVJBCallback threw:', e); } } } })();