diff --git a/ylgamehall/Resources/JS/WebViewJavascriptBridge.js b/ylgamehall/Resources/JS/WebViewJavascriptBridge.js new file mode 100644 index 0000000..be41c3e --- /dev/null +++ b/ylgamehall/Resources/JS/WebViewJavascriptBridge.js @@ -0,0 +1,151 @@ +/* + * 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); } + } + } +})(); diff --git a/ylgamehall/RootViewController.swift b/ylgamehall/RootViewController.swift index 138359e..decc469 100644 --- a/ylgamehall/RootViewController.swift +++ b/ylgamehall/RootViewController.swift @@ -56,6 +56,22 @@ final class RootViewController: UIViewController { print("[ResourceUnzipper] ERROR: \(error)") } } + + // Phase 1.8 烟雾测试:确认 WebViewJavascriptBridge.js 已入 Bundle 且可读 + // 注:synchronized group 会把 ylgamehall/Resources/JS/ 子目录扁平化为 bundle 根, + // 所以无需 subdirectory 参数。 + if let url = Bundle.main.url(forResource: "WebViewJavascriptBridge", + withExtension: "js"), + let content = try? String(contentsOf: url, encoding: .utf8) { + print(""" + [WVJB.js] 已就绪 + path = \(url.path) + bytes = \(content.utf8.count) + head = \(content.prefix(60).replacingOccurrences(of: "\n", with: " ⏎ "))… + """) + } else { + print("[WVJB.js] ERROR: 找不到 Bundle 内 JS/WebViewJavascriptBridge.js") + } } override var supportedInterfaceOrientations: UIInterfaceOrientationMask {