Chrome DevTools Frontend 运行原理浅析

前言

《深入理解 Chrome DevTools》一文中从整体上介绍了 Chrome DevTools 及 Electron 中集成 DevTools 实现调试器的基本原理,目前主流的小程序 IDE 调试器都是集成 Chrome DevTools Frontend 实现,主要有两种实现方式:

  • 不修改 Chrome 调试器源代码,使用默认的 chrome-devtools 协议,所有定制化需求都通过 Chrome DevTools Extensions 实现;(微信方案)
  • 修改 Chrome 调试器源代码,使用 http 协议,hard code 方式或提供 Chrome DevTools Extensions 运行环境进行定制。(阿里、百度方案)

这两种方式都需要对 Devtools Frontend 项目的代码有一定的了解,本文尝试从源码角度入手,体系化理解 DevTools Frontend 和 DevTools Extensions 运行机制,从而更完善的定制调试器功能。

DevTools 工作原理

Chrome DevTools 是辅助开发者进行 Web 开发的重要调试工具,DevTools 是 Chromium 的一部分,可整体集成于 Electron 应用中。

DevTools 主要由四部分组成:

  • Frontend:调试器前端,默认由 Chromium 内核层集成,DevTools Frontend 是一个 Web 应用程序;
  • Backend:调试器后端,Chromium、V8 或 Node.js;
  • Protocol:调试协议,调试器前端和后端使用此协议通信。 它分为代表被检查实体的语义方面的域。 每个域定义类型、命令(从前端发送到后端的消息)和事件(从后端发送到前端的消息)。该协议基于 json rpc 2.0 运行;
  • Message Channels:消息通道,消息通道是在后端和前端之间发送协议消息的一种方式。包括:Embedder Channel、WebSocket Channel、Chrome Extensions Channel、USB/ADB Channel。

这四部分的交互逻辑如下图所示:

源码下载及编译

DevTools Frontend 项目属于 Chromium 项目的子项目,代码托管在 Google 开源仓库上,代码开源从这里下载:Chromium DevTools FrontendGitHub DevTools Frontend 镜像。DevTools Frontend 项目和 Chromium 项目一样,也是基于 GN 构建,GN 是一种元构建系统,生成 Ninja 构建文件,编译速度快,效率上比基于 python 的 GYP 快了近 20 倍。

下载 DevTools Frontend 代码,发现在构建时执行 gn gen out/Default 报错,这是因为没有下载 depot_tools 工具,所以没有 gn 命令。

发现百度小程序又一个简化编译流程的方案:如何定制 chrome 开发者工具,但是考虑到后续可维护性,尽可能保持和源码结构、编译方式的一致性,这里还是按照官方的方式编译,后面确实有不方便的时候再考虑自定义编译流程。

安装 depot_tools 工具

下载 depot_tools 源码:

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

打开 ~/.bash_profile 文件,将 depot_tools 添加到 PATH 中 (/path/to/depot_tools 为你 depot_tools 本地的路径):

export PATH=$PATH:/path/to/depot_tools

执行环境变量立即生效命令:

source ~/.bash_profile

基于 Chrome 运行 DevTools

Chrome DevTools Frontend 可以检出并独立于 Chromium 作为独立项目构建。 主要优点是不必检出并构建 Chromium,但是在此工作流程中也无法运行布局测试。

检出代码

通过下面的命令可以只检出 DevTools frontend 的代码:

mkdir devtools
cd devtools
fetch devtools-frontend

构建

cd devtools-frontend
gn gen out/Default
autoninja -C out/Default

构建生成的文件在 out/Default/resources/inspector 中。

更新代码

git fetch origin
git checkout origin/master
gclient sync

运行

可以启动 Chrome (M79 或更高版本) 并与自定义的 DevTools frontend 一起运行。

<path-to-chrome>/chrome --custom-devtools-frontend=file://$(realpath out/Default/resources/inspector)

基于 DevServer 运行 DevTools

可以使用 DevTools Frontend 项目中的 npm start 命令来启动 Chrome canary 版本并启动 DevTools DevServer。

cd devtools/devtools-frontend
npm start

npm start 相当于同时执行了 npm run chrome 和 npm run server 命令。默认情况下,服务在端口 8090 上运行,远程调试端口为 9222,也可以在启动服务的时候设置一些参数修改。

# 设置服务端口
PORT=3000 npm run server
# 设置远程调试端口
REMOTE_DEBUGGING_PORT=9333 npm run server
# 设置 Chromium Commit
CHROMIUM_COMMIT = 813208daf9e9821657e55ef7c40a14815efdcf49 npm run server

执行完 npm start 命令会自动打开 Chrome,并且打开 localhost:9222,选择您要调试的目标。

再新建一个窗口,打开要远程调试的目标,这样就可以实现远程调试调试了,这里的 inspector.html 就是由我们自定义的 DevTools Frontend 提供。

源码整体分析

目录结构

DevTools Frontend 源码的目录结构如下:

├── build_overrides // Build 配置文件
├── build           // GN 模板和配置、Python 构建脚本(https://chromium.googlesource.com/chromium/src/build.git)
├── buildtools      // 构建工具 (https://chromium.googlesource.com/chromium/src/buildtools.git)
├── front_end       // devtools frontent 代码
|   ├── root        //
|   |   ├── Runtime.js  // 包含存储数据并可以检索的简单类,加载相应数据的帮助函数
|   ├── sdk
|   |   ├──
|   ├── ui
|   |   ├──
|   ├── devtools_compatibility.js
|   ├── externs.js
|   ├── root.js
|   ├── RuntimeInstantiator.js   // 解析配置加载 Modules,实例化 Runtime
|   ├── ...
|   ├── devtools_app.html // devtools_app 应用入口
|   ├── devtools_app.js
|   ├── devtools_app.json
|   ├── ...
|   ├── inspector.html    // inspector 应用入口
|   ├── inspector.js      // inspector 应用逻辑
|   ├── inspector.json    // inspector 描述文件
|   ├── ...
|   ├── node_app.html     // node_app 应用入口
|   ├── node_app.js
|   ├── node_app.json
|   ├── ...
├── scripts         // 编译、测试脚本
├── test
├── third_party
├── v8
├── .gn             // GN build 的 “源文件”,指定 Build 配置文件位置
├── BUILD.gn
├── DEPS            // Python 脚本,用来设定包含路径
└── WATCHLISTS

Application & Modules & Extensions

DevTools Frontend 项目核心逻辑在 front_end 中,front_end 是由 Application 和 Modules 组成。Application 是不同的调试器前端应用,类型分为:devtools_app、inspector、node_app、js_app、ndb_app、toolbox、worker_app 等,应用配置文件为 ${apppName}.json,modules 参数定义了依赖的模块,extends 定义继承配置的应用。应用的区别在于依赖的 Modules 不同,例如 inspector 比 devtools_app 仅仅多了一个 screencast 模块。

// inspector.json
{
  // 依赖的模块
  "modules": [{ "name": "screencast", "type": "autostart" }],
  // 继承的应用
  "extends": "devtools_app",
  // 是否有 html 页面
  "has_html": true
}

