快应用 IDE 定制 Devtools 元素面板系列一:背景需求及方案分析
背景需求
快应用开发工具致力于:让开发者能够更高效开发和调试快应用;为此增加了 Web 预览功能,同时开发预览调试器。由于预览的实现,是将快应用标签,解析成原生标签来模拟完成,导致调试器的 Elements 面板,无法审查真实的快应用元素。作为一个以前端技术栈为基础的框架,开发过程不能审查元素,体验是极其糟糕的,因此便产生了这个需求。
备注:此功能已于 5 月中开发完毕,将在 6 月 下旬,于 IDE 3.1
版本发布,敬请期待。
方案分析
首先市面上存在的同类功能的产品,包括 vue-devtools、react-devtools、各类小程序开发工具等,其中 vue-devtools、react-devtools 都是基于 devtools 插件或 electron 实现。各类小程序开发工具,如支付宝小程序开发工具:基于 devtools-frontend 实现。因此我们开始的预研方向分为两种:
- 基于 devtools 插件方式,使用插件 api 新增 elements panel;
- 自定义 devtools frontend,直接修改 devtools frontend 源码并集成到 IDE;
Chrome 插件开发简介
分析之前我们需要了解一定的 Chrome 插件开发的知识,这里简单介绍一下插件开发的几个核心概念
-
- manifest.json
Chrome 插件最重要也是必不可少的文件,用来配置所有和插件相关的配置,必须放在根目录。
-
- content-scripts
Chrome 插件提供的可向页面注入的脚本(JS 或 CSS),只能共享页面 DOM,而不共享页面 js 。
-
- background
- Chrome 插件提供的可运行在后台的页面,是一个常驻的页面,它的生命周期是插件中所有类型页面中最长的,它随着浏览器的打开而打开,随着浏览器的关闭而关闭,所以通常把需要一直运行的、启动就运行的、全局的代码放在 background 里面。
- 用于管理浏览器事件,在需要时加载,比如第一次安装、插件更新、有 content-script 向它发送消息。空闲时被卸载。( “persistent” : false )
Chrome 插件
如上图所示,content script 只共享页面的 DOM 数据,而不共享页面 js。content script 想要共享页面的 js ,插件中需要显示的声明能被页面访问的 js 文件,也就是图中的 web accessible resources 。同时 content script 跟 background 是可以通信的。
devtools 插件
DevTools 插件,主要是为 Chrome DevTools 添加功能。它可以添加新的 UI 面板和侧边栏,与检查的页面进行交互,获取有关网络请求的信息等等。DevTools 扩展可以访问一组特定的 DevTools API:
- devtools.inspectedWindow
- devtools.network
- devtools.panels
DevTools 插件程序的结构与普通插件程序一样:它也有 background、content-script 等项目。此外,每个 DevTools 插件都有一个 DevTools 页面,该页面可以访问 DevTools API。
更多 Chrome 插件知识
- https://developer.chrome.com/extensions/overview
- https://www.cnblogs.com/liuxianan/p/chrome-plugin-develop.html
- https://wiki.jikexueyuan.com/project/chrome-devtools/devtools-extensions-api.html
vue-devtools 插件分析
代码结构
这里我们重点关注 packages 目录下的代码
-
app-backend:被注入到 vue 页面的 js 模块
-
app-frontend:基于 vue 实现的 vue panel 模块
-
build-tools:编译工具模块
-
shared-utils:共享的工具模块,包含 Bridge 对象,数据存储等
-
shell-chrome:基于 chrome/Firefox 浏览器插件的实现模块
-
shell-dev:忽略
-
shell-electron:基于 electron 运行的实现模块
manifest.json
{
// 截取 manifest.json 片段
"web_accessible_resources": [
"devtools.html",
"devtools-background.html",
"build/backend.js"
],
"devtools_page": "devtools-background.html",
"background": {
"scripts": ["build/background.js"],
"persistent": false
},
"permissions": [
"http://*/*",
"https://*/*",
"file:///*",
"contextMenus",
"storage"
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["build/hook.js"],
"run_at": "document_start"
},
{
"matches": ["<all_urls>"],
"js": ["build/detector.js"],
"run_at": "document_idle"
}
]
}
根据上面介绍的插件知识,我们可以看到 vue-devtools 插件核心组件包含 devtools、background、content script。所以我们也主要重这几个模块入手分析。
框架
整体框架分为三大块:
- devtools 页面负责渲染 vue panel,通过 chrome.devtools.inspectedWindow.eval 方法向页面注入 backend.js 。
- background 页面负责建立 devtools 与 backend 之间的双向通信。
- backend 主要负责获取 vue 实例,并对该实例进行操作。
VueComponent
VueComponent 可以理解为 vue 页面渲染的虚拟 DOM,包含整个页面渲染所需的结构数据。
- Vue 直接把 VueComponent 实例挂载到了 document 的 childNode 上,即 vue。
- backend 扫描拿到 VueComponent 。
- vue panle 的数据都可通过前面建立的通信操作该实例获取。
扫描代码如下,backend 从 document 的 childNodes 逐层向下扫描直到找到 vue 实例:
if (isBrowser) {
walk(document, function(node) {
if (inFragment) {
if (node === currentFragment._fragmentEnd) {
inFragment = false;
currentFragment = null;
}
return true;
}
let instance = node.__vue__;
return processInstance(instance);
});
}
/**
* DOM walk helper
*
* @param {NodeList} nodes
* @param {Function} fn
*/
function walk(node, fn) {
if (node.childNodes) {
for (let i = 0, l = node.childNodes.length; i < l; i++) {
const child = node.childNodes[i];
const stop = fn(child);
if (!stop) {
walk(child, fn);
}
}
}
// also walk shadow DOM
if (node.shadowRoot) {
walk(node.shadowRoot, fn);
}
}
VueComponent 实例如下图:
vue-devtools 总结
vue-devtools 整体方案大致为:
- devtools 页面创建 vue panel,并向页面注入 backend.js。
- 通过 chrome.runtime.connect api 经过 background 页面 建立双向通信。
- backend 获取 vue 实例,后续 vue panel 与 页面的交互都可操作该实例完成。
react-devtools 插件分析
react-devtools 我们从基于 electron 平台的实现着手分析。
代码结构
-
agent:backend 端的交互类功能包含 Agent、Bridge 实现
-
backend:backend 端对 renderer 操作等功能
-
frontend:调试界面实现模块,基于 react-native
-
package:基于 electron 运行的实现模块
-
shells:不同平台实现入口
运行流程
-
启动 electron 窗口,加载 app.html 页面。页面 js 会开启一个 http server 和 websocket server。 http server 用于提供外部通过访问 url 获取 backend.js 文件的能力。 websocket server 用于 devtools 和 backend 间的长链接通信。
-
react 页面根 html 中需要额外加入一段引用 backend js 的 script,react 页面在加载后,backend 被注入页面,同时在 window 对象上 挂载 hook 并开始与 devtools 建立 socket 通信。
-
socket 通信建立成功后 devtools 和 backend 端各自持有 socket 的句柄。
-
devtools 端开始加载 react panel 页面,页面初始化后,向 backend 发送一个请求能力的命令。同时 backend 端也开始做一些配置初始化的操作,其中包含初始化 Bridge 和 Agent 。
-
backend 在接收到 devtools 请求能力的命令后,开始订阅 hook 的事件。并获取 window.React 实例,这个实例对象是 react 页面渲染的核心,接下来 backend 会对这个实例对象添加必要的钩子函数,以便监控 react 页面渲染过程。
-
最后 react panel 与页面间的操作都可以经过以上机制交互完成。
backend 三个核心模块
-
Hook.js
renderer 中的钩子函数的触发时,通过 Hook 将事件发射出来。
-
Agent.js
代理 renderer,并处理 Hook 发射出来的事件,同时转发给 Bridge 处理。
-
Bridge.js
Backend 与 Frontend 通信的桥梁,socket wall 的封装类,包括协议的解析和组装。
钩子函数
-
decorateResult
重写原函数,保持原函数执行的同时放入 fn 回调函数,将原函数的执行结果当作回调函数的参数传递给外部。
- renderer.Mount._renderNewRootComponent
- renderer.Mount.renderComponent
function decorateResult(obj, attr, fn) {
var old = obj[attr];
obj[attr] = function(instance: NodeLike) {
var res = old.apply(this, arguments);
fn(res);
return res;
};
return old;
}
-
decorate
重写原函数,保持原函数执行的同时放入 fn 回调函数,将原函数的参数当作回调函数的参数传递给外部。
- mountComponent
- updateComponent
- unmountComponent
function decorate(obj, attr, fn) {
var old = obj[attr];
obj[attr] = function(instance: NodeLike) {
var res = old.apply(this, arguments);
fn.apply(this, arguments);
return res;
};
return old;
}
框架
总结
基于对 vue-devtools 和 react-devtools 源码的分析我们不难发现,整个需求的实现其实主体分为三大块:
- 前端实现,也就是 elements 面板实现
- 通信方案及协议
- 后端实现,注入页面 js ,获取与渲染相关操作的实例对象
前端实现
参考同类开发工具我们首先排除插件的实现方案,采用定制 devtools frontend 方案,在 frontend 源码增加 panel 实现前端部分。
通信方案及协议
通信方案同样采用 websocket 方案,通信基于 json rpc 协议。
后端实现
后端注入采用 react-devtools 的方案,打包时在根页面内置 script ,开发工具提供 backend.js 。
初步方案模型基本与 react-devtools electron 部分的实现类似,不同的是我们前端部分使用了 devtools frontend,也方便调试器后续能力的丰富完善。