排查「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 设计要点。
15 KiB
H5 调试指南(Safari Web Inspector)
定位:本文档面向开发者,描述开发期调试 H5(大厅 / 子游戏 / 弹层)网页输出的标准方法。
适用范围:仅 Debug 编译;Release 包不受影响(详见 §6 安全性说明)。
不在本文档范围内:
- 桥接接口契约 →
H5-Native-Contract.md- 业务功能验收清单 →
Verification-Checklist.md- 原生
os.log日志查看(直接看 Xcode 底部输出窗口即可)
1. 原理与适用范围
WebKit 自 iOS 16.4 起把 WKWebView 的网页检查器开关收敛到了 isInspectable 属性,默认关闭。打开后,该 WKWebView 加载的页面会被 Mac Safari 的"开发"菜单识别为可调试目标,等价于在浏览器里按 F12 打开 DevTools,能力包括:
- Console:
console.log/info/warn/error/trace全部可见;可在控制台中直接evaluateJS 表达式 - Sources:查看 H5 包内所有 .js / .html / .css 源码;下断点、单步、Watch 表达式
- Network:所有 XHR / fetch / 图片 / 字体请求,请求头/响应/Timing 全可见(含
file://本地资源) - Elements:DOM 树、计算后样式、动画检视
- Storage:localStorage / sessionStorage / Cookies / IndexedDB
- Timelines:CPU / 渲染 / 网络时间线
与 Xcode 控制台关系:Safari Web Inspector 看的是 H5 网页里的 console(JS 端运行结果);Xcode 控制台看的是原生 Swift print / os.log 输出。两者互补。
2. 工程内的代码改动(已落地,无需重做)
代码侧已经在两处 WKWebView 创建点加好了 #if DEBUG 守卫的 inspectable 开关:
ylgamehall/Source/WebView/BridgedWebView.swift:大厅 / 子游戏共用的WKWebViewylgamehall/Source/WebView/OverlayViewController.swift:弹层(msextthreeView等价容器)的WKWebView
补丁形态(两处一致):
let webView = WKWebView(frame: .zero, configuration: configuration)
#if DEBUG
if #available(iOS 16.4, *) {
webView.isInspectable = true
}
#endif
重要约束:
#if DEBUG守卫不可去除 —— Release 包绝对不能让生产 IPA 暴露 Web Inspector,避免被外部审阅 JS 源码 / Storage / 注入表达式@available(iOS 16.4, *)兼容守卫不可去除 —— 本项目最低 iOS 15.6,低于 16.4 的设备走else分支(即不开启 inspectable),新设备才走真路径- 新增任何第 4 个
WKWebView创建点时,同样补这 6 行,否则该 WebView 在 Safari 里看不到
3. 前置准备
3.1 Xcode 端
确认 Scheme Run 配置是 Debug(默认就是):
- 菜单 Product → Scheme → Edit Scheme...(
⌘<)→ 左侧 Run → 右侧 Info 标签页 - Build Configuration = Debug ✅
3.2 Mac Safari 端(一次性)
- Safari 顶部 → Safari → 设置(
⌘,)→ 高级 - 勾选最下方 "在菜单栏中显示开发菜单"
- 关闭设置窗口,菜单栏出现"开发"项
3.3 真机端(一次性,仅真机需要;模拟器跳过)
iPhone 上:设置 → Safari → 高级 → 网页检查器 = 开。
⚠️ 这个"网页检查器"开关与 Safari 浏览器自身的网页检查无关,是 iOS 18+ 起 Apple 把
WKWebView的可检视性也纳入了这个开关。如果不开,Mac Safari 的"开发"菜单里看不到该设备的 H5 页面。
4. 标准操作流程
4.1 启动 Debug build
- Xcode 顶部 device picker → 选择目标设备/模拟器
⌘R启动- 等大厅 H5 加载完成(屏幕上看到大厅 UI)
4.2 打开 Web Inspector
Mac Safari 顶部菜单 → 开发 → [设备名] → [页面标题或 URL] → Web Inspector 浮窗弹出。
- 模拟器:菜单层级类似
开发 → Simulator – iPhone 16 Pro → file:///.../index.html - 真机:
开发 → 你的 iPhone 名字 → file:///.../index.html
4.3 看 console 输出
Web Inspector 顶部 tab → Console → 实时显示 H5 端的 console.* 输出 + JS 异常堆栈。
支持直接在底部输入框运行表达式,例如:
window.WebViewJavascriptBridge // 查看桥对象
typeof app_gameid // 查看预注入变量
bridge.callHandler('getphoneInfo', null, r => console.log(r)) // 触发桥调用
4.4 关闭 Web Inspector
直接关掉浮窗即可,不影响 app 运行。下次需要时重新打开。
5. 多 WebView 场景
本项目同时存在最多 3 个 WKWebView 实例:
| 场景 | 创建位置 | 何时存在 |
|---|---|---|
| 大厅 | WebContainerViewController → BridgedWebView |
App 全程 |
| 子游戏 | SubGameViewController → BridgedWebView |
push 进子游戏后,pop 销毁 |
| 弹层 | OverlayViewController 自建 WKWebView |
present 显示中,dismiss 销毁 |
Safari "开发" 菜单会把每个已加载页面的 WKWebView 列为独立一项。
5.1 关键 gotcha:Develop 菜单不会自动刷新 ⭐
进入子游戏 / 打开弹层之后,已经展开过的"开发"菜单不会自动刷新,新出现的 WebView 不会冒出来。
做法:
- 子游戏 push / 弹层 present 完成后
- 回 Mac,先点其它菜单(如 文件、编辑),让"开发"菜单收起
- 再展开 开发 → [设备名] → 此时新 WebView 才会列出来
5.2 多条目区分
如果列表里多个条目 <title> 相同 / 都显示为 file://...index.html,分不清哪个是哪个:
- 鼠标 hover 每个条目 → iPhone/模拟器屏幕上对应的
WKWebView会高亮蓝色边框 - 用这个方法定位到目标 WebView,再点击进入
5.3 弹层 dismiss 后条目消失
弹层关闭后 OverlayViewController 释放,其 WKWebView 被回收,菜单条目随之消失。必须在弹层显示着的时候调试,关掉就来不及了。
6. 安全性 / Release 包说明
#if DEBUG守卫保证只有 Xcode 的 Debug 配置启用isInspectable = true- 渠道分发 / 企业签包都用 Release 配置 → 编译期就把 inspectable 那 5 行剔除 → 生产 IPA 没有任何"可检视性"信号
- 即便用户的 iPhone 开了"网页检查器"开关、Mac Safari 也开了"开发"菜单,Release 包的 WebView 依然不会出现在"开发"菜单
- 因此生产环境无需任何额外清理动作,提交前也不用手动关任何开关
7. 常见问题 FAQ
Q1:Mac Safari "开发"菜单里完全看不到我的设备
按概率排查:
- 菜单本身没开 → §3.2 把"在菜单栏中显示开发菜单"勾上
- 真机未开"网页检查器" → §3.3 设置 → Safari → 高级
- 真机未连线 / 未信任电脑 → 数据线重插、iPhone 上重新点"信任"
- 跑的不是 Debug build → §3.1 Scheme → Run → Build Configuration = Debug
- iOS 版本 < 16.4 → 不支持,换个新设备 / 模拟器
- Mac 与 iPhone 不在同一 Wi-Fi → 仅无线调试需要;有线优先
isInspectable补丁丢了 → grepisInspectable应该有 2 处命中,否则被回滚了
Q2:只看到大厅,子游戏 / 弹层看不到
→ §5.1 关闭"开发"菜单再重开。99% 是这个原因。
Q3:能看到条目但 Console 是空的
首要原因:H5 自带的调试总开关出厂是关的 ⭐
gamehall.zip → js/01_SubGame/00_SubGame_Config.js:1-12:
Game_Config.Debugger={
isDebugger : false, // debugger模式下会将所有收发的包输出到控制台(正式发布改为false)
...
整包 220 处 console.log,82 处已被 // 注释掉,剩下的高频日志全部挡在这个开关后面:
| 位置 | 内容 |
|---|---|
js/00_Surface/09_Net.js:47 |
发送数据:... |
js/00_Surface/12_Logic.js:210 |
接收数据:... |
js/00_Surface/12_Logic.js:463 / 1317 |
包体输出 |
没被挡的只剩 12_Logic.js:784/829/870/921 的 Connect:...,那几行在大厅启动建 websocket 时就打完了,等你挂上 Inspector 早已过去。
打开方式(不改 H5 一个字节,符合原则 A) —— 在 Web Inspector 控制台运行:
Game_Config.Debugger.isDebugger = true
这是运行时改内存里的对象属性,不碰 gamehall.zip、不碰任何文件;杀进程重启即恢复 false。
⚠️ 不要去改
Resources/gamehall.zip里的这个false—— 那是 H5 修改,违反原则 A,且会跟着渠道包发出去。
其它可能原因:
- Web Inspector 打开前的旧日志没被记录 → 在 Web Inspector 已经打开的状态下触发一次 H5 操作(点按钮、切 tab)再看
- Console 顶部过滤器把日志级别过滤掉了 → 确认 All Levels 都选中
- 连错 WebView(3 个实例 title 都是
gameabc)→ 敲typeof Game_Config,返回"undefined"说明不是大厅那个,按 §5.2 用 hover 高亮法重选 - H5 端确实没有
console.*调用(业务在沉默运行,没问题就不输出)
Q4:Sources 里看不到 .js 源码
- 检查 H5 zip 是否在沙盒里正常解压(
AppDataWriter.swift写入的app_*.js也应出现) - 如果某文件被打包成 minified / 不带 sourcemap,源码可读性差 —— 这是上游 H5 构建问题,原生侧无能为力
Q5:Network 标签里看到一堆奇怪的 wkbridge://__bridge_loaded__ 请求
这是 WebViewJavascriptBridge JS 端用 iframe + src 协议触发原生回调的机制,正常行为,不要去掉。详见 Resources/JS/WebViewJavascriptBridge.js。
Q6:Safari 提示 "Web Inspector cannot connect"
- 重连数据线 / 重启 Safari / 重启 Xcode
- iPhone 16+ 偶发协议握手失败,重启 iPhone 通常恢复
8. 何时本方法不够用
Safari Web Inspector 是开发期主力,但有它够不到的场景:
- 真机 + 用户已在生产环境:Release 包没有 inspectable;需要的是事后日志,参考原生侧的崩溃 / 错误上报(项目目前无 crash 监控,依赖用户反馈)
- 断网 / 弱网下的 H5 表现:用 Mac 上的 Network Link Conditioner,或模拟器 Features → Network Link Conditioner
- 不想开 Safari、只在 Xcode 控制台看 H5 输出:原生侧的
H5ErrorRelay(Source/Bridge/H5ErrorRelay.swift)已覆盖,见下面 §8.1 - 断点调试原生 Swift 与 H5 同时跑:Xcode + Safari Web Inspector 同时打开,两边各自下断点互不影响
8.1 H5 console → Xcode 控制台(H5ErrorRelay)
Source/Bridge/H5ErrorRelay.swift 用 WKUserScript(.atDocumentStart) hook 住 console 与全局异常,通过 webkit.messageHandlers.h5error 转发到原生。不改 H5 一行(原则 A)。
前缀里的 lobby / subGame 是 BridgedWebView(label:),用来区分同时存在的多个 WebView——也用来反推该在 Safari「开发」菜单里选哪个条目。
| 来源 | Xcode 输出前缀 | 落 Documents/diag.log |
构建配置 |
|---|---|---|---|
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] - 原始 console 调用被透传(
origLog.apply(console, arguments)),所以转发不影响 Safari Web Inspector 里照常看到日志,两条路可以同时用 - 仅
BridgedWebView(大厅 + 子游戏)注入,OverlayViewController是第三方外链,不挂
⚠️ 依然受 §7 Q3 的 Game_Config.Debugger.isDebugger 制约:收发包日志被 H5 自己的开关挡着,Xcode 控制台同样看不到。需要时在 Safari 控制台运行 Game_Config.Debugger.isDebugger = true。
9. 修订记录
| 日期 | 内容 |
|---|---|
| 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 设计要点。 |