front_end 下的文件夹是具体的 module,module 通过文件夹下的 module.json 配置文件进行定义,配置文件有以下几个属性:

  • scripts:模块中包含的 JavaScript 文件数组,这里的路径名称是相对于 module.json 的位置;
  • skip_compilation:类似于脚本,但是 Closure Compiler 不会对这些文件进行类型检查;
  • resources:模块使用的非 JavaScript 文件数组;
  • dependencies:模块使用的其他模块的数组;
  • extensions:具有 type 属性的对象数组。 扩展可以通过运行时系统查询,并可以通过任何模块中的代码进行访问。类型包括 "setting"、"view","context-menu-item"。例如可以按如下方式注册出现在设置屏幕中的设置:
{
  "extensions": [
    {
      "type": "setting",
      "settingName": "interdimensionalWarpEnabled",
      "settingType": "boolean",
      "defaultValue": false,
      "storageType": "session",
      "title": "Show web pages from other dimensions"
    },
    ...
  ]
}

DevTools Frontend 通过 Module 和 Extension 机制为 Application 增加了“插件化”的能力,然后通过配置进行灵活的组装。Cordova 也是提供了类似的机制,看来整体思想上都是相通的。

Inspector 启动流程

npm start 命令流程

npm start 启动脚本是 scripts/hosted_mode/start_chrome_and_server.js,通过 childProcess.fork 启动 Chrome 和开发 Server。

const childProcess = require('child_process');

const chrome = childProcess.fork('scripts/hosted_mode/launch_chrome.js', process.argv.slice(2));
const server = childProcess.fork('scripts/hosted_mode/server.js');

chrome.on('exit', function() {
  server.kill();
});

launch_chrome 中的主要逻辑如下:

function launchChrome(filePath, chromeArgs) {
  console.log(`Launching Chrome from ${filePath}`);
  console.log('Chrome args:', chromeArgs.join(' '), '\n');
  let child;
  try {
    child = childProcess.spawn(filePath, chromeArgs, {
      stdio: 'inherit',
    });
  } catch (error) {
    onLaunchChromeError();
    return;
  }
  child.on('error', onLaunchChromeError);
  child.on('exit', onExit);
  function onExit(code) {
    console.log('Exited Chrome with code', code);
  }
}

查看 Chrome Canary 启动文件在电脑上的地址,设置启动参数,然后调用 childProcess.spawn 启动。

// Windows:
const suffix = '\\Google\\Chrome SxS\\Application\\chrome.exe';
const prefixes = [process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)']];
for (let i = 0; i < prefixes.length; i++) {
  const prefix = prefixes[i];
  try {
    chromeCanaryPath = path.join(prefix, suffix);
    fs.accessSync(chromeCanaryPath);
    break;
  } catch (e) {
  }
}

// Mac OS:
const lsregister = '/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister';
const chromeCanaryPath = shellOutput(`${lsregister} -dump | grep -i 'applications/google chrome canary.app$' | awk '{$1=""; print $0}' | head -n 1`);
chromeExecPath = `${chromeCanaryPath}/Contents/MacOS/Google Chrome Canary`;

启动参数获取如下,主要是配置远程调试端口(--remote-debugging-port)、自定义 DevTools Frontend(--custom-devtools-frontend)及重定向 user-data 目录(--user-data-dir)。

const REMOTE_DEBUGGING_PORT = parseInt(process.env.REMOTE_DEBUGGING_PORT, 10) || 9222;
const SERVER_PORT = parseInt(process.env.PORT, 10) || 8090;
const chromeArgs = [
  `--remote-debugging-port=${REMOTE_DEBUGGING_PORT}`,
  `--custom-devtools-frontend=http://localhost:${SERVER_PORT}/front_end/`, '--no-first-run',
  `http://localhost:${REMOTE_DEBUGGING_PORT}#custom=true`, 'https://devtools.chrome.com',
  `--user-data-dir=${CHROME_PROFILE_PATH}`
].concat(process.argv.slice(2));

开发 Server 就是定义了一个 Node.js 的 Http 服务器,可以通过 Http 服务的方式访问 frontend_end 代码。我们知道 DevTools Frontend 就是一个前端项目,可以通过 http://localhost:8090/front_end/inspector.html 访问。

startApplication 启动流程

调试器前端入口是 inspector.html 文件,我们可以看到引用了 root.js 和 inspector.js。项目中有多个调试器前端应用,根据不同的功能我们选择不同的调试器,这里我们选择最场景的 inspector.hrml 进行分析。

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Security-Policy" content="object-src 'none'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://chrome-devtools-frontend.appspot.com">
    <meta name="referrer" content="no-referrer">
    <script type="module" src="root.js"></script>
    <script type="module" src="inspector.js"></script>
</head>
<body class="undocked" id="-blink-dev-tools"></body>
</html>

root.js 中主要是引入一堆 JS 模块。inspector.js 中通过 startApplication 启动应用。

import './devtools_app.js';
import {startApplication} from './RuntimeInstantiator.js';

startApplication('inspector');

Factor out Runtime to root/Runtime.js 可以知道 RuntimeInstantiator.js 是程序入口。 startApplication 定义在 RuntimeInstantiator.js 中。 RuntimeInstantiator.js 中定义了 Root 对象,Root 对象下挂载了 Runtime 、allDescriptors、applicationDescriptor 三个对象。 allDescriptors 对象是所有的描述符(Descriptor)的集合,通过解析每个模块的 module.json 获取。

applicationDescriptor 是当前应用的描述对象。

startApplication 的具体实现为:

/**
 * @param {string} appName
 * @return {!Promise.<void>}
 */
