补齐非 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:
+26
-8
@@ -238,17 +238,34 @@ Safari Web Inspector 是开发期主力,但有它够不到的场景:
|
||||
|
||||
`Source/Bridge/H5ErrorRelay.swift` 用 `WKUserScript(.atDocumentStart)` hook 住 console 与全局异常,通过 `webkit.messageHandlers.h5error` 转发到原生。**不改 H5 一行**(原则 A)。
|
||||
|
||||
| JS 侧 | Xcode 输出前缀 | 落 `Documents/diag.log` | 构建配置 |
|
||||
前缀里的 `lobby` / `subGame` 是 `BridgedWebView(label:)`,用来区分同时存在的多个 WebView——也用来反推该在 Safari「开发」菜单里选哪个条目。
|
||||
|
||||
| 来源 | Xcode 输出前缀 | 落 `Documents/diag.log` | 构建配置 |
|
||||
|-------|---------------|------------------------|---------|
|
||||
| `console.log` | `[H5 console.log]` | ❌ | **仅 Debug** |
|
||||
| `console.info` | `[H5 console.info]` | ❌ | **仅 Debug** |
|
||||
| `console.warn` | `[H5 console.warn]` | ✅ | Debug + Release |
|
||||
| `console.error` | `[H5 console.error]` | ✅ | Debug + Release |
|
||||
| `window.onerror` | `[H5 onerror]` | ✅ | Debug + Release |
|
||||
| `unhandledrejection` | `[H5 unhandledrejection]` | ✅ | Debug + Release |
|
||||
| `console.log` | `[H5:lobby console.log]` | ❌ | **仅 Debug** |
|
||||
| `console.info` | `[H5:lobby console.info]` | ❌ | **仅 Debug** |
|
||||
| `console.warn` | `[H5:lobby console.warn]` | ✅ | Debug + Release |
|
||||
| `console.error` | `[H5:lobby console.error]` | ✅ | Debug + Release |
|
||||
| 未捕获 JS 异常 | `[H5:lobby onerror]` | ✅ | Debug + Release |
|
||||
| 未处理的 Promise rejection | `[H5:lobby unhandledrejection]` | ✅ | Debug + Release |
|
||||
| 资源 404(`<script>`/`<img>`/`<audio>`) | `[H5:lobby resourceerror]` | ✅ | Debug + Release |
|
||||
| WebView 首次/提交后加载失败 | `[WebView:lobby] …加载失败` | ✅ | Debug + Release |
|
||||
| WebContent 进程终止(白屏) | `[WebView:lobby] ⚠️ WebContent 进程终止` | ✅ | Debug + Release |
|
||||
|
||||
设计要点:
|
||||
### 8.2 仍然抓不到的(已知盲区)
|
||||
|
||||
| 场景 | 为什么抓不到 |
|
||||
|------|------------|
|
||||
| 被 H5 自己 `try/catch` 吞掉的异常 | 根本不冒到 window。本包里 `12_Logic.js:254` / `gameabc.min.js:2175` 就是 catch 后用 `console.log(e.stack)` 打印 → 只在 Debug 的 `[H5:… console.log]` 里能看到 |
|
||||
| iframe 内的异常 | `WKUserScript(forMainFrameOnly: true)`,只 hook 主框架 |
|
||||
| 弹层(`OverlayViewController`)的 JS | 第三方外链,按设计不挂 relay |
|
||||
| 跨域脚本的异常细节 | 浏览器安全策略统一报 `Script error.`,无行号无堆栈 |
|
||||
| 原生 handler 内部抛错 | 属原生侧,走 `BridgeBus` 自己的 `diagLog` |
|
||||
|
||||
### 8.3 设计要点
|
||||
|
||||
- **资源 404 必须开捕获阶段**:`<script>`/`<img>`/`<audio>` 的 error 事件只在元素自身触发且**不冒泡**,`addEventListener('error', fn)` 收不到,必须 `addEventListener('error', fn, true)`。JS 异常的 `ev.target` 是 `window`,据此在同一 listener 内分流两类
|
||||
- **导航层失败只 log 不自动 reload**:自动恢复是行为变化,msext 没有,不引入
|
||||
- **`log` / `info` 只在 Debug 注入**:Release 包不该为每条业务日志付一次 JS→Native IPC,也不该把 H5 内部输出暴露给外部审阅。`#if DEBUG` 在编译期就把这段 JS 从 `javaScriptSource` 里摘掉
|
||||
- **`log` / `info` 走裸 `print` 而非 `diagLog`**:`diagLog` 会同步落盘到 `Documents/diag.log`,而该 sink 无轮转、无大小上限(`BridgeBus.swift:47`)。一旦 §7 Q3 里的 `isDebugger` 被打开,收发包 firehose 灌进去会把设备磁盘吃光
|
||||
- **对象参数走 `JSON.stringify`**:大厅业务大量使用 `console.log(msg)` / `console.log(res)` 打整包,直接 `String(obj)` 只会得到无用的 `[object Object]`。循环引用时 stringify 抛错 → 回退 `String()` → 再抛则输出 `[unstringifiable]`
|
||||
@@ -265,3 +282,4 @@ Safari Web Inspector 是开发期主力,但有它够不到的场景:
|
||||
|------|------|
|
||||
| 2026-06-24 | 文档建立。落地 `BridgedWebView` / `OverlayViewController` 两处 `isInspectable` 补丁。 |
|
||||
| 2026-08-08 | 查明「Console 空」的首要原因是 H5 自带 `Game_Config.Debugger.isDebugger = false`,补进 §7 Q3。`H5ErrorRelay` 扩展 `console.log` / `console.info` 转发(Debug only)+ 对象参数 JSON 序列化,新增 §8.1。 |
|
||||
| 2026-08-08 | 补齐「非 H5 主动 console.error」的报错通路:资源 404(error 事件捕获阶段)、WebView 加载失败 / WebContent 进程终止(两个 VC 的 `WKNavigationDelegate`,此前完全没实现)。输出加 `lobby` / `subGame` 前缀。新增 §8.2 已知盲区、§8.3 设计要点。 |
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ public final class BridgedWebView: UIView {
|
||||
// ── H5 错误中继(Phase 9.2,原则 A 零修改 H5)─────────────
|
||||
// 把 console.error / window.onerror / unhandledrejection 通过独立
|
||||
// 的 webkit.messageHandlers.h5error 桥到原生 print,开发期减少盲点。
|
||||
H5ErrorRelay.shared.install(into: configuration.userContentController)
|
||||
H5ErrorRelay.shared.install(into: configuration.userContentController, label: label)
|
||||
|
||||
// ── 创建 WKWebView + BridgeBus ─────────────────────────
|
||||
let webView = WKWebView(frame: .zero, configuration: configuration)
|
||||
|
||||
@@ -412,5 +412,26 @@ extension SubGameViewController: WKNavigationDelegate {
|
||||
self?.splash.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
// ── 加载失败 / WebContent 进程崩溃:仅记录,不改行为 ──────────────
|
||||
// 说明同 WebContainerViewController 对应实现。子游戏更容易踩到:zip 下载不全 /
|
||||
// 解压残缺 / gameStart 目录名对不上时都会走 didFailProvisionalNavigation。
|
||||
public func webView(_ webView: WKWebView,
|
||||
didFailProvisionalNavigation navigation: WKNavigation!,
|
||||
withError error: Error) {
|
||||
let ns = error as NSError
|
||||
diagLog("[WebView:subGame] 首次加载失败 code=\(ns.code) \(ns.localizedDescription)")
|
||||
}
|
||||
|
||||
public func webView(_ webView: WKWebView,
|
||||
didFail navigation: WKNavigation!,
|
||||
withError error: Error) {
|
||||
let ns = error as NSError
|
||||
diagLog("[WebView:subGame] 提交后加载失败 code=\(ns.code) \(ns.localizedDescription)")
|
||||
}
|
||||
|
||||
public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
||||
diagLog("[WebView:subGame] ⚠️ WebContent 进程终止(多为内存不足),页面已白屏")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -760,5 +760,30 @@ extension WebContainerViewController: WKNavigationDelegate {
|
||||
self?.splash.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
// ── 加载失败 / WebContent 进程崩溃:仅记录,不改行为 ──────────────
|
||||
// 此前这三个回调都没实现:H5 加载不出来(AppSchemeHandler 找不到 index.html、
|
||||
// zip 解压残缺)时 splash 永不淡出、卡在启动图,Xcode 里一行日志都没有;
|
||||
// WebContent 进程被系统回收(canvas 游戏 JSC OOM)时页面直接白屏且同样静默。
|
||||
// 这些都不是 JS 异常,H5ErrorRelay 抓不到,只能在原生导航层观察。
|
||||
// ⚠️ 只 log 不自动 reload —— 自动恢复是行为变化,msext 没有,不引入。
|
||||
public func webView(_ webView: WKWebView,
|
||||
didFailProvisionalNavigation navigation: WKNavigation!,
|
||||
withError error: Error) {
|
||||
let ns = error as NSError
|
||||
// -999 = NSURLErrorCancelled,多为后续导航覆盖前一次,属正常噪声
|
||||
diagLog("[WebView:lobby] 首次加载失败 code=\(ns.code) \(ns.localizedDescription)")
|
||||
}
|
||||
|
||||
public func webView(_ webView: WKWebView,
|
||||
didFail navigation: WKNavigation!,
|
||||
withError error: Error) {
|
||||
let ns = error as NSError
|
||||
diagLog("[WebView:lobby] 提交后加载失败 code=\(ns.code) \(ns.localizedDescription)")
|
||||
}
|
||||
|
||||
public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
||||
diagLog("[WebView:lobby] ⚠️ WebContent 进程终止(多为内存不足),页面已白屏")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user