Files
youle_app_ios_v2/ylgamehall/Source/Bridge/H5ErrorRelay.swift
T
joywayerandClaude Opus 5 3f8b382ad3 加桥/定位边界诊断日志,排查子游戏收不到 getlocationinfo
现象:大厅 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
2026-08-07 01:54:18 +08:00

137 lines
5.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// H5ErrorRelay.swift
// ylgamehall
//
// H5 错误中继:把 WebView 内 `console.error` / `window.onerror` /
// `unhandledrejection` 通过 `webkit.messageHandlers.h5error` 上报到原生 print。
//
// Phase 9.2Design §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 proxyweak 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)
}
}