export async function startApplication(appName) {
  console.timeStamp('Root.Runtime.startApplication');

  // 步骤1:优先遍历 Root.allDescriptors 生成 Object<string, RootModule.Runtime.ModuleDescriptor>} 类型的对象
  const allDescriptorsByName = {};
  for (let i = 0; i < Root.allDescriptors.length; ++i) {
    const d = Root.allDescriptors[i];
    allDescriptorsByName[d['name']] = d;
  }
  
  if (!Root.applicationDescriptor) {
    // 步骤2:如果当前应用的描述对象(Root.applicationDescriptor)不存在,通过 XMLHttpRequest 的方式加载 `${appName}.json` 文件
    let data = await RootModule.Runtime.loadResourcePromise(appName + '.json');
    Root.applicationDescriptor = JSON.parse(data);
    let descriptor = Root.applicationDescriptor;
    // 步骤3:递归加载依赖的 Modules 合并到 Root.applicationDescriptor 的 modules 对象(Array<{ name: string, tyle: 'autostart' | 'remote' } 类型).
    // 应用的描述对象(applicationDescriptor) 中的 extends 字段指的是继承关系(inspector => devtools_app => shell)
    while (descriptor.extends) {
      data = await RootModule.Runtime.loadResourcePromise(descriptor.extends + '.json');
      descriptor = JSON.parse(data);
      Root.applicationDescriptor.modules = descriptor.modules.concat(Root.applicationDescriptor.modules);
    }
  }
	
  // 步骤4:遍历 Root.applicationDescriptor.modules,生成 moduleJSONPromises,coreModuleNames 对象
  const configuration = Root.applicationDescriptor.modules;
  const moduleJSONPromises = [];
  const coreModuleNames = [];
  for (let i = 0; i < configuration.length; ++i) {
    const descriptor = configuration[i];
    const name = descriptor['name'];
    const moduleJSON = allDescriptorsByName[name];
    if (moduleJSON) {
      moduleJSONPromises.push(Promise.resolve(moduleJSON));
    } else {
      // 加载模块下的 module.json 文件
      moduleJSONPromises.push(
          RootModule.Runtime.loadResourcePromise(name + '/module.json').then(JSON.parse.bind(JSON)));
    }
    // 模块有 autostart | remote 两种类型,将 autostart 类型的模块加入到核心模块
    if (descriptor['type'] === 'autostart') {
      coreModuleNames.push(name);
    }
  }
	
  // 步骤5:所有的 module.json 读取完成,生成 moduleDescriptors 对象(Array<RootModule.Runtime.ModuleDescriptor> 类型),
  // 并将 moduleDescriptors 传入 Runtime 实例化函数,生成 Runtime 对象实例化对象 runtime 挂载在全局对象上
  const moduleDescriptors = await Promise.all(moduleJSONPromises);
  for (let i = 0; i < moduleDescriptors.length; ++i) {
    moduleDescriptors[i].name = configuration[i]['name'];
    moduleDescriptors[i].condition = configuration[i]['condition'];
    moduleDescriptors[i].remote = configuration[i]['type'] === 'remote';
  }
  self.runtime = RootModule.Runtime.Runtime.instance({forceNew: true, moduleDescriptors});
   // 步骤6:加载并自启动核心模块
  if (coreModuleNames) {
    await self.runtime.loadAutoStartModules(coreModuleNames);
  }
  appStartedPromiseCallback();
}

实例化 Runtime 对象流程:

export class Runtime {
  /**
   * @private
   * @param {!Array.<!ModuleDescriptor>} descriptors
   */
  constructor(descriptors) {
    /** @type {!Array<!Module>} */
    this._modules = [];
    /** @type {!Object<string, !Module>} */
    this._modulesMap = {};
    /** @type {!Array<!Extension>} */
    this._extensions = [];
    /** @type {!Object<string, function(new:Object):void>} */
    this._cachedTypeClasses = {};
    /** @type {!Object<string, !ModuleDescriptor>} */
    this._descriptorsMap = {};
		// 注册模块
    for (let i = 0; i < descriptors.length; ++i) {
      this._registerModule(descriptors[i]);
    }
  }
  
  /**
   * @param {!ModuleDescriptor} descriptor
   */
  _registerModule(descriptor) {
    // 根据描述符(Descriptor)对象实例化模块
    const module = new Module(this, descriptor);
    this._modules.push(module);
    this._modulesMap[descriptor['name']] = module;
  }
  ...
}

export class Module {
  /**
   * @param {!Runtime} manager
   * @param {!ModuleDescriptor} descriptor
   */
  constructor(manager, descriptor) {
    // 持有 Runtime 实例对象 runtime
    this._manager = manager;
    // 持有描述符对象 descriptor(RootModule.Runtime.ModuleDescriptor类型)
    this._descriptor = descriptor;
    // 模块名称
    this._name = descriptor.name;
    // 依赖的拓展列表(Array<!Extension> 类型)
    this._extensions = [];
		// {!Map<string, !Array<!Extension>>}
    this._extensionsByClassName = new Map();
    const extensions = /** @type {?Array.<!RuntimeExtensionDescriptor>} */ (descriptor.extensions);
    // 遍历 descriptor 对象下的 extensions
    for (let i = 0; extensions && i < extensions.length; ++i) {
      // 实例化模块下的扩展
      const extension = new Extension(this, extensions[i]);
      this._manager._extensions.push(extension);
      this._extensions.push(extension);
    }
    this._loadedForTest = false;
  }
  ...
}

export class Extension {
  /**
  * @param {!Module} moduleParam
  * @param {!RuntimeExtensionDescriptor} descriptor
  */
  constructor(moduleParam, descriptor) {
    this._module = moduleParam;
    this._descriptor = descriptor;

    this._type = descriptor.type;
    this._hasTypeClass = this._type.charAt(0) === '@';

    /**
    * @type {?string}
    */
    this._className = descriptor.className || null;
    this._factoryName = descriptor.factoryName || null;
  }
  ...
}

Runtime 初始化完成后,意味着 Runtime 对象与 Module 对象、Module 对象与模块下的 Extension 对象建立了联系,但是没有完成任何代码的加载。

步骤 6 主要是加载并启动核心模块,核心模块是在步骤 4 中根据 type 字段标记的,核心模块如下:

bindingscommoncomponentsconsole_countersdom_extensionextensionshostmainpersistenceplatformprotocol_clientsdkbrowser_sdkrootservicestext_utilsuiworkspacescreencast

Module 类的 _loadPromise 方法:

_loadPromise() {
  if (!this.enabled()) {
    return Promise.reject(new Error('Module ' + this._name + ' is not enabled'));
  }

  if (this._pendingLoadPromise) {
    return this._pendingLoadPromise;
  }

  const dependencies = this._descriptor.dependencies;
  const dependencyPromises = [];
  // module.json 中 dependencies 配置制定了 Module 的依赖,先保证依赖 Module 加载
  for (let i = 0; dependencies && i < dependencies.length; ++i) {
    dependencyPromises.push(this._manager._modulesMap[dependencies[i]]._loadPromise());
  }

  this._pendingLoadPromise = Promise.all(dependencyPromises)
  	// 加载 resources
    .then(this._loadResources.bind(this))
  	// 加载 Module JS
    .then(this._loadModules.bind(this))
  	// 加载 Scripts
    .then(this._loadScripts.bind(this))
  	// Module 加载完成标识,构造函数中初始化为 false
    .then(() => this._loadedForTest = true);

  return this._pendingLoadPromise;
}

_loadPromise 的核心逻辑就是处理模块间的依赖关系及模块内的资源引用。module.json 中 dependencies 字段配置了 Module 的依赖 Module,先保证依赖 Module 完成加载,依赖 Module 加载完成后再加载 Module 自身的资源 (resources、modules、scripts)。这里需要注意一个容易混淆的概念,Module 是指 front_end 文件夹下定义的模块,modules 是指 ES Module 规范的 JS 代码。

_loadModules 加载的实际就是 JS 代码,加载逻辑如下:

_loadModules() {
  if (!this._descriptor.modules || !this._descriptor.modules.length) {
    return Promise.resolve();
  }

  const namespace = this._computeNamespace();
  // @ts-ignore Legacy global namespace instantation
  self[namespace] = self[namespace] || {};

  const legacyFileName = `${this._name}-legacy.js`;
  const fileName = this._descriptor.modules.includes(legacyFileName) ? legacyFileName : `${this._name}.js`;

  // TODO(crbug.com/1011811): Remove eval when we use TypeScript which does support dynamic imports
  return eval(`import('../${this._name}/${fileName}')`);
}

