Phase 9.2:H5ErrorRelay 把 H5 异常中继到原生 print
零修改 H5(原则 A):WKUserScript .atDocumentStart 在业务 JS 跑前 hook 三类异常源: - `console.error(...)`:保留原函数链式调用 + 转发 args - `window.onerror`:onmessage / filename / lineno / colno / error.stack - `unhandledrejection`:reason + reason.stack 走独立的 webkit.messageHandlers.h5error 通道(与 WVJB 不同 channel,不冲突)。 原生侧 MessageProxy weak target 切断 retain cycle。仅 BridgedWebView 安装 (大厅+子游戏),OverlayViewController 是第三方外链,不挂。 开发期价值:H5 出错时 Xcode console 直接能看到 "[H5 onerror] ... at xxx.js:42:8", 减少调试盲点。生产期 print 走 OSLog,对体积/性能影响极小。 新增: - Source/Bridge/H5ErrorRelay.swift(@MainActor singleton) 接入: - BridgedWebView.init:H5ErrorRelay.shared.install(into:) 在 WVJB 之后 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
5885f97715
commit
a1c5883cc5
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// 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 "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); }
|
||||
};
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,11 @@ public final class BridgedWebView: UIView {
|
||||
print("[BridgedWebView] ERROR: WebViewJavascriptBridge.js 未在 Bundle 找到")
|
||||
}
|
||||
|
||||
// ── H5 错误中继(Phase 9.2,原则 A 零修改 H5)─────────────
|
||||
// 把 console.error / window.onerror / unhandledrejection 通过独立
|
||||
// 的 webkit.messageHandlers.h5error 桥到原生 print,开发期减少盲点。
|
||||
H5ErrorRelay.shared.install(into: configuration.userContentController)
|
||||
|
||||
// ── 创建 WKWebView + BridgeBus ─────────────────────────
|
||||
let webView = WKWebView(frame: .zero, configuration: configuration)
|
||||
let bridge = BridgeBus(webView: webView,
|
||||
|
||||
Reference in New Issue
Block a user