补齐非 console.error 类 H5 报错的观察通路

排查「H5 有输出但 Safari 看不到」时发现,即便绕开 Safari 只看 Xcode,也有三类
故障是完全静默的:

1. 资源 404(<script>/<img>/<audio> 加载失败)
   error 事件只在元素自身触发且不冒泡,原先 addEventListener('error', fn) 收不到。
   改为捕获阶段 (capture=true),JS 异常的 ev.target 是 window,据此在同一 listener
   内分流 onerror / resourceerror 两类。缺图缺 js 是 H5 最常见的线上故障,此前零信号。

2. WebView 加载失败(didFailProvisionalNavigation / didFail)
   两个 VC 的 WKNavigationDelegate 此前只实现了 didFinish。AppSchemeHandler 找不到
   index.html、子游戏 zip 解压残缺时,splash 永不淡出、卡在启动图,Xcode 无任何输出。

3. WebContent 进程终止(webViewWebContentProcessDidTerminate)
   canvas 游戏 JSC OOM 被系统回收时页面白屏,同样静默。
   ⚠️ 只 log 不自动 reload —— 自动恢复是行为变化,msext 没有,不引入。

另:H5ErrorRelay.install 增加 label 参数,输出前缀从 [H5 xxx] 变为
[H5:lobby xxx] / [H5:subGame xxx]。同时存在多个 BridgedWebView 时原先分不清
日志来自谁,也就无法反推该在 Safari「开发」菜单里选哪个条目。

无契约影响:H5 可观察行为不变,新增回调仅记录不改流程。
Debug / Release 两个配置均已编译验证。
docs/H5-Debug-Guide.md §8.1 更新能力表,新增 §8.2 已知盲区 / §8.3 设计要点。
This commit is contained in:
joywayer
2026-08-08 14:19:58 +08:00
parent f4acccdd6c
commit 7fe089577b
5 changed files with 112 additions and 21 deletions
+39 -12
View File
@@ -32,19 +32,24 @@ public final class H5ErrorRelay: NSObject {
/// hook JS + message handler UserContentController
/// BridgedWebView.init WVJB
public func install(into controller: WKUserContentController) {
///
/// - Parameter label: WebView /
/// BridgedWebView
/// Safari
public func install(into controller: WKUserContentController, label: String) {
let userScript = WKUserScript(
source: Self.javaScriptSource,
injectionTime: .atDocumentStart, // JS hook
forMainFrameOnly: true // iframe
)
controller.addUserScript(userScript)
controller.add(MessageProxy(target: self), name: Self.messageName)
controller.add(MessageProxy(target: self, label: label), name: Self.messageName)
}
fileprivate func handle(_ body: Any) {
fileprivate func handle(_ body: Any, label: String) {
guard let dict = body as? [String: Any],
let kind = dict["kind"] as? String else { return }
let tag = "[H5:\(label)"
switch kind {
case "console.log", "console.info":
// Debug javaScriptSource #if DEBUG
@@ -53,28 +58,34 @@ public final class H5ErrorRelay: NSObject {
// `Documents/diag.log` BridgeBus.swift:47
// firehose Xcode console
let args = (dict["args"] as? [String]) ?? []
print("[H5 \(kind)] " + args.joined(separator: " "))
print("\(tag) \(kind)] " + args.joined(separator: " "))
case "console.error":
let args = (dict["args"] as? [String]) ?? []
diagLog("[H5 console.error] " + args.joined(separator: " "))
diagLog("\(tag) 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: " "))
diagLog("\(tag) 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)" } ?? ""))
diagLog("\(tag) onerror] \(msg) at \(src):\(line):\(col)" + (stack.map { "\n\($0)" } ?? ""))
case "resourceerror":
// 404 / H5 console.error
// / js 线
let tagName = (dict["tag"] as? String) ?? "?"
let src = (dict["src"] as? String) ?? ""
diagLog("\(tag) resourceerror] <\(tagName)> 加载失败:\(src)")
case "unhandledrejection":
let reason = (dict["reason"] as? String) ?? ""
let stack = dict["stack"] as? String
diagLog("[H5 unhandledrejection] " + reason + (stack.map { "\n\($0)" } ?? ""))
diagLog("\(tag) unhandledrejection] " + reason + (stack.map { "\n\($0)" } ?? ""))
default:
diagLog("[H5 unknown error] \(dict)")
diagLog("\(tag) unknown] \(dict)")
}
}
@@ -127,7 +138,21 @@ public final class H5ErrorRelay: NSObject {
if (origWarn) { origWarn.apply(console, arguments); }
};
\(verboseHooks)
// capture = true:资源加载失败(<script>/<img>/<audio> 的 404)只在元素自身
// 触发 error 且**不冒泡**,不开捕获阶段就完全收不到 —— 而这恰恰是 H5 最常见的
// 线上故障(缺图 / 缺 js)。JS 异常的 ev.target 是 window,据此分流两类。
// 注:listener 挂在 window 上,window 自己是 target 时走 AT_TARGET 阶段,
// 不受 capture 标记影响,所以一个 listener 同时收得到两类。
window.addEventListener('error', function(ev){
var t = ev.target;
if (t && t !== window && t.tagName) {
send({
kind: 'resourceerror',
tag: String(t.tagName),
src: String(t.src || t.href || '')
});
return;
}
send({
kind:'onerror',
msg: String(ev.message || ''),
@@ -136,7 +161,7 @@ public final class H5ErrorRelay: NSObject {
col: ev.colno || 0,
stack: (ev.error && ev.error.stack) ? String(ev.error.stack) : null
});
});
}, true);
window.addEventListener('unhandledrejection', function(ev){
var r = ev.reason;
send({
@@ -175,9 +200,11 @@ public final class H5ErrorRelay: NSObject {
@MainActor
private final class MessageProxy: NSObject, WKScriptMessageHandler {
private weak var target: H5ErrorRelay?
private let label: String
init(target: H5ErrorRelay) {
init(target: H5ErrorRelay, label: String) {
self.target = target
self.label = label
super.init()
}
@@ -185,6 +212,6 @@ private final class MessageProxy: NSObject, WKScriptMessageHandler {
didReceive message: WKScriptMessage) {
// WKScriptMessageHandler MessageProxy / H5ErrorRelay
// MainActor Task
target?.handle(message.body)
target?.handle(message.body, label: label)
}
}