Author:长亭,直播技术团队
B站:https://space.bilibili.com/524735717
当前,开发 Hybrid 页面时,传统前端开发方式往往是在本地开发,通过浏览器如 Chrome 中响应式模式模拟移动设备来调试 Hybrid 页面。这种方式的问题在于:
兼容性:浏览器响应式模式通常只是修改了 UA 和模拟了移动设备视图呈现方式,但其与手机 Webview 的兼容性表现往往是不同的,因此可能出现各种 PC 浏览器中无法复现的兼容性问题,而开发人员在不借助工具的情况下无法调试页面,较难定位问题。
Native 交互:Hybrid 页面经常伴随着通过 JSBridge 与 Native 交互,PC 浏览器上即使可以提前通过 Console 等打印出交互内容,但在发布之前都无法确认交互的具体效果。
代理 Host:当开发 Hybrid 页面时,如果希望 APP 内的 Hybrid 页面可以访问测试环境,有两种方法:APP 将 APP 环境染色为测试环境,并在 Hybrid 页面请求时拦截并转发到测试环境;或者通过 Charles 等工具将手机网络请求代理到本地,走本地 Host 发起请求。前者需要 APP 接入,成本较高且可能对线上产生影响。后者学习成本相对较高,需要安装证书、配置一系列代理等操作。
本地开发项目调试:在上述方法中,想要在 APP 上访问本地开发项目,有两种方式:将本地项目发布到静态资源服务机器上;或者通过 Charles 等代理工具的 Map Local、Map Remote 等将 Hybrid 页面请求映射到本地开发项目资源。后者时间成本相对较低,但同样要求开发人员熟练使用 Chrales 等工具配置代理,学习成本相对较高。
当前较为成熟的真机调试工具主要有微信的开发者工具,能够实现从构建、调试、发布较为完善的 Work Flow。并支持在 PC 客户端上调试远程真机上的小程序,进行元素审查、Console 等操作,其基本类同于 Chrome 开发者工具的调试行为习惯,并能够将本地项目打包上传后打开小程序调试本地开发的项目,但目前暂不支持 HMR 热模块重载。
由于微信开发者工具是针对微信小程序等具有特定语法的应用开发而设计的,因此并不能够作为常规 Hybrid 页面调试工具的解决方案。
相对而言,谷歌远程调试协议 Chrome Devtools Protocol(以下简称 CDP,谷歌开发者工具协议)作为 Chrome 内置的开源调试协议,配合 Google 开源的 Chrome Devtools Frontend 调试前端(以下简称 CDF 也就是 Chrome F12 开发者工具,本质上是一个前端页面,由 Chrome 内嵌用于调试 Web 页面),可以实现跨端的基于 WebSocket 协议的 Webview 调试流程。并且鉴于 Chrome Devtools Frontend 对于前端开发人员的熟悉度与使用便利度,前端开发人员用起来会更加得心应手。
调试工具 Work Flow:

真机远程调试
远程调试的路程基本如下:
Native 打开 Webview 后与 Webview 内核建立 WebSocket 调试通道。
通道建立后,Native 再与前端 Node 中间层建立 WebSocket 通道,用于转发 Webview 内核发出的页面调试数据。
PC 客户端与 Node Server 中间层建立 WebSocket 通道,用于接收 Webview 内核发出的页面事件调试数据,以及转发从 CDF 发出的本地调试请求数据。
PC 客户端在本地建立 Websocket Server 服务器,供 CDF 调试页面连接,接收 CDF 发出的调试请求数据。
【App 扫码后 PC 客户端与手机 WebView 建立链接,调试效果预览】

【手机端 App 建立链接,WebView 调试效果展示】

本地资源真机预览
本地资源真机调试,有两种思路:
打包上传(撰写本文时,作者暂时使用的方式):每次开发改动时将本地静态资源打包上传到公网的 Node 静态资源服务供 APP 访问,页面手动需刷新
HMR(推荐):在本地开发模式下,实现热重载,通过 Node 中间层将 Webview 静态资源请求通过 WebSocket 转发映射到本地 Dev 开发中的静态资源,由本地客户端映射到对应的开发资源。同时在基于 Webpack DevServer 开发时,将开发页面的 DevServer 的用于 HMR 的 WebSocket 重定向到 Node 中间层,以转发、接收 Node 从本地 DevServer 获取到的 HMR 调试数据
第二种方式相对复杂,但基本可以实现 PC 上同样效果的 HMR 效果,不需要像第一点那样刷新页面。
无代理本地 Host 化,指不通过 Charles 等代理工具,或其他通过劫持等方式实现访问本地 PC Host 环境, 实现 Hybrid 页面的实际网络请求实际访问的是本地 Host 配置指向的地址,如本地通过 SwitchHosts 配置的测试环境 Host,以实现调试页面访问测试环境接口资源。
该功能主要基于 Service Worker 实现:
加载 Service Worker:在本地资源打包过程中,在打包后的 HTML 资源中注入 Service Worker 加载脚本,并将 Service Worker 脚本放入打包后的根目录下,就可以实现通过 ./ 路径加载 Service Worker 脚本。
请求转发:Service Worker 监听 Fetch 事件,将 Request 请求转发到 Node 中间层,Node 中间层再转发至本地客户端,由客户端发送请求,并将请求结果原路转发至 Service Worker,从而实现请求的本地 Host 化。
CDP 是 Google 针对 Chromium、Chrome 以及其他基于 Blink 内核的浏览器或 Webview 调试的一整套协议,也是 CDF 直接适配使用的协议。
在 PC 上,当我们以:
命令启动 Chrome 浏览器时,在浏览器中访问:
可以看到该 Chrome 实例打开的所有页面及其 WebSocket 调试地址,访问:
接口通过 HTTP 访问:

