接口 200 但页面白屏?Android WebView IntersectionObserver polyfill 排查实录

活动 H5 在部分 Android 设备上点击按钮后白屏,抓包却显示接口全部 200——这类问题网络层给不出答案,得看 JavaScript 运行时。下文按「抓包 → Console → Mock」的顺序,记录一次完整排查。


现象:能进页、能点按钮,随后白屏

压测群里发来录屏:

「三星 S23,Android 14,微信里面打开活动页,点击『立即参与』之后页面白屏。」

不是整页打不开。首页正常、按钮可点、点击后有 loading,随后整页变白。iOS 和其他 Android 机型均正常。

第一反应通常会拆三条线:

  1. JS 运行时是否在交互后抛错、中断渲染;
  2. 接口数据是否触发了前端未处理的边界;
  3. WebView 内核是否存在机型相关的 API 兼容差异。

先从最容易证伪的网络层下手。

第一步:抓包,排除「接口挂了」

手机配置 HTTPS 代理(Charles、mitmproxy 或同类工具均可),安装并信任根证书,对目标域名开启 SSL 解密。

在活动页点击「立即参与」,找到:

POST /api/campaign/join

响应:

200 OK

Body 结构与文档一致,业务字段无异常。

抓包列表:HTTPS 已解密,可按 URL / 方法筛选

结论:网络层正常,白屏是前端执行链路在某一步断掉。 没有运行时日志的话,只能对着 200 瞎猜——是渲染逻辑 bug?数据结构踩坑?还是某个 Web API 在特定内核上炸了?

第二步:为什么 vConsole 和 chrome://inspect 都不好使

vConsole

活动页已上线,测试包没法随时重打注入 vConsole。即便能注入,也得先知道「点按钮之后才崩」——错误若只在交互瞬间出现,日志容易被后续路由跳转冲掉。

Chrome Remote Debug

chrome://inspect → 选中 WebView → Inspect

能连上,但有两个硬伤:

  • 复现窗口短:问题出在点击之后;一刷新,错误栈消失,得反复操作才能撞上。
  • 环境门槛:WebView 需开 setWebContentsDebuggingEnabled、USB 调试、对应内核支持——线上包、微信内置 WebView 经常凑不齐。

需要的是:不改编译产物、不依赖 USB,在异常发生的当下拿到 Console 输出。

第三步:代理注入,拿到 WebView 运行时 Console

思路是走代理链路,向 HTML 响应里注入一段调试 bootstrap,页面加载后在真机上下文里挂 Console 钩子,日志回传到电脑端面板——类似 Eruda / vConsole,但不用改 H5 构建,线上 URL 直接可用。

前置步骤:

  1. 手机 Wi‑Fi 代理指向抓包工具;
  2. 安装并信任 HTTPS 根证书;
  3. 对活动域名开启 SSL 解密;
  4. 打开调试面板,等设备连上后复现「点击 → 白屏」。

(笔者这次用的是 DevPeek 的代理注入调试;Charles + 自写脚本、mitmproxy addon 也能做同类注入,核心都是「代理改写 HTML + 回传 Console」。)

远程调试:实机镜像与 Console / Network 同屏

复现后,Console 立刻出现:

Uncaught TypeError:
e.isIntersecting is not a function

at IntersectionObserver polyfill
(chunk-vendors.js:1847)

Console 报错栈 + 远程 eval

栈指向 IntersectionObserver 的 polyfill,而不是业务组件——方向明确了。

根因:旧 polyfill 覆盖了 Android 14 的原生实现

IntersectionObserver 用于监听元素是否进入视口,懒加载、曝光埋点、无限滚动列表都常见。

Android 14 系统 WebView(Chrome 108+)已原生支持该 API。 但项目打包时仍无条件引入了旧版 intersection-observer polyfill,且在初始化时覆写了 window.IntersectionObserver

旧 polyfill 的回调参数结构与原生不一致:原生 IntersectionObserverEntryisIntersecting 是 boolean;旧 polyfill 部分版本用不同字段或省略该 getter。业务代码(或依赖库)按原生写法访问 entry.isIntersecting,在 polyfill 路径下就变成 undefined is not a function 一类错误,后续渲染中断 → 白屏。

在 Console 里远程执行几行即可确认:

// 1. 确认 Android 版本
navigator.userAgent.match(/Android\s([\d.]+)/)?.[1];
// → "14"

// 2. API「存在」,但是否为原生?
typeof IntersectionObserver;
// → "function"

IntersectionObserver.toString().includes('native code');
// → false   ← 关键:跑的是 polyfill,不是浏览器实现

再对照 Chrome 版本(WebView 与系统 Chrome 同源):

navigator.userAgent.match(/Chrome\/([\d.]+)/)?.[1];
// Android 14 通常 ≥ 108,IntersectionObserver 已内置