核心模块加载完了,Inspector 应用就启动起来了,但是至此我们还是没有搞清楚程序是怎么完成页面渲染初始化的呢?像多数应用一样,Inspector 也是通过 “main” 函数启动的。

上面说的核心模块中有一个 main 模块,module.json 如下:

{
    "extensions": [
        {
            "type": "@Common.AppProvider",
            "className": "Main.SimpleAppProvider",
            "order": 10
        },
      	...
    ],
    "dependencies": [
        "extensions",
        "host",
        "platform",
        "sdk",
        "persistence"
    ],
    "scripts": [],
    "modules": [
        "main.js",
        "main-legacy.js",
        "SimpleApp.js",
        "ExecutionContextSelector.js",
        "MainImpl.js"
    ]
}

modules 最后一个文件是 MainImpl.js,里面定义了 MainImpl 类:

export class MainImpl {
  constructor() {
    MainImpl._instanceForTest = this;
    runOnWindowLoad(this._loaded.bind(this));
  }

  async _loaded() {
    console.timeStamp('Main._loaded');
    await Runtime.appStarted;
    Root.Runtime.setPlatform(Host.Platform.platform());
    Root.Runtime.setL10nCallback(ls);
    Host.InspectorFrontendHost.InspectorFrontendHostInstance.getPreferences(this._gotPreferences.bind(this));
  }
  ...
}

new MainImpl();

MainImpl 构造函数中的 runOnWindowLoad 方法定义在 Platform 模块。Platform 模块包含一堆全局的公用方法(例如 runOnWindowLoad、unescapeCssString、base64ToSize)和 JS API 的扩展(例如 string-utilities、number-utilities、array-utilities),无其他的外部依赖,按理来说他应该叫 utils。

// platform/utilities.js
self.runOnWindowLoad = function(callback) {
  function windowLoaded() {
    self.removeEventListener('DOMContentLoaded', windowLoaded, false);
    callback();
  }

  if (document.readyState === 'complete' || document.readyState === 'interactive') {
    callback();
  } else {
    self.addEventListener('DOMContentLoaded', windowLoaded, false);
  }
};

当页面加载完成,然后执行 _loaded 方法。_loaded 方法中有一个关于 Promise 非常有意思的写法。在 RuntimeInstantiator.js 中定义了 Runtime.appStarted:

let appStartedPromiseCallback;
Runtime.appStarted = new Promise(fulfil => appStartedPromiseCallback = fulfil);

这里的 await Runtime.appStarted 什么都没有返回,乍一看好像这里没有什么用啊?那么你屏蔽 startApplication 方法中的 appStartedPromiseCallback 试试,就会发现页面不渲染。

这里也是防止 _load 方法触发的时候其他 Module 还没有加载完,等所有的模块加载完了再调用 appStartedPromiseCallback() 让 await Runtime.appStarted 结束,从而继续执行下面的代码。 appStartedPromiseCallback() 执行完,意味着 startApplication 方法真正执行完成。

JS 与 Embedder 通信机制

MainImpl _load 方法调用了一个 Host.InspectorFrontendHost.InspectorFrontendHostInstance 对象下的 getPreferences 方法。

Host.InspectorFrontendHost.InspectorFrontendHostInstance.getPreferences(this._gotPreferences.bind(this));

这里的 Host.InspectorFrontendHost.InspectorFrontendHostInstance 对象是在 host/InspectorFrontendHost.js 中定义的。

// @ts-ignore Global injected by devtools-compatibility.js
export let InspectorFrontendHostInstance = window.InspectorFrontendHost;

(function() {
  function initializeInspectorFrontendHost() {
    let proto;
    if (!InspectorFrontendHostInstance) {
      // Instantiate stub for web-hosted mode if necessary.
      // @ts-ignore Global injected by devtools-compatibility.js
      window.InspectorFrontendHost = InspectorFrontendHostInstance = new InspectorFrontendHostStub();
    } else {
      // Otherwise add stubs for missing methods that are declared in the interface.
      proto = InspectorFrontendHostStub.prototype;
      for (const name of Object.getOwnPropertyNames(proto)) {
        const stub = proto[name];
        // @ts-ignore Global injected by devtools-compatibility.js
        if (typeof stub !== 'function' || InspectorFrontendHostInstance[name]) {
          continue;
        }

        console.error(
            'Incompatible embedder: method Host.InspectorFrontendHost.' + name + ' is missing. Using stub instead.');
        // @ts-ignore Global injected by devtools-compatibility.js
        InspectorFrontendHostInstance[name] = stub;
      }
    }

    // Attach the events object.
    InspectorFrontendHostInstance.events = new Common.ObjectWrapper.ObjectWrapper();
  }

  // FIXME: This file is included into both apps, since the devtools_app needs the InspectorFrontendHostAPI only,
  // so the host instance should not initialized there.
  initializeInspectorFrontendHost();
  // @ts-ignore Global injected by devtools-compatibility.js
  window.InspectorFrontendAPI = new InspectorFrontendAPIImpl();
})();

InspectorFrontendHostInstance 初始化值是 window.InspectorFrontendHost (在 devtools-compatibility.js 中定义)。 调用 initializeInspectorFrontendHost() 时,会给 InspectorFrontendHostInstance 重新赋值。如果 window.InspectorFrontendHost 不存在,就实例化 InspectorFrontendHostStub 对象,将实例赋值给 window.InspectorFrontendHostInspectorFrontendHostInstance;否则将 InspectorFrontendHostStub 原型链上的方法合并到 InspectorFrontendHostInstance 上。

我们在分析 startApplication 过程中没有地方加载 devtools-compatibility.js,全局搜索会发现 BUILD.gn 中定义了:

devtools_embedder_scripts = [
  "front_end/devtools_compatibility.js",
  "front_end/Tests.js",
]

调试过程中加载的是浏览器默认的文件:devtools://devtools/bundled/devtools_compatibility.js。devtools_compatibility.js 实际上是由 Embedder 自动注入到前端,具体实现可以看 devtools_frontend_host_impl.cc。我们知道 InspectorFrontendHostInstance 的方法通过 devtools_compatibility.js 定义,比如上面的 getPreferences 方法定位为:

const InspectorFrontendHostImpl = class {
  ...
  /**
   * @override
   * @param {function(!Object<string, string>)} callback
   */
  getPreferences(callback) {
    DevToolsAPI.sendMessageToEmbedder('getPreferences', [], /** @type {function(?Object)} */ (callback));
  }
  ...
}

window.InspectorFrontendHost = new InspectorFrontendHostImpl();

这里调用的 DevToolsAPI 对象定义在 devtools_compatibility.js 中,整体调用流程为:

DevToolsAPI 是挂载全局的对象,调用 DevToolsAPI.sendMessageToEmbedder(method, args, callback) 方法时,生成 callId,将 callId、callback 以 Key-Value 的方式保存在 _callbacks 对象中,最后调用DevToolsHost.sendMessageToEmbedder 方法实现 JS 层调用 Native 层。

