Pines-Cheng / blog

技术博客
https://pines-cheng.github.io/blog/
540 stars 42 forks source link

Chrome DevTools 原理、拓展与整合 #78

Open Pines-Cheng opened 4 years ago

Pines-Cheng commented 4 years ago

DevTools 原理

DevTools 本质上可以看成是一个前端小应用,代码在这里: ChromeDevTools/devtools-frontend,当然,你也可以在 Chrome 浏览器直接打开:devtools://devtools/bundled/inspector.html 查看运行效果。

image

DevTools 是通过 Chrome 远程调试协议(Remote Debugger Protocal) 来和后端进行交互和调试的,这里说的后端一般指的是:Chrome 的远程调试功能,可以通过

sudo /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222

在指定端口开启,然后在浏览器地址栏输入 http://localhost:${port} 能看到一个列表页面,列出了当前所有可调试的页面和插件。

image

点击Example Page,会导向到 http://localhost:9222/devtools/inspector.html?ws=localhost:9222/devtools/page/55A4F84F6A66845F72388146E3B8F986。长得和内嵌devtools 一样的 html 页面。

image

inspector.html 和 Chrome Host 之间通过 WebSocket 建立连接,这个 WebSocket 地址就是 url 中 ws参数的值。其中 55A4F84F6A66845F72388146E3B8F986page id,每个页面都有一个唯一的page id,Chrome 就是通过这个 id 确定哪个是目标页面。页面和 Chrome 内核之间就是通过这个连接交换数据的。Chrome 调试器实例和目标页面实例之间是进程通信,所以 inspector.html 可以通过Chrome 调试器实例加载目标页面的 Source 文件,还可以操作目标页面,例如加断点、刷新、记录Network 信息等。

通过 Chrome 远程调试协议(Remote Debugger Protocal) 建立连接之后,就会向调试后台发送很多请求来展现数据并进行交互。

image

此外,你还可以使用 Chrome Remote Debugger Protocal,通过 Node 与 Chrome 调试后台进行交互,并直接控制 Chrome。

Chrome Debugging Protocol

简单来说,远程调试协议就是利用 WebSocket 建立连接 DevTools 和浏览器内核的快速数据通道。那么我们也可以自己打开这个 WebSocket,遵从它的协议来发送消息。

在前面 inspector.html 和 Chrome Host 之间通过 WebSocket 建立连接后,从整个调试过程中的 Websocket 通讯可以看出,这个接口里面有两种通讯模式:Cmmand 和 Event。

