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:
joywayer
2026-06-23 02:04:46 +08:00
co-authored by Claude Opus 4.7
parent 5885f97715
commit a1c5883cc5
3 changed files with 129 additions and 2 deletions
+2 -2
View File
@@ -715,7 +715,7 @@ H5 调 `OpenurlTitleData` 打开弹层 WebView,弹层内 H5 用 `window.settin
- [ ] **9.1** Sentry-Cocoa SPM 依赖 + `Source/Analytics/SentryCrashReporter.swift`
- 编译开关 `SENTRY_ENABLED`(默认 ON+ `NoopCrashReporter` fallback
- DSN 通过 xcconfig 注入,不入 git
- [ ] **9.2** `Source/Bridge/Handlers/H5ErrorRelay.swift`注入 `window.onerror` / `unhandledrejection` polyfill + `reportH5Error` 桥 handlerDesign §11.4,新增 handler 不破坏契约)
- [x] **9.2** `Source/Bridge/H5ErrorRelay.swift`WKUserScript .atDocumentStart 注入 hook,拦截 `console.error` / `window.onerror` / `unhandledrejection`,通过独立的 `webkit.messageHandlers.h5error` 桥(与 WVJB 不同 channel)转发到原生 print。仅 BridgedWebView 安装(大厅+子游戏),Overlay 第三方外链不挂。零修改 H5。
- [ ] **9.3** 极光(JAnalytics):保持 `NoopAnalytics` stub**ADR-005 决策**
- `Source/Analytics/Tracker.swift` 协议 + `NoopAnalytics` 实现
- `JAnalyticsTracker.swift` 留蓝图骨架(`#if JANALYTICS_ENABLED` 包裹,默认 OFF
@@ -972,7 +972,7 @@ H5 调 `OpenurlTitleData` 打开弹层 WebView,弹层内 H5 用 `window.settin
### Phase 9 SDK 真实化 + 监控
- [ ] 9.1 Sentry SPM + CrashReporter
- [ ] 9.2 H5ErrorRelay
- [x] 9.2 H5ErrorRelay(独立 webkit.messageHandlers.h5errorBridgedWebView 安装,Overlay 不挂)
- [ ] 9.3 极光保持 NoopAnalyticsJANALYTICS_ENABLED OFF
- [ ] 9.4 闲聊保持 NoopSharePlatform
- [ ] 9.5 Agora 保持 NoopVideoRoomAGORA_ENABLED OFF
+122
View File
@@ -0,0 +1,122 @@
//
// H5ErrorRelay.swift
// ylgamehall
//
// H5 WebView `console.error` / `window.onerror` /
// `unhandledrejection` `webkit.messageHandlers.h5error` print
//
// Phase 9.2Design §11.2msext /
// 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/catchhook
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 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)
}
}
@@ -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,