上述的 webSocketDebuggerUrl 字段的值是 WebSocket 通道,需要通过 WebSocket 客户端连接之后才可以发送 CDP 定义的调试信息进行调试。
对于 Android Webview,Webview 内核同样会在本地 127.0.0.1 开启这些调试服务。但不同于 PC 上的是 Webview 开启的 Socket 是基于 Unix Domain Socket(基于文件系统的 IPC 进程间通信的通道),该 Socket 通道无法通过网络访问,无法直接在手机浏览器中打开,只能通过 Adb Forward 转发到 PC,或者 Native 通过 LocalSocket 库与该 Unix Domain Socket 通信。
具体 CDP 协议及 Android Webview、Unix Domain Socket 相关见参考文档链接。
在不通过 USB 数据线连接 PC 的情况下,要实现与 Webview 的 Unix Domain Socket 的通信,需要 Native 使用 LocalSocket 库连接:
LocalSocket 向该 Unix Domain Socket 建立连接,之后通过该 Socket 套接字发送 HTTP 报文至 /path/to/json 路径获取页面调试的 JSON 信息(主要是 webSocketDebuggerUrl 字段)。
LocalSocket 再通过该 Socket 套接字向获取到的 WS 调试地址 webSocketDebuggerUrl 发送 HTTP 请求报文升级通信协议为 WebSocket 协议。
升级为 WebSocket 协议后,Native 直接通过 localSocket 发送 WebSocket 报文即可(需要符合 WebSocket 报文格式)。
Service 加载生效之后,通过监听 Fetch 事件,可以拦截到 Webview 页面上的 AJAX、Fetch 请求,然后可以进行指定域名拦截:
对于不需要拦截的域名,如静态资源,直接走默认的页面请求途径。
对于拦截请求,GET、HEAD 请求直接转发即可;但对于 POST、PUT 等带有请求 Body 的请求,需要将 Body 放入新的 Fetch Request 的 Body 中。

Node 中间层转发 Request
Node 转发请求的核心点在于:Node 服务无法访问 PC 本地网络,因此需要将请求转为 JSON后,再通过 PC 客户端主动与 Node 建立的 WebSocket 通道将请求转发给 PC 客户端,再由 PC 客户端发起请求并返回响应结果。
流程如下:
Node 接收到 Service Worker 发送来的转发请求 Request 后,将请求转为 JSON,通过 WebSocket 转发给 PC 客户端。
PC 客户端发起请求,并将请求响应 Response 转为 JSON,通过 WebSocket 转给 Node,Node 解析 JSON,将解析结果响应给 Service Worker。

在 Native 与 Unix Domain Socket 通信过程中,WebSocket 报文需要严格遵守 WebSocket 报文格式:
WebSocket 通信两端,客户端向服务端发送 WebSocket 报文时,需要通过掩码加密算法加密 Payload 数据。具体体现在:报文的第八位 MASK 需要为 1,1 代表报文经过了掩码加密,并且报文会携带 4 个字节的 Masking-key 掩码。如果 MASK 为 0,则没有 Masking-key,比有掩码加密时少 4 个字节。
Native 发往 Webview 内核的报文由 Native 将 Payload 数据等拼接成 WebSocket 报文。

Chrome Devtools,即 Chrome 开发者工具,是内置于 Google Chrome 的一套 Web 调试工具,可以对 Web 页面进行调试、性能分析。
官方文档:Chrome Devtools
Chrome Devtools 主要包括四个部分:
Frontend:调试器前端。
Backend:调试器后端。
Protocol:CDP,调试协议。
Message Channels:消息通道,包括 Embedder Channel、WebSocket Channel、Chrome Extensions Channel、USB/ADB Channel。
其中,Frontend 也就是 Chrome 浏览器 F12 打开的对开发者可见的调试器 UI 页面,本质上是一个前端 Web 页面。可以作为独立的前端项目,从 Chromium 中剥离并单独开发、构建。
Frontend 由于采用 Google 自身的模块解析与打包机制,需要使用 Google 的 depot_tools 工具进行开发。
首先,安装 depot_tools 工具:Get depot_tools first。
拉取 Frontend 项目:

Build 构建:

构建完成后,将构建后的 resources/inspector 在 Chromium 中运行:

[1] Chrome Devtools Protocol: https://chromedevtools.github.io/devtools-protocol/ [2] Chrome Devtools Frontend: https://www.npmjs.com/package/chrome-devtools-frontend/v/1.0.708769 [3] Chrome Devtools Frontend WorkFlow: https://chromium.googlesource.com/devtools/devtools-frontend/+/HEAD/docs/workflows.md [4] Unix Domain Socket: http://docs.linuxtone.org/ebooks/C&CPP/c/ch37s04.html [5] Google depot_tools: https://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/depot_tools_tutorial.html [6] WebSocket 协议: https://www.cnblogs.com/chyingp/p/websocket-deep-in.html [7] ATX 利用 Chrome Devtools 进行 Webview 的测试: https://testerhome.com/topics/19461 [8] Chrome 远程调试: https://mabin004.github.io/2018/09/01/%E8%B0%83%E8%AF%95%E7%A0%94%E7%A9%B6/