Files
youle_app_ios_v2/ylgamehall/Source/Bridge/H5ErrorRelay.swift
T
joywayer f4acccdd6c H5ErrorRelay 增加 console.log/info 转发,查明 Console 空的根因
排查「Safari Web Inspector 里 H5 控制台没输出」时定位到首要原因不在原生侧:
gamehall.zip 的 js/01_SubGame/00_SubGame_Config.js 里 Game_Config.Debugger
.isDebugger 出厂为 false,把「发送数据 / 接收数据」等高频日志全挡住了;未被
挡住的 Connect:... 又在建 websocket 时就打完,挂上 Inspector 时早已过去。
调试时在 Safari 控制台运行时改 Game_Config.Debugger.isDebugger = true 即可,
不动 H5 任何文件(原则 A)。

顺带补齐原生转发能力:
- 新增 console.log / console.info 转发,仅 Debug 构建注入(#if DEBUG 编译期
  摘掉那段 JS)。Release 包不为每条业务日志付 JS→Native IPC,也不把 H5 内部
  输出暴露给外部审阅;已验证 Release 二进制中该 hook 字符串命中 0 次
- log/info 走裸 print 而非 diagLog:diagLog 会落盘到 Documents/diag.log,而该
  sink 无轮转无上限(BridgeBus.swift:47),isDebugger 打开后 firehose 会吃光磁盘
- 参数格式化统一走 fmt(),对象改用 JSON.stringify —— 大厅业务大量 console.log(msg)
  打整包,原先 String(obj) 只得到无用的 [object Object];循环引用逐级回退
- 原始 console 调用照常透传,不影响 Safari 侧同时查看

无契约影响:H5 可观察行为不变,Release 行为完全不变。
docs/H5-Debug-Guide.md 同步补 §7 Q3 根因 + 新增 §8.1 转发能力表。
2026-08-08 14:05:37 +08:00

191 lines
8.2 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` / `console.warn` /
// `window.onerror` / `unhandledrejection` 通过 `webkit.messageHandlers.h5error`
// 上报到原生 printDebug 构建下额外转发 `console.log` / `console.info`。
//
// 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.log", "console.info":
// 仅 Debug 构建会有这两类(见 javaScriptSource 的 #if DEBUG)。
// ⚠️ 走裸 print 而不是 diagLogH5 把 `Game_Config.Debugger.isDebugger`
// 打开后每个收发包都会 console.log 一次,而 diagLog 会同步落盘到
// `Documents/diag.log`(无轮转、无大小上限,见 BridgeBus.swift:47),
// firehose 灌进去会把设备磁盘吃光。Xcode console 看得到就够了。
let args = (dict["args"] as? [String]) ?? []
print("[H5 \(kind)] " + args.joined(separator: " "))
case "console.error":
let args = (dict["args"] as? [String]) ?? []
diagLog("[H5 console.error] " + args.joined(separator: " "))
case "console.warn":
// 桥诊断需要:WebViewJavascriptBridge.js 在「Native 发来的 handlerName
// H5 侧没注册」时走 console.warn,之前不上报导致这类丢包完全不可见。
let args = (dict["args"] as? [String]) ?? []
diagLog("[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
diagLog("[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
diagLog("[H5 unhandledrejection] " + reason + (stack.map { "\n\($0)" } ?? ""))
default:
diagLog("[H5 unknown error] \(dict)")
}
}
/// JS 端 hook 各类日志/异常源,统一通过 `webkit.messageHandlers.h5error.postMessage(payload)`
/// 上报。所有 send 调用都包 try/catch,hook 自身永远不应抛错(避免污染业务流程)。
///
/// `console.log` / `console.info` 只在 Debug 构建注入:Release 包不该为每条业务
/// 日志付一次 JS→Native IPC,也不该把 H5 内部输出暴露给外部审阅。
private static var javaScriptSource: String {
#if DEBUG
let verboseHooks = verboseConsoleHookJS
#else
let verboseHooks = ""
#endif
return """
(function(){
function send(payload){
try { window.webkit.messageHandlers.h5error.postMessage(payload); } catch(e){}
}
// 参数格式化:直接 String(obj) 会得到无用的 "[object Object]",而大厅业务
// 大量使用 console.log(msg) / console.log(res) 打整包,故对象走 JSON.stringify。
// 循环引用时 stringify 抛错,回退 String();再抛就放弃,绝不让 hook 影响业务。
function fmt(v){
try {
if (v === null) { return 'null'; }
if (v === undefined) { return 'undefined'; }
var t = typeof v;
if (t === 'string') { return v; }
if (t === 'number' || t === 'boolean' || t === 'function') { return String(v); }
if (v instanceof Error) { return v.stack || (v.name + ': ' + v.message); }
var s = JSON.stringify(v);
return (s === undefined) ? String(v) : s;
} catch(e) {
try { return String(v); } catch(e2) { return '[unstringifiable]'; }
}
}
function collect(a){
var out = [];
for (var i=0; i<a.length; i++) { out.push(fmt(a[i])); }
return out;
}
var origErr = console.error;
console.error = function(){
try { send({ kind:'console.error', args: collect(arguments) }); } catch(e){}
if (origErr) { origErr.apply(console, arguments); }
};
var origWarn = console.warn;
console.warn = function(){
try { send({ kind:'console.warn', args: collect(arguments) }); } catch(e){}
if (origWarn) { origWarn.apply(console, arguments); }
};
\(verboseHooks)
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
});
});
})();
"""
}
/// Debug 专用:`console.log` / `console.info` 全量转发。
///
/// 注意 H5 自带调试总开关 `Game_Config.Debugger.isDebugger`
/// `gamehall.zip` → `js/01_SubGame/00_SubGame_Config.js`)出厂为 `false`
/// 收发包日志(`发送数据:` / `接收数据:`)被它挡住。要看这些需在
/// Safari Web Inspector 控制台运行时打开:`Game_Config.Debugger.isDebugger = true`
/// —— 运行时改内存属性,不动 H5 任何文件(契约原则 A)。
private static let verboseConsoleHookJS = """
var origLog = console.log;
console.log = function(){
try { send({ kind:'console.log', args: collect(arguments) }); } catch(e){}
if (origLog) { origLog.apply(console, arguments); }
};
var origInfo = console.info;
console.info = function(){
try { send({ kind:'console.info', args: collect(arguments) }); } catch(e){}
if (origInfo) { origInfo.apply(console, arguments); }
};
"""
}
// 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)
}
}