背景需求
快应用开发工具致力于:让开发者能够更高效开发和调试快应用;为此增加了 Web 预览功能,同时开发预览调试器。由于预览的实现,是将快应用标签,解析成原生标签来模拟完成,导致调试器的 Elements 面板,无法审查真实的快应用元素。作为一个以前端技术栈为基础的框架,开发过程不能审查元素,体验是极其糟糕的,因此便产生了这个需求。
备注:此功能已于 2020 年 5 月中开发完毕,并在 2020 6 月 IDE 3.1
版本发布。原文出自:快应用 IDE 定制 Devtools 元素面板系列一:背景需求及方案分析;于此同时,还产出有另外三篇具体实现相关文章:
方案分析
首先市面上存在的同类功能的产品,包括 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 插件知识
代码结构
这里我们重点关注 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 我们从基于 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 的封装类,包括协议的解析和组装。
钩子函数
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;
}
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,也方便调试器后续能力的丰富完善。