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 Frontend 或 GitHub 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 字段标记的,核心模块如下:
bindings
、common
、components
、console_counters
、dom_extension
、extensions
、host
、main
、persistence
、platform
、protocol_client
、sdk
、browser_sdk
、root
、services
、text_utils
、ui
、workspace
、screencast
。
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();
})();
InspectorFrontendHostInstanc
e 初始化值是 window.InspectorFrontendHost
(在 devtools-compatibility.js 中定义)。 调用 initializeInspectorFrontendHost()
时,会给 InspectorFrontendHostInstance
重新赋值。如果 window.InspectorFrontendHost
不存在,就实例化 InspectorFrontendHostStub
对象,将实例赋值给 window.InspectorFrontendHost
和 InspectorFrontendHostInstance
;否则将 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-initialization
和 late-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.NetworkManager
、SDK.SecurityOriginManager
、SDK.ResourceTreeModel
、SDK.RuntimeModel
、SDK.DebuggerModel
、SDK.CPUProfilerModel
、SDK.DOMModel
、SDK.CSSModel
、SDK.DOMDebuggerModel
、SDK.OverlayModel
、SDK.EmulationModel
、SDK.LogModel
、SDK.ServiceWorkerManager
、Components.TargetDetachedDialog
、SDK.ChildTargetManager
等。
获取 Target 对象:
- window.SDK.targetManager.mainTarget():获取主连接对象
- window.SDK.targetManager.targetById(id):根据 id 获取连接对象
**获取 Target 对象的 models:**target.models()
Agent 是与前端同步的后端数据对象,Agent 对象与 Chrome DevTools Protocol 中的 Domain 一一对应,Agent 对象包括:
inspectorAgent
、pageAgent
、networkAgent
、runtimeAgent
、debuggerAgent
、consoleAgent
、domAgent
、cssAgent
、profilerAgent
、timelineAgent
、workerAgent
等。
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._agentPrototypes
是 Map<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 内核的交互,本文暂未展开分析其运行机制。
参考
- The Hello World of Chrome DevTools Hacking
- Contributing to Chrome DevTools
- What is GN?
- 跨平台:GN实践详解(ninja, 编译, windows/mac/android实战)
- DevTools Frontend 源码分析
- How Blink works
- What happens when DevTools Pauses
- 理解 Embedder,理解 Chromium 的系统层次结构
- DevTools 架构浅析
写这些代码也许就一两个小时的事,写一篇大家好接受的文章需要几天的酝酿,如果文章对您有帮助请我喝杯咖啡吧!