返回博客首页
← 所有文章

从技术角度看 Chrome DevTools 集成

2017 年 7 月 11 日 — 作者 Peter Kanev

您可能已经听说,上周发布了 NativeScript 3.1,它的一个重要功能是为 NativeScript 的调试工具库添加了一个新成员,而这个成员原本是Chrome 的开发者工具提供给 Web 开发者的 - 元素面板。它显示了有效的视图树,页面当前的视图,以及它们的属性和计算样式。

dom-elements

NativeScript 和 Chrome 的开发者工具有什么共同点?

Chrome DevTools 不是只被 Web 开发者用来调试渲染的页面、进行实时编辑以查看即时结果等等吗?既对又错。由于 NativeScript 主要面向 Web 开发者,允许他们重复使用自己的技能并使用熟悉的工具,因此我们努力简化开发流程,使其尽可能接近实际情况。

I dont know what I am doing

为了实现这一点,我们开始在 NativeScript 运行时逐步实现 Chrome DevTools 协议。NativeScript 2.5 是第一个支持 DevTools 的版本,从那时起,我们一直致力于扩展 NativeScript 提供的调试机会。这些功能并不是无缝且毫不费力地实现的,所以今天我将分享一些关于我们在这一过程中遇到的挑战的技术细节。

provide all the features

底层协议如何运作?

首先,我想提一下,Chromium、Chrome、Node.js、NativeScript 和许多其他现有项目目前都在使用该协议。它允许工具检测、检查、调试和分析已实现该协议的应用程序。检测被划分为多个域(DOM、Debugger、Network 等)。

可以在Chrome DevTools 协议查看器页面上找到由 DevTools 协议定义的域的完整列表。一些复杂的 Chrome DevTools 功能需要实现多个域代理。例如,要完全支持“元素”面板中的所有功能,嵌入器需要实现多个域的规范,即 DOM、DOM Debugger、CSS、Overlay。

 

通过命令和事件进行通信

每个域都定义了它支持的多个命令以及它生成的事件。命令和事件都是固定结构的序列化 JSON 对象。应用程序通过与 Chrome DevTools 前端客户端建立套接字连接来进行调试,使用与域文档中描述的原始消息相同的消息。

以下是负责检索视图树的 DOM.getDocument 命令的外观

DOM.getDocument(): out ‘root’ of type Node
 
类型 Node: {
            nodeId: number
            parentId: number
            nodeType: number
            nodeName: string
            children: array [ Node]
            attributes: array [ string ]
}


然后,当命令来自 DevTools 时,嵌入器有责任构建一个类型为 `Node` 的对象并返回它。还有一些域事件,这些事件可以在应用程序执行期间的特定时间或特定条件下触发。虽然命令来自 DevTools 前端 - 当按钮被点击、选项卡被打开或 WebSocket 连接到前端建立时,事件是从应用程序中调用出来的,无论何时何地嵌入器认为合适,这取决于应用程序逻辑和域。传入的命令会影响应用程序的状态,或者请求结果。事件是传出的消息,在应用程序的某些状态下发生。域调度程序负责处理与 DevTools 前端的通信。

communication-diagram

调度程序,这是什么?

域调度程序是基于每个域的协议规范的类。它们定义什么是有效的方法或命令,什么不是,以及可用的功能。当 DevTools 连接到应用程序时,其调度程序未注册的 DevTools 域不会发送命令,也不应尝试向前端发送事件消息。

正如我之前提到的 - 域调度程序还负责接收来自前端客户端的传入消息,并将它们映射到特定域命令实现。还记得 DOM.getDocument 吗?它在应用程序中的实现将包括获取视觉树、提取相关信息(如节点名称、节点 ID、属性、子节点等)以及构建然后返回的 Node 对象的代码。

there is no magic

除了将您编写的 JavaScript 代码在移动平台上运行的魔法之外,运行时还与检查器(Android 中的 V8Inspector;iOS 中的 WebKit WebInspector)集成,这些检查器管理已注册的调度程序并向它们委托调用。