/**
 * @param {string} method
 * @param {!Array.<*>} args
 * @param {?function(?Object)} callback
 */
sendMessageToEmbedder(method, args, callback) {
  const callId = ++this._lastCallId;
  if (callback) {
    this._callbacks[callId] = callback;
  }
  const message = {'id': callId, 'method': method};
  if (args.length) {
    message.params = args;
  }
  DevToolsHost.sendMessageToEmbedder(JSON.stringify(message));
}

DevToolsHost.sendMessageToEmbedder 是原生方法,在原生层定义,将我们调用的方法名和参数进行序列化发到原生层。Native 层调用 DevToolsAPI.embedderMessageAck 方法将回调结果返回,具体实现参考:ShellDevToolsBindings::SendMessageAck

void ShellDevToolsBindings::SendMessageAck(int request_id,
                                           const base::Value* arg) {
  base::Value id_value(request_id);
  CallClientFunction("DevToolsAPI.embedderMessageAck", &id_value, arg, nullptr);
}

JS 层 DevToolsAPI.embedderMessageAck 执行时根据 id(调用时的 callId) 获取回调函数然后执行,执行示意图如下:

DevTools Frontend UI 组件机制

Widget

DevTools Frontend 没有采用"关注点分离"(采用 HTML、CSS、JS 三种技术分离)的方式构建 UI,而是采用组件化方式,ui 模块中定义了 Widget。Widget 其实和前端 Web Component 组件概念类似,包含完整的生命周期管理,提供方法和事件更新组件状态,通过 JS 的方式构建 UI。Widget 包含 element 属性,还可以包含 ShadowRoot 和 CSS。

export class Widget extends Common.ObjectWrapper.ObjectWrapper {
  constructor(isWebComponent, delegatesFocus) {}
  parentWidget() {}
  children() {}
  isShowing() {}
  wasShown() {}
  willHide() {}
  onResize() {}
  onLayout() 
  ownerViewDisposed() {}
	show(parentElement, insertBefore) {}
	showWidget() {}
	hideWidget() {}
	detach(overrideHideOnDetach) {}
	doResize() {}
	doLayout() {}
	registerRequiredCSS(cssFile) {}
	calculateConstraints() {}
	...
}

通过 Widget 相关的 API 创建布局:

var parentWidget = new UI.Widget();
var childWidget = new UI.Widget();
childWidget.show(parentWidget.element);

Widget 可以显示在任何其他 Widget 元素上,也可以显示在 Widget 元素的任何后代上。

Widgets 类型的组件:

  • SplitWidget:SplitWidget 有两个子 Widget,并且它们之间有一个边框,边框可以由用户调整大小;
  • VBox:Flex 垂直布局容器,CSS 值为 flex-direction: column;子类包括:RootView、InspectorView、Panel、TabbedPane、ContainerWidget、ListWidget 等;
  • HBox:Flex 水平布局容器,CSS 值为 flex-direction: row,子类包括:FilterBar、SegmentedButton 等

createAppUI

前面的启动流程中我分析了Root 模块中 startApplication 的启动流程及 Host 模块中 JS 与 Embedder 的通信机制,但是对于 UI 元素是如何展示在页面的没有分析,我们接着 main 模块 MainImpl.js 中的流程进行

分析。MainImpl.js 中通过 InspectorFrontendHostInstance.getPreferences 获取到了首选项 (Preferences) 相关参数,然后调用 _createSettings 方法配置 Experiments、Settings 相关对象的初始化。设置初始化完成后,调用 _createAppUI 方法,开始创建应用视图。

// main/MainImpl.js
async _createAppUI() {
  MainImpl.time('Main._createAppUI');
	// 初始化 ViewManager(视图管理器),UI.viewManager._views (Map<string, ProvidedView> 类型)
  self.UI.viewManager = UI.ViewManager.ViewManager.instance();

  // Request filesystems early, we won't create connections until callback is fired. Things will happen in parallel.
  self.Persistence.isolatedFileSystemManager =
    Persistence.IsolatedFileSystemManager.IsolatedFileSystemManager.instance();

  const themeSetting = Common.Settings.Settings.instance().createSetting('uiTheme', 'systemPreferred');
  UI.UIUtils.initializeUIUtils(document, themeSetting);
	// document 节点注入 className,注入全局 CSS 样式
  UI.UIUtils.installComponentRootStyles(/** @type {!Element} */ (document.body));
	// keydown、beforecopy、copy、cut、paste、contextmenu 事件处理
  this._addMainEventListeners(document);

  const canDock = !!Root.Runtime.queryParam('can_dock');
  self.UI.zoomManager = UI.ZoomManager.ZoomManager.instance(
    {forceNew: true, win: window, frontendHost: Host.InspectorFrontendHost.InspectorFrontendHostInstance});
  // 初始化 InspectorView
  self.UI.inspectorView = UI.InspectorView.InspectorView.instance();
  // 上下文菜单初始化(鼠标点击右键或者按下键盘上的菜单键时被触发)
  UI.ContextMenu.ContextMenu.initialize();
  // 初始化 ContextMenu 监听事件
  UI.ContextMenu.ContextMenu.installHandler(document);
  // 初始化 Tooltip 监听事件
  UI.Tooltip.Tooltip.installHandler(document);
  // 初始化 SDK.ConsoleModel
  self.SDK.consoleModel = SDK.ConsoleModel.ConsoleModel.instance();
  // 初始化 devtools dock 控制器
  self.Components.dockController = new Components.DockController.DockController(canDock);
  // 初始化 SDK.NetworkManager
  self.SDK.multitargetNetworkManager = new SDK.NetworkManager.MultitargetNetworkManager();
  // 初始化 SDK.DOMDebuggerModel(管理 DOM 断点、DOM 事件断点等)
  self.SDK.domDebuggerManager = new SDK.DOMDebuggerModel.DOMDebuggerManager();
  // 初始化 SDK.TargetManager
  SDK.SDKModel.TargetManager.instance().addEventListener(
    SDK.SDKModel.Events.SuspendStateChanged, this._onSuspendStateChanged.bind(this));

  self.UI.shortcutsScreen = new UI.ShortcutsScreen.ShortcutsScreen();
  // set order of some sections explicitly
  self.UI.shortcutsScreen.section(Common.UIString.UIString('Elements Panel'));
  self.UI.shortcutsScreen.section(Common.UIString.UIString('Styles Pane'));
  self.UI.shortcutsScreen.section(Common.UIString.UIString('Debugger'));
  self.UI.shortcutsScreen.section(Common.UIString.UIString('Console'));
	// 初始化 WorkSpace (位于 Resource 面板内)
  self.Workspace.fileManager = new Workspace.FileManager.FileManager();
  self.Workspace.workspace = Workspace.Workspace.WorkspaceImpl.instance();
	// 初始化 Bindings
  self.Bindings.networkProjectManager = Bindings.NetworkProject.NetworkProjectManager.instance();
  self.Bindings.resourceMapping = Bindings.ResourceMapping.ResourceMapping.instance({
    forceNew: true,
    targetManager: SDK.SDKModel.TargetManager.instance(),
    workspace: Workspace.Workspace.WorkspaceImpl.instance()
  });
  new Bindings.PresentationConsoleMessageHelper.PresentationConsoleMessageManager();
  self.Bindings.cssWorkspaceBinding = Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance({
    forceNew: true,
    targetManager: SDK.SDKModel.TargetManager.instance(),
    workspace: Workspace.Workspace.WorkspaceImpl.instance()
  });
  self.Bindings.debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({
    forceNew: true,
    targetManager: SDK.SDKModel.TargetManager.instance(),
    workspace: Workspace.Workspace.WorkspaceImpl.instance()
  });
  self.Bindings.breakpointManager = Bindings.BreakpointManager.BreakpointManager.instance({
    forceNew: true,
    workspace: Workspace.Workspace.WorkspaceImpl.instance(),
    targetManager: SDK.SDKModel.TargetManager.instance(),
    debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance()
  });
  // 初始化 ExtensionServer
  self.Extensions.extensionServer = new Extensions.ExtensionServer.ExtensionServer();
	
  new Persistence.FileSystemWorkspaceBinding.FileSystemWorkspaceBinding(
    Persistence.IsolatedFileSystemManager.IsolatedFileSystemManager.instance(),
    Workspace.Workspace.WorkspaceImpl.instance());
  self.Persistence.persistence = new Persistence.Persistence.PersistenceImpl(
    Workspace.Workspace.WorkspaceImpl.instance(), Bindings.BreakpointManager.BreakpointManager.instance());
  self.Persistence.networkPersistenceManager = new Persistence.NetworkPersistenceManager.NetworkPersistenceManager(
    Workspace.Workspace.WorkspaceImpl.instance());

  new ExecutionContextSelector(SDK.SDKModel.TargetManager.instance(), self.UI.context);
  self.Bindings.blackboxManager = Bindings.BlackboxManager.BlackboxManager.instance({
    forceNew: true,
    debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance()
  });
	
  // 中断监听器(初始化 SDK.DebuggerModel.Events.DebuggerPaused 事件监听)
  new PauseListener();

  self.UI.actionRegistry = new UI.ActionRegistry.ActionRegistry();
  self.UI.shortcutRegistry = new UI.ShortcutRegistry.ShortcutRegistry(self.UI.actionRegistry);
  UI.ShortcutsScreen.ShortcutsScreen.registerShortcuts();
  this._registerForwardedShortcuts();
  this._registerMessageSinkListener();

  MainImpl.timeEnd('Main._createAppUI');
	// 初始化 Common.AppProvider 插件
  this._showAppUI(await self.runtime.extension(Common.AppProvider.AppProvider).instance());
}

