From a1c5883cc5dbb4b1ecec30f71f633778a6961d59 Mon Sep 17 00:00:00 2001 From: joywayer Date: Tue, 23 Jun 2026 02:04:46 +0800 Subject: [PATCH] =?UTF-8?q?Phase=209.2=EF=BC=9AH5ErrorRelay=20=E6=8A=8A=20?= =?UTF-8?q?H5=20=E5=BC=82=E5=B8=B8=E4=B8=AD=E7=BB=A7=E5=88=B0=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=20print?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 零修改 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 --- docs/Development-Plan.md | 4 +- ylgamehall/Source/Bridge/H5ErrorRelay.swift | 122 ++++++++++++++++++ .../Source/WebView/BridgedWebView.swift | 5 + 3 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 ylgamehall/Source/Bridge/H5ErrorRelay.swift diff --git a/docs/Development-Plan.md b/docs/Development-Plan.md index a59cb47..c01e4eb 100644 --- a/docs/Development-Plan.md +++ b/docs/Development-Plan.md @@ -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` 桥 handler(Design §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.h5error,BridgedWebView 安装,Overlay 不挂) - [ ] 9.3 极光保持 NoopAnalytics(JANALYTICS_ENABLED OFF) - [ ] 9.4 闲聊保持 NoopSharePlatform - [ ] 9.5 Agora 保持 NoopVideoRoom(AGORA_ENABLED OFF) diff --git a/ylgamehall/Source/Bridge/H5ErrorRelay.swift b/ylgamehall/Source/Bridge/H5ErrorRelay.swift new file mode 100644 index 0000000..0624363 --- /dev/null +++ b/ylgamehall/Source/Bridge/H5ErrorRelay.swift @@ -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