定位完成:不是接口、不是业务数据结构,是「不该加载的 polyfill 在真机上覆盖了原生 API」。

修复方向

原则:只在缺失时加载,禁止无脑覆盖。

// ✅ 推荐:feature detect,不覆盖已有原生实现
if (!('IntersectionObserver' in window)) {
  await import('intersection-observer');
}

若历史包已全量引入,可改为按 UA / Chrome 版本条件加载,或在入口显式检测:

const hasNativeIO =
  'IntersectionObserver' in window &&
  IntersectionObserver.toString().includes('native code');

if (!hasNativeIO) {
  await import('intersection-observer');
}

打包侧检查 babel-preset-env / @vitejs/plugin-legacy 是否把 IntersectionObserver 误判为需 polyfill——Android 10+ WebView 多数场景已不需要。

第四步:Mock 接口,验证降级 UI

polyfill 修复进 PR 的同时,压测还得继续。测试要确认:接口返回业务错误码时,页面是否正常降级,而不是再次白屏。

典型场景:

场景 期望 UI
活动已结束 提示文案,可返回
无参与权限 权限说明
库存不足 售罄提示

每次都找后端改数据或发预发不现实,在代理层 Mock 响应是最快的。

在抓包列表找到 POST /api/campaign/join,配置 Mock 规则(Charles 用 Map Local / Rewrite,mitmproxy 用 flow.response 脚本,逻辑相同):

匹配:

  • URL 含 campaign/join
  • Method:POST

响应体改为:

{
  "code": 10086,
  "message": "活动已结束"
}

HTTP 状态码保持 200(与真实接口一致,只改业务 body)。

Mock 规则:匹配条件 + 响应编辑

保存启用后,测试机重新点击。页面未白屏,正常展示「活动已结束」——降级逻辑 OK。

临时验证也可用「响应拦截改包」:请求发出后、到达页面前改写 body,改完放行:

响应阶段 Mock 篡改

继续换错误码即可批量验 UI,例如:

{ "code": 10010, "message": "当前活动不可用" }

全程无需:改后端、等发布、重打 H5 包。联调结束记得关闭或删除 Mock 规则,避免污染后续抓包。

排查链路小结

用户点击「立即参与」
        ↓
抓包:POST /api/campaign/join → 200,body 正常
        ↓
Console:TypeError: e.isIntersecting is not a function
        ↓
eval:IntersectionObserver 非 native → polyfill 覆盖
        ↓
修复:条件加载 / 移除多余 polyfill
        ↓
Mock:code 10086 / 10010 … → 验证降级 UI

未重打包、未开 USB 调试、未改线上包。

把 Network 和 Console 放同一时间轴

这类问题常卡在「工具切换」:

  • 抓包工具里看到 200;
  • vConsole 里看到报错;
  • Mock 改完不确定手机有没有打到新响应。

若 Console、Network 与抓包列表同源,可以在报错的时间点附近直接点开对应请求,对照响应 body:

调试 Network:选中请求查看响应详情

「接口 200 返回了什么」和「同一时刻 Console 报了什么」放在一条时间线上,比三个 App 来回切省很多时间。

局限(代理调试通用)

1. 主线程已死

无限循环、栈溢出、严重阻塞时,注入脚本可能也跑不起来,先从静态资源加载、首屏请求查起。

2. iframe 隔离

Console 默认在当前 frame。问题在 iframe 内时,需对 iframe URL 单独开调试会话。

3. Mock 走代理

Service Worker 缓存命中、App 原生网络层(不走系统代理)、部分 WebView 特殊通道可能绕过 Mock。改完包后看抓包列表里是否出现预期请求。

4. 远程 eval 别乱来

与页面共享上下文,适合 typeoftoString 这类探测;避免长时间同步循环或大量 IndexedDB 遍历。

总结

现象 根因 手段
点击后白屏,接口 200 旧 polyfill 覆盖原生 IntersectionObserverisIntersecting 访问失败 代理注入 Console + 远程 eval
需验多种错误码 UI 后端不便配合 代理 Mock 响应 body

Takeaway: 「接口 200」只说明 HTTP 层没问题;WebView 白屏优先查 JS 运行时异常,机型相关的 polyfill 误覆盖是高频坑。Android 10+ 部署前建议对关键 Web API 做一次 fn.toString().includes('native code') 抽检。


文中截图来自 DevPeek;Charles、mitmproxy 等工具在抓包与 Mock 环节可平替,差异主要在是否自带远程 Console 注入。若你常做微信 / WebView 联调,把「抓包 + 运行时日志 + Mock」串成一条链路,比单点工具省不少来回。

posted @ 2026-08-02 10:10  GYPengDev  阅读(2)  评论(0)    收藏  举报