实现 DevTools 域

Android 和 iOS 的调试检查器都是用 C++ 实现的,这意味着它们直接与运行时 API 交互。有关 JavaScript 执行上下文的任何信息 - 调用堆栈、断点、暂停的上下文等等 - 都可用于底层的 JavaScript VM,并在调试会话期间被积极使用。例如,V8 检查器附带一个现成的调试器代理,它用于 Node.js,允许与 JavaScript 运行时无缝集成。

到目前为止一切都好 - 网络请求或应用程序计算的视图树的信息呢?我们如何让 Chrome DevTools 了解这些?我们只需要实现 Network 和 DOM 代理!只是,信息很少,协议描述有时要么不够,要么过于模糊,而且过去很少有人嵌入除 node.js 类应用程序以外的代理,更少的人对其进行过记录。但是,有什么比从Chromium 项目本身借鉴想法更好的来源呢!阅读源代码并调试 Chrome DevTools 的前端帮助我了解了工作流程、命令调用的顺序、预期参数等等。

您知道吗,在构建 Node 对象来描述 Chrome DevTools 中的视图树结构时,文档(或根)节点的“nodeType”为 9,而所有其他子元素的“nodeType”为 1?是的,我也不知道,事实证明,除非您阅读了“WHATWG”(Web 超文本应用技术工作组)DOM 规范,否则您会有一段时间处于黑暗之中。

实现细节!

在所有这些闲聊之后,让我们深入了解一些实现细节。

如前所述 - NativeScript 运行时本身无法直接了解当前有效的视图树,那么它如何报告网络请求统计信息和 DOM 树结构?

i know what you are thinking

一个词 - **回调**!

运行时公开全局函数,任何预定义 DevTools 接口的实现者都会调用这些函数。例如,让我们看看 `Network.requestWillBeSent` 事件 - 当页面即将发送 HTTP 请求时,需要向 DevTools 前端发送消息,这将在网络面板中的请求列表中创建一个挂起的条目。为了填充该列表,处理 HTTP 请求并希望与 DevTools 集成的插件需要构建一个 Request 对象,其接口是预定义的并提供的,然后只需调用 `global.__inspector.requestWillBeSent(myHttpRequest)` 即可。运行时将根据情况处理回调,并将序列化对象相应地发送到 Chrome DevTools 前端。

to infinity and beyond


事实上,这就是跨平台模块(`tns-core-modules` 包)http 模块实现方式 - 它将全局回调封装在一个 TypeScript 接口中,以确保代码的一致性,构造一个 Request 对象,并调用回调。下图显示了 DevTools 的工作流程,以及如何启动网络请求使其在网络选项卡中显示。

network-request-devtools

同样,当 NativeScript 跨平台视图(继承自 View)发生变化时,将调用全局回调,从而通知 Chrome DevTools 前端使用新信息进行更新,例如当视图子元素被追加移除另一个视图时。类似地,更改视图的属性,或尝试从 DevTools 前端中删除视图,将使运行时中的 DOM 调度程序执行一个命令,其中嵌入器 - 在这种情况下 - 我们应该对底层视图结构进行相应更改,以反映元素面板中视图树的新状态。在这种情况下,运行时应该调用跨平台模块公开的函数,这些函数负责处理修改。这就是上面 **DevTools** <-> **运行时** <-> **核心模块** 工作流程的说明(点击图片放大)。

dom-delete-element-diagram

现在您知道了 - Chrome DevTools 利用运行时已有的信息。DevTools 通过套接字连接与另一端的 DevTools 域抽象进行通信。剩下的只是实现细节问题。

 

下一步是什么?

to infinity and beyond correct

现在 Elements 面板支持已经推出 Android 平台,您很快就能在 iOS 应用中使用该功能。请期待已支持功能的完善和改进。一如既往,我们感谢您对我们工作的任何 反馈 - 随时分享您在使用调试器时遇到的任何问题,或可能的特性请求!