现象:大厅 H5 能正确拿到定位,子游戏拿不到。已静态排除「子游戏未注册 handler」与「回调错发给大厅」两个猜测(BridgeBus 是 per-WebView 实例、 handlers 是实例状态,反向 call 捕获各自的 bridge,物理上不可能串台; 下载真实子游戏 zip 比对后确认 H5 侧大厅/子游戏逻辑完全对称)。 为定位真正的失败层,在各组件边界加日志(纯诊断,不改任何行为): - BridgeBus 加 label(lobby / subGame)区分来源,记录 ← H5 调用、 → H5 反向 call、native handler 未注册、evaluateJavaScript 失败 - sendToJS 改为返回 'ok'/'no-bridge',H5 侧 bridge 未就绪导致的静默丢包 现在会打错误日志(原实现 `if (window.X)` 直接丢弃,完全不可见) - LocationService 记录 requestOnce 序号 / shared manager id / requestLocation 的 BOOL 返回值 / completionBlock 是否回来 / stop() 调用 —— 高德文档明确 requestLocation 返回 NO 时 completionBlock 永不调用, 当前代码忽略该返回值会让 continuation 永挂,H5 连 errorCode 12 都收不到 - H5ErrorRelay 补 console.warn 中继,让 WVJB「H5 侧无对应 handler」的 warn 能出现在 Xcode console 契约影响:无。仅新增日志与可选 label 参数,桥接接口名 / 字段名 / 数据结构 均未变动(docs/H5-Native-Contract.md §3.1[20]/ §3.2[6]不变)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UMPCLfsuxvwgMotzsb67QH
137 lines
5.3 KiB
Swift
137 lines
5.3 KiB
Swift
//
|
||
// H5ErrorRelay.swift
|
||
// ylgamehall
|
||
//
|
||
// H5 错误中继:把 WebView 内 `console.error` / `window.onerror` /
|
||
// `unhandledrejection` 通过 `webkit.messageHandlers.h5error` 上报到原生 print。
|
||
//
|
||
// Phase 9.2(Design §11.2)。msext 时代仅靠用户/运营手工反馈;本项目作为
|
||
// 原则 B「内部自由现代化」的一部分,开发期把 H5 异常打到 Xcode console,
|
||
// 减少调试盲点。生产期无开关控制(输出 print 默认走 OSLog,对体积影响极小)。
|
||
//
|
||
// **不修改 H5 一行**(契约原则 A):用 WKUserScript `.atDocumentStart` 在
|
||
// 业务 JS 跑前就 hook,不要求 H5 团队配合。
|
||
//
|
||
// 仅在 BridgedWebView(大厅 + 子游戏)注入;OverlayViewController 是
|
||
// 第三方外链,错误不属于 ylgamehall 内部信号,故不挂。
|
||
//
|
||
|
||
import Foundation
|
||
import WebKit
|
||
|
||
@MainActor
|
||
public final class H5ErrorRelay: NSObject {
|
||
|
||
public static let shared = H5ErrorRelay()
|
||
|
||
/// WKScriptMessageHandler 名字。与下面 JS source 内 `webkit.messageHandlers.h5error` 必须一致。
|
||
public static let messageName = "h5error"
|
||
|
||
public override init() { super.init() }
|
||
|
||
/// 把 hook JS + message handler 注册到给定的 UserContentController。
|
||
/// BridgedWebView.init 在挂 WVJB 之后调一次。
|
||
public func install(into controller: WKUserContentController) {
|
||
let userScript = WKUserScript(
|
||
source: Self.javaScriptSource,
|
||
injectionTime: .atDocumentStart, // 业务 JS 之前 hook
|
||
forMainFrameOnly: true // iframe 不抓
|
||
)
|
||
controller.addUserScript(userScript)
|
||
controller.add(MessageProxy(target: self), name: Self.messageName)
|
||
}
|
||
|
||
fileprivate func handle(_ body: Any) {
|
||
guard let dict = body as? [String: Any],
|
||
let kind = dict["kind"] as? String else { return }
|
||
switch kind {
|
||
case "console.error":
|
||
let args = (dict["args"] as? [String]) ?? []
|
||
print("[H5 console.error]", args.joined(separator: " "))
|
||
case "console.warn":
|
||
// 桥诊断需要:WebViewJavascriptBridge.js 在「Native 发来的 handlerName
|
||
// H5 侧没注册」时走 console.warn,之前不上报导致这类丢包完全不可见。
|
||
let args = (dict["args"] as? [String]) ?? []
|
||
print("[H5 console.warn]", args.joined(separator: " "))
|
||
case "onerror":
|
||
let msg = (dict["msg"] as? String) ?? ""
|
||
let src = (dict["src"] as? String) ?? ""
|
||
let line = (dict["line"] as? Int) ?? 0
|
||
let col = (dict["col"] as? Int) ?? 0
|
||
let stack = dict["stack"] as? String
|
||
print("[H5 onerror] \(msg) at \(src):\(line):\(col)" + (stack.map { "\n\($0)" } ?? ""))
|
||
case "unhandledrejection":
|
||
let reason = (dict["reason"] as? String) ?? ""
|
||
let stack = dict["stack"] as? String
|
||
print("[H5 unhandledrejection]", reason + (stack.map { "\n\($0)" } ?? ""))
|
||
default:
|
||
print("[H5 unknown error]", dict)
|
||
}
|
||
}
|
||
|
||
/// JS 端 hook 三类异常源,统一通过 `webkit.messageHandlers.h5error.postMessage(payload)`
|
||
/// 上报。所有 send 调用都包 try/catch,hook 自身永远不应抛错(避免污染业务流程)。
|
||
private static let javaScriptSource = """
|
||
(function(){
|
||
function send(payload){
|
||
try { window.webkit.messageHandlers.h5error.postMessage(payload); } catch(e){}
|
||
}
|
||
var origErr = console.error;
|
||
console.error = function(){
|
||
try {
|
||
var args = [];
|
||
for (var i=0; i<arguments.length; i++) { args.push(String(arguments[i])); }
|
||
send({ kind:'console.error', args: args });
|
||
} catch(e){}
|
||
if (origErr) { origErr.apply(console, arguments); }
|
||
};
|
||
var origWarn = console.warn;
|
||
console.warn = function(){
|
||
try {
|
||
var args = [];
|
||
for (var i=0; i<arguments.length; i++) { args.push(String(arguments[i])); }
|
||
send({ kind:'console.warn', args: args });
|
||
} catch(e){}
|
||
if (origWarn) { origWarn.apply(console, arguments); }
|
||
};
|
||
window.addEventListener('error', function(ev){
|
||
send({
|
||
kind:'onerror',
|
||
msg: String(ev.message || ''),
|
||
src: String(ev.filename || ''),
|
||
line: ev.lineno || 0,
|
||
col: ev.colno || 0,
|
||
stack: (ev.error && ev.error.stack) ? String(ev.error.stack) : null
|
||
});
|
||
});
|
||
window.addEventListener('unhandledrejection', function(ev){
|
||
var r = ev.reason;
|
||
send({
|
||
kind:'unhandledrejection',
|
||
reason: String(r),
|
||
stack: (r && r.stack) ? String(r.stack) : null
|
||
});
|
||
});
|
||
})();
|
||
"""
|
||
}
|
||
|
||
// MARK: - WKScriptMessageHandler proxy(weak target 切断 retain cycle)
|
||
|
||
@MainActor
|
||
private final class MessageProxy: NSObject, WKScriptMessageHandler {
|
||
private weak var target: H5ErrorRelay?
|
||
|
||
init(target: H5ErrorRelay) {
|
||
self.target = target
|
||
super.init()
|
||
}
|
||
|
||
func userContentController(_ ucc: WKUserContentController,
|
||
didReceive message: WKScriptMessage) {
|
||
// WKScriptMessageHandler 在主队列派发;MessageProxy / H5ErrorRelay 都
|
||
// MainActor,直接调即可,无需 Task 切换。
|
||
target?.handle(message.body)
|
||
}
|
||
}
|