快应用 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 插件开发的知识,这里简单介绍一下插件开发的几个核心概念

    1. manifest.json

    Chrome 插件最重要也是必不可少的文件,用来配置所有和插件相关的配置,必须放在根目录。

    1. content-scripts

    Chrome 插件提供的可向页面注入的脚本(JS 或 CSS),只能共享页面 DOM,而不共享页面 js 。

    1. 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 插件知识

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,也方便调试器后续能力的丰富完善。