_createAppUI 方法主要是核心模块的初始化,这里我只分析其中部分的具体实现,如果想比较系统的过一遍,可以通过 DevTools 打断点走一遍流程。

实例化 ViewManager

视图管理器 (ViewManager) 提供了对于对于视图插件 (View Extension) 的管理,包括对视图插件的初始化、访问,展示。

export class ViewManager {
  constructor() {
    /** @type {!Map<string, !View>} */
    this._views = new Map();
    /** @type {!Map<string, string>} */
    this._locationNameByViewId = new Map();
		// Runtime 实例化过程保存了 _extensions 变量,筛选出 view 类型的 extensions
    for (const extension of self.runtime.extensions('view')) {
      const descriptor = extension.descriptor();
      this._views.set(descriptor['id'], new ProvidedView(extension));
      this._locationNameByViewId.set(descriptor['id'], descriptor['location']);
    }
  }

  view(viewId) {
    return this._views.get(viewId);
  }

  showView(viewId, userGesture, omitFocus) {
    const view = this._views.get(viewId);
   	...
  }
  ...
}

Runtime 实例化过程保存了 _extensions 变量,通过 runtime.extensions('view') 方法筛选出 view 类型的 extensions,ProvidedView 是一个类,对如下对 extension 对象进行解析然后生成一个实例保存到成员变量 _views 中。可以通过 view 方法获取 View Extension,通过 showView 显示 View Extension。

{
  "type": "view",
  "location": "panel",
  "id": "elements",
  "title": "Elements",
  "order": 10,
  "className": "Elements.ElementsPanel"
}

实例化 InspectorView

调试器视图 (InspectorView) 定义 DevTools 调试面板的布局,提供对调试面板管理的功能。DevTools Frontend InspectorView 层级为:

export class InspectorView extends VBox {
  constructor() {
    super();
    GlassPane.setContainer(this.element);
    this.setMinimumSize(240, 72);

    // DevTools sidebar is a vertical split of panels tabbed pane and a drawer.
    this._drawerSplitWidget = new SplitWidget(false, true, 'Inspector.drawerSplitViewState', 200, 200);
    this._drawerSplitWidget.hideSidebar();
    this._drawerSplitWidget.hideDefaultResizer();
    this._drawerSplitWidget.enableShowModeSaving();
    // 将 _drawerSplitWidget 填加到 element 作为子视图
    this._drawerSplitWidget.show(this.element);

    // Create drawer tabbed pane.
    this._drawerTabbedLocation =
        ViewManager.instance().createTabbedLocation(this._showDrawer.bind(this, false), 'drawer-view', true, true);
    const moreTabsButton = this._drawerTabbedLocation.enableMoreTabsButton();
    moreTabsButton.setTitle(ls`More Tools`);
    this._drawerTabbedPane = this._drawerTabbedLocation.tabbedPane();
    this._drawerTabbedPane.setMinimumSize(0, 27);
    ...

    // 创建选项卡视图
    this._tabbedLocation = ViewManager.instance().createTabbedLocation(
        Host.InspectorFrontendHost.InspectorFrontendHostInstance.bringToFront.bind(
            Host.InspectorFrontendHost.InspectorFrontendHostInstance),
        'panel', true, true, Root.Runtime.queryParam('panel'));

    this._tabbedPane = this._tabbedLocation.tabbedPane();
    this._tabbedPane.registerRequiredCSS('ui/inspectorViewTabbedPane.css');
    this._tabbedPane.addEventListener(TabbedPaneEvents.TabSelected, this._tabSelected, this);
    this._tabbedPane.setAccessibleName(Common.UIString.UIString('Panels'));

    // Store the initial selected panel for use in launch histograms
    Host.userMetrics.setLaunchPanel(this._tabbedPane.selectedTabId);

    if (Host.InspectorFrontendHost.isUnderTest()) {
      this._tabbedPane.setAutoSelectFirstItemOnShow(false);
    }
    this._drawerSplitWidget.setMainWidget(this._tabbedPane);

    this._keyDownBound = this._keyDown.bind(this);
    Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.addEventListener(
        Host.InspectorFrontendHostAPI.Events.ShowPanel, showPanel.bind(this));

    /**
     * @this {InspectorView}
     * @param {!Common.EventTarget.EventTargetEvent} event
     */
    function showPanel(event) {
      const panelName = /** @type {string} */ (event.data);
      this.showPanel(panelName);
    }
  }
  
  addPanel(view) {}
  hasPanel(panelName) {}
  panel(panelName) {}
  showPanel(panelName) {}
  ...
}