request: {"id":1,"method":"Page.canScreencast"}
response: {"id":1,"result":{"result":false}}
{"method":"Network.loadingFinished","params:{"requestId":"14307.143","timestamp":1424097364.31611,"encodedDataLength":0}}

远程调试协议把操作划分为不同的域 domain ,比如

可以理解为 DevTools 中的不同功能模块。每个域(domain)定义了它所支持的 command 和它所产生的 event(就是上面讲的两种通讯方式)。每个 command 包含 request 和 response 两部分,request 部分指定所要进行的操作以及操作说要的参数,response 部分表明操作状态,成功或失败。command 和 event 中可能涉及到非基本数据类型,在 domain 中被归为 Type,比如:'frameId': ,其中 FrameId 为非基本数据类型。

至此,不难理解: domain = command + event + type

image

很多工具都使用了Chrome Debugging Protocol,包括 PhantomJS,Selenium 的 ChromeDriver,本质都是一样的实现,它就相当于 Chrome 内核提供的 API 让应用调用。官网列出了很多有意思的工具:awesome-chrome-devtools/Developing with the protocol,因为 API 丰富,所以才有了这么多的 Chrome 插件。

协议调试

使用 "Protocol Monitor":

image

打开 devTools-on-devTools,然后在内部 DevTools 窗口中,使用 Main。控制台中的 MainImpl.sendOverProtocol():

let Main = await import('./main/main.js');
await Main.MainImpl.sendOverProtocol('Emulation.setDeviceMetricsOverride', {
  mobile: true,
  width: 412,
  height: 732,
  deviceScaleFactor: 2.625,
});

const data = await Main.MainImpl.sendOverProtocol("Page.captureScreenshot");

DevTools 拓展与整合

Chrome DevTools 本身就具备很好的拓展性。如果 DevTools 缺少一个你需要的特性,你可以找一找现成的扩展(extension),或者干脆自己写一个,同时你也可以选择将 DevTools 功能集成到你的应用中。

使用 DevTools 构建自定义解决方案有两种基本方式:

下面的小节将讨论这两种方法。

DevTools Chrome extensions

DevTools UI 是一个嵌入在 Chrome 中的 web 应用程序。 Devtools 扩展使用 Chrome extensions system 为 DevTools 添加功能。DevTools 扩展可以向 DevTools 添加新的面板(panels),向 Elements 和 Sources 面板侧边栏(panel sidebar)添加新的窗格(panes),检查 resources 和 network 事件,以及在被 inspected 的浏览器选项卡(tab)中执行 JavaScript 表达式。

如果你想开发一个 DevTools 扩展:

image

image

有关 DevTools 扩展的实例列表,请参考 Sample DevTools Extensions。 这些示例包括许多可供参考的 Extensions 源码。

Debugging protocol clients

第三方应用程序,如 IDE、编辑器、持续集成框架和测试框架都可以与 Chrome 调试器集成,以调试代码、实时预览代码和 CSS 更改,并控制浏览器。 客户端使用 Chrome debugging protocol 与 Chrome 实例进行交互,该实例既可以在同一个系统上运行,也可以远程运行。

注意:目前,Chrome debugging protocol 每个 Page 只支持一个客户端。 因此,您可以使用 DevTools inspect 页面,或者使用第三方客户端,但两者不能同时 inspect。

有两种方法可以与调试协议集成:

image

有关一些集成示例,请参考:Sample Debugging Protocol Clients

参考

Pines-Cheng commented 4 years ago

主要内容:

  1. 内容脚本(content scripts)和 扩展(extension)
  2. 跨插件通信(chrome.runtime.sendMessage(laserExtensionId,)
  3. 从网页发送信息( chrome.runtime.sendMessage(editorExtensionId,)
  4. Native messaging

Message Passing

由于内容脚本(content scripts)运行在 web 页面的上下文中,而不是在扩展(extension)中,因此它们通常需要某种方式与扩展(extension)的其余部分进行通信。 例如,RSS 阅读器扩展可能使用内容脚本(content scripts)检测页面上是否存在 RSS feed,然后通知后台页面以显示该页面的页面操作图标。

扩展(extension)和它们的内容脚本(content scripts)之间的通信是通过消息传递(message passing)进行的。 任何一方都可以 监听(listen) 从另一端发送的消息,并在同一个通道(channel)上作出响应。 消息可以包含任何有效的 JSON 对象(null、boolean、number、string、array 或 object)。 有一个用于 一次性请求(one-time requests)的简单 API ,还有一个更复杂的 API,它们允许你使用 长连接(long-lived connections) 通过共享上下文(shared context)交换(exchanging)多个消息。 如果您知道另一个扩展的 ID,也可以将消息发送到该扩展,这在 跨插件消息(cross-extension messages)消息部分中有介绍。

另外还有 Sending messages from web pages 以及 Native messaging

Simple one-time requests

如果您只需要向扩展的另一部分发送一条消息(并且可以选择获得回复) ,那么您应该使用简化的 runtime.sendMessagetabs.sendMessage。 这允许您分别将一次性的 JSON-serializable 消息从内容脚本(content script)发送到扩展,或者反之亦然。 可选的回调参数允许您处理来自另一端的响应(如果有的话)。

从内容脚本发送请求如下:

chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
  console.log(response.farewell);
});

从扩展向内容脚本发送请求看起来非常相似。

chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
  chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function(response) {
    console.log(response.farewell);
  });
});

Long-lived connections

有时候,比起单一的请求和响应,有一个持续时间更长的对话是有用的。 在这种情况下,您可以分别使用 runtime.connecttabs.connect 打开从内容脚本(content script)到扩展页面的 long-lived channel,反之亦然。 通道可以有选择地有一个名称,允许您区分不同类型的连接。