我们知道如果要实现一个选项卡,我们需要构造两部分:选项卡标题组、选项卡内容,选项卡标题切换会切换选项卡内容。上面的 _tabbedPane 对象就是创建了一个选项卡对象。具体实现逻辑为:

this._tabbedLocation = ViewManager.instance().createTabbedLocation(
   Host.InspectorFrontendHost.InspectorFrontendHostInstance.bringToFront.bind(
     Host.InspectorFrontendHost.InspectorFrontendHostInstance),
   'panel', true, true, Root.Runtime.queryParam('panel'));

createTabbedLocation 方法中实例化 _TabbedLocation 类,第二个参数是 location,_TabbedLocation 中将 location 筛选出 location 为 'panel' 的 _views,通过 _tabbedPane.appendTab 添加为 Tab。我们可以通过 UI.inspectorView._tabbedPane 对 Tab 进行操作。例如下面的逻辑:

隐藏默认面板:

const tabbedPane = window.UI.inspectorView._tabbedPane;
tabbedPane.closeTab('elements');

监听 TabbedPane 事件:

window.UI.inspectorView._tabbedPane.addEventListener(UI.TabbedPane.Events.TabSelected, tabSelectedCallback);
window.UI.inspectorView._tabbedPane.addEventListener(TabbedPaneEvents.TabClosed, tabClosedCallback);
window.UI.inspectorView._tabbedPane.addEventListener(TabbedPaneEvents.TabOrderChanged, tabClosedCallback);

实例化 SimpleApp

_createAppUI() 最后一行代码显示 App 的 UI。

this._showAppUI(await self.runtime.extension(Common.AppProvider.AppProvider).instance());

通过 extension 方法获取 AppProvider 类,实例化 AppProvider。

main 模块 module.json 中注册了 AppProvider Extension:

{
	"extensions": [
		{
			"type": "@Common.AppProvider",
			"className": "Main.SimpleAppProvider",
			"order": 10
		},
    ...
	]
}

通过 Extension type 获取插件类,然后获取 AppProvider 的实例化对象 ,传入到 _showAppUI 方法中进行初始化。

_showAppUI(appProvider) {
    MainImpl.time('Main._showAppUI');
    const app = /** @type {!Common.AppProvider.AppProvider} */ (appProvider).createApp();
    // It is important to kick controller lifetime after apps are instantiated.
    self.Components.dockController.initialize();
    app.presentUI(document);
  	...
     // Allow UI cycles to repaint prior to creating connection.
    setTimeout(this._initializeTarget.bind(this), 0);
    MainImpl.timeEnd('Main._showAppUI');
}

实例化 AppProvider 实际上是初始化 SimpleAppProvider 类,SimpleAppProvider 类的 createApp 方法中进行实例化 SimpleApp。

export default class SimpleApp {
  presentUI(document) {
    const rootView = new UI.RootView.RootView();
    self.UI.inspectorView.show(rootView.element);
    rootView.attachToDocument(document);
    rootView.focus();
  }
}

presentUI 方法生成 RootView 实例 rootView 视图对象,将 inspectorView 视图对象添加到 rootView 视图对象,rootView 视图挂载于 document.body 上,至此完成了 DOM 的生成和挂载。

Frontend 与 Backend 的通信机制

Target & Models & Agents

MainImpl.js 后续流程的 _initializeTarget 和 _lateInitialization 两个方法分别加载 early-initializationlate-initialization 类型的 Extension。

async _initializeTarget() {
    MainImpl.time('Main._initializeTarget');
    const instances =
        await Promise.all(self.runtime.extensions('early-initialization').map(extension => extension.instance()));
    for (const instance of instances) {
      await /** @type {!Common.Runnable.Runnable} */ (instance).run();
    }
    // Used for browser tests.
    Host.InspectorFrontendHost.InspectorFrontendHostInstance.readyForTest();
    // Asynchronously run the extensions.
    setTimeout(this._lateInitialization.bind(this), 100);
    MainImpl.timeEnd('Main._initializeTarget');
}

Application 在初始化 early-initialization 类型时关联的主模块:

  • devtools_app:InspectorMain.InspectorMain
  • js_app:JsMain.JsMain
  • node_app:NodeMain.NodeMain
  • worker_app:WorkerMain.WorkerMain

devtools_app 中会根据 early-initialization 类型找到 inspector_main 模块的 InspectorMain 类,然后获取插件实例,调用 run 方法。