下面是如何从内容脚本中打开一个频道,并发送和监听消息:

var port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
  if (msg.question == "Who's there?")
    port.postMessage({answer: "Madame"});
  else if (msg.question == "Madame who?")
    port.postMessage({answer: "Madame... Bovary"});
});

为了处理传入的连接,需要设置 runtime.onConnect 事件侦听器。 从内容脚本或扩展页面来看,这看起来是一样的。

chrome.runtime.onConnect.addListener(function(port) {
  console.assert(port.name == "knockknock");
  port.onMessage.addListener(function(msg) {
    if (msg.joke == "Knock knock")
      port.postMessage({question: "Who's there?"});
    else if (msg.answer == "Madame")
      port.postMessage({question: "Madame who?"});
    else if (msg.answer == "Madame... Bovary")
      port.postMessage({question: "I don't get it."});
  });
});

需要注意 Port lifetime。

Cross-extension messaging

除了在扩展(extension)中的不同组件之间发送消息外,还可以使用消息传递 API 与其他扩展进行通信。 这使您可以公开其他扩展可以利用的公共 API。

侦听传入的请求和连接与内部情况类似,只是使用 runtime.onMessageExternalruntime.onConnectExternal 方法。 下面是每种方法的一个例子:

// For simple requests:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id == blocklistedExtension)
      return;  // don't allow this extension access
    else if (request.getTargetData)
      sendResponse({targetData: targetData});
    else if (request.activateLasers) {
      var success = activateLasers();
      sendResponse({activateLasers: success});
    }
  });

// For long-lived connections:
chrome.runtime.onConnectExternal.addListener(function(port) {
  port.onMessage.addListener(function(msg) {
    // See other examples for sample onMessage handlers.
  });
});

同样,向另一个扩展发送消息类似于在扩展中发送消息。 唯一的区别是,您必须传递要与之通信的扩展的 ID。 例如:

// The ID of the extension we want to talk to.
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// Make a simple request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
  });

// Start a long-running conversation:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

Sending messages from web pages

与跨扩展消息传递(cross-extension messaging,)类似,你的应用程序或扩展程序可以接收和响应来自常规网页的消息。 要使用这个功能,你必须首先在你的 manifest.json 中指定你想要与哪些网站通信。 例如:

"externally_connectable": {
  "matches": ["*://*.example.com/*"]
}

这将向任何与你指定的 URL 模式匹配(patterns matches)的页面公开消息传递 API。 URL 模式(patterns)必须至少包含一个二级域(second-level domain),即主机名模式(hostname patterns),如 “ * ”、“ * .com”、“ *.co.uk” 和 “ *.appspot.com”是禁止的。 在网页上,使用 runtime.sendMessageruntime.connect API 向特定的应用程序或扩展发送消息。 例如:

// The ID of the extension we want to talk to.
var editorExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// Make a simple request:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
  function(response) {
    if (!response.success)
      handleError(url);
  });

从您的应用程序(app)或扩展(extension),您可以通过 runtime.onMessageExternalruntime.onConnectExternal API 监听来自网页的消息,类似于跨扩展消息传递(cross-extension messaging)。 只有网页可以启动连接。 下面是一个例子:

chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.url == blocklistedWebsite)
      return;  // don't allow this web page access
    if (request.openUrlInEditor)
      openUrl(request.openUrlInEditor);
  });

Native messaging

扩展和应用程序可以与注册为 [原生消息主机(native messaging host)] (https://developer.chrome.com/extensions/nativeMessaging#native-messaging-host)的 原生应用(native applications) 交换消息 exchange messages 。 要了解关于此特性的更多信息,请参见 Native messaging

Security considerations

Examples

你可以在 examples/api/messaging 目录中找到通过消息进行通信的简单示例。 native messaging sample演示了 Chrome 应用程序如何与本地应用程序通信。 有关更多示例和查看源代码的帮助,请参见 示例

参考

tidys commented 3 years ago

我想问题,devtools页面要和content注入到webpage的脚本进行交互, 在devtoos页面使用api chrome.devtools.inspectedWindow.eval能够和注入脚本进行通讯 除了这种方式,还有其他的没有?