// inspector_main/InspectorMain.js
export class InspectorMainImpl extends Common.ObjectWrapper.ObjectWrapper {
  async run() {
    let firstCall = true;
    await SDK.Connections.initMainConnection(async () => {
      const type = Root.Runtime.queryParam('v8only') ? SDK.SDKModel.Type.Node : SDK.SDKModel.Type.Frame;
      const waitForDebuggerInPage = type === SDK.SDKModel.Type.Frame && Root.Runtime.queryParam('panel') === 'sources';
      // 创建 Target 对象
      const target = SDK.SDKModel.TargetManager.instance().createTarget(
          'main', Common.UIString.UIString('Main'), type, null, undefined, waitForDebuggerInPage);
      ...
  }
}

上面代码中的 TargetManager 管理 Target 对象,Target 对象管理模型 (Model) 与代理 (Agent) 之间的通信。createTarget 会创建一个 name 为 Target 对象。

createTarget(id, name, type, parentTarget, sessionId, waitForDebuggerInPage, connection) {
  const target =
        new Target(this, id, name, type, parentTarget, sessionId || '', this._isSuspended, connection || null);
  if (waitForDebuggerInPage) {
    target.pageAgent().waitForDebugger();
  }
  // 创建 Models 对象,_modelObservers Multimap 类型,观察并收集 Model 对象
  target.createModels(new Set(this._modelObservers.keysArray()));
  // 添加到 TargetManager _targets 集合中
  this._targets.add(target);

  // Iterate over a copy. _observers might be modified during iteration.
  for (const observer of [...this._observers]) {
    observer.targetAdded(target);
  }

  for (const modelClass of target.models().keys()) {
    const model = /** @type {!SDKModel} */ (target.models().get(modelClass));
    this.modelAdded(target, modelClass, model);
  }

  for (const key of this._modelListeners.keysArray()) {
    for (const info of this._modelListeners.get(key)) {
      const model = target.model(info.modelClass);
      if (model) {
        model.addEventListener(key, info.listener, info.thisObject);
      }
    }
  }

  return target;
}

Model 是与后端同步的前端数据对象,Model 对象包含:

SDK.NetworkManagerSDK.SecurityOriginManagerSDK.ResourceTreeModelSDK.RuntimeModelSDK.DebuggerModelSDK.CPUProfilerModelSDK.DOMModelSDK.CSSModelSDK.DOMDebuggerModelSDK.OverlayModelSDK.EmulationModelSDK.LogModelSDK.ServiceWorkerManagerComponents.TargetDetachedDialogSDK.ChildTargetManager 等。

获取 Target 对象:

  • window.SDK.targetManager.mainTarget():获取主连接对象
  • window.SDK.targetManager.targetById(id):根据 id 获取连接对象

**获取 Target 对象的 models:**target.models()

Agent 是与前端同步的后端数据对象,Agent 对象与 Chrome DevTools Protocol 中的 Domain 一一对应,Agent 对象包括:

inspectorAgentpageAgentnetworkAgentruntimeAgentdebuggerAgentconsoleAgentdomAgentcssAgentprofilerAgenttimelineAgentworkerAgent 等。

Target 继承自 TargetBase 类,TargetBase 类构建函数将 inspectorBackend._agentPrototypes 按照 domian 分类挂载到 _agents 对象上。

export class TargetBase {
    constructor(needsNodeJSPatching, parentTarget, sessionId, connection) {
      	...
        /** @type {!Object<string,!_AgentPrototype>} */
        this._agents = {};
        for (const [domain, agentPrototype] of inspectorBackend._agentPrototypes) {
          this._agents[domain] = Object.create(/** @type {!_AgentPrototype} */ (agentPrototype));
          this._agents[domain]._target = this;
        }
      	...
    }
}

front_end/generated/InspectorBackendCommands.js 是根据调试协议自动生成的文件,InspectorBackendCommands.js 中通过 inspectorBackend.registerCommand 注册调用 _agentPrototype 方法。

_agentPrototype(domain) {
  if (!this._agentPrototypes.has(domain)) {
    this._agentPrototypes.set(domain, new _AgentPrototype(domain));
    this._addAgentGetterMethodToProtocolTargetPrototype(domain);
  }

  return /** @type {!_AgentPrototype} */ (this._agentPrototypes.get(domain));
}

inspectorBackend._agentPrototypesMap<domain, _AgentPrototype> 类型,同时通过 _addAgentGetterMethodToProtocolTargetPrototype{domian}Agent 方法挂载 TargetBase 原型上。

const methodName = domain.substr(0, upperCaseLength).toLowerCase() + domain.slice(upperCaseLength) + 'Agent';

/**
 * @this {TargetBase}
 */
function agentGetter() {
  return this._agents[domain];
}

TargetBase.prototype[methodName] = agentGetter;

初始化 Main Connection

initMainConnection 方法中是 Connection 具体过程:

// sdk/Connections.js
export async function initMainConnection(createMainTarget, websocketConnectionLost) {
  ProtocolClient.InspectorBackend.Connection.setFactory(_createMainConnection.bind(null, websocketConnectionLost));
  await createMainTarget();
  Host.InspectorFrontendHost.InspectorFrontendHostInstance.connectionReady();
  Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.addEventListener(
      Host.InspectorFrontendHostAPI.Events.ReattachMainTarget, () => {
        TargetManager.instance().mainTarget().router().connection().disconnect();
        createMainTarget();
      });
  return Promise.resolve();
}

export function _createMainConnection(websocketConnectionLost) {
  const wsParam = Root.Runtime.queryParam('ws');
  const wssParam = Root.Runtime.queryParam('wss');
  if (wsParam || wssParam) {
    const ws = wsParam ? `ws://${wsParam}` : `wss://${wssParam}`;
    return new WebSocketConnection(ws, websocketConnectionLost);
  }
  if (Host.InspectorFrontendHost.InspectorFrontendHostInstance.isHostedMode()) {
    return new StubConnection();
  }

  return new MainConnection();
}

从 _createMainConnection 方法我们可以看到建立 Main Connection 的方式有三种:

  • Query Params 模式:实例化 WebSocketConnection 对象,建立 WebSocket Channel,远程调试就是基于这种模式;
  • HostedMode 模式:实例化 StubConnection 对象,建立 Embedder Channel,Inspected Window 与 DevTools Page 的通信就是这种模式;
  • MainConnection 模式:实例化 MainConnection 对象,具体是什么待进一步研究。

**获取 Target 的 connection:**SDK.targetManager.mainTarget().router().connection()

Inspector.html Query Params:

http://localhost:8090/front_end/inspector.html?experiments=true&can_dock=true&dockSide=right&ws=localhost:9222/devtools/page/1FFDE891-34F1-485C-A0CB-066EBF852E53
  • experiments=true:强制 DevTools 实验功能可用
  • can_dock=true&dockSide=right:设置 DevTools dock 方式
  • ws=…:Target WebSocket Server URL

DevTools Protocol Client

上面 Connections.js 中建立了 WebSocket Channel 或 Embedder Channel,借助于通信通道能够实现调试器前端与后端的交互。类似于 HTTP 协议一样,调试器定义了调试协议(DevTools Protocol),DevTools Frontend 中实现了一个 Protocol Client (protocol_client / InspectorBackend.js),通过 sendMessage 发送协议消息,_onMessage 接收协议消息。

Chrome 可以通过 Protocol monitor 工具来查看调试协议。

调试协议把操作划分为不同的域 (domain) ,比如 DOM、Debugger、Network、Console 和 Timeline 等,可以理解为 DevTools 中的不同功能模块,每个域 (domain) 定义了支持的 command 及监听的 event。每个 command 包含 request 和 response 两部分,request 部分指定的操作以及操作相关的参数,response 部分表明操作状态,成功或失败。command 和 event 中可能涉及到非基本数据类型,在 domain 中被归为 Type,比如:'frameId': <FrameId>,其中 FrameId 为非基本数据类型。

DevTools Frontend 中关于 Chrome DevTools Protocol 的处理是借助 Agent 对象给 Protocol Server 发送消息,例如 DOMModel 对象请问 Document 内容:

/**
 * @return {!Promise<?DOMDocument>}
 */
async _requestDocument() {
	const documentPayload = await this._agent.getDocument();
	delete this._pendingDocumentRequestPromise;
		
	if (documentPayload) {
		this._setDocument(documentPayload);
	}
  ...
}

只要按照对应的规范构建通信协议消息也可以实现类似的功能,比如我们也可以通过下面的方式向 WebView 中注入 JS 代码:

SDK.targetManager.mainTarget().router().connection().sendRawMessage(JSON.stringify({ 
  method: 'Runtime.evaluate',
  params: {
    expression: 'injectScript()'
  }
}));

WebView 内核层收到 Runtime.evaluate 消息后就可以执行 Runtime.evaluate 相关的方法实现代码执行。原理上和 RPC 机制类似,可以实现远程过程调用。

总结

本文从编译 Chrome DevTools Frontend 源码入手,介绍了调试器启动流程、UI 组件机制、DevTools 前后端通信机制等多个重要部分,文章内容较长适合结合源码一起看,详细看完本文您对 Devtools 整体的运行原理有一定的认知,当然 Chrome DevTools Frontend 中模块较多,没有办法一一完整讲解,不过熟悉了整体的设计思路其他模块也是类似的机制,另外在 Chrome DevTools 中 Extension 机制是非常重要的部分,由于 Extension 机制涉及到和 Chromium 内核的交互,本文暂未展开分析其运行机制。

参考

写这些代码也许就一两个小时的事,写一篇大家好接受的文章需要几天的酝酿,如果文章对您有帮助请我喝杯咖啡吧!