依据远程调节和测量检验左券的H5品质测量试验解
分类:前端技术

揭秘浏览器远程调试技术

2016/10/19 · 基础技术 · 1 评论 · 浏览器, 调试, 远程

原文出处: 淘宝前端团队(FED) - 肖焉   

图片 1

图片 2

图片 3

简介

chrome-remote-interface是chrome调试协议的第三方调试客户端实现,该项目开源,提供了命令行工具,且为node程序提供了api。
chrome-remote-interface为基于chrome调试协议编写自己的node调试工具提供了便捷的途径,因为利用它,你不需要基于原始的协议通过websocket编程去开发调试工具了。
项目地址https://github.com/cyrus-and/chrome-remote-interface。

调试技术的起源

1947 年 9 月 9 日,一名美国的科学家格蕾丝.霍普和她的同伴在对 Mark II 计算机进行研究的时候发现,一只飞蛾粘在一个继电器上,导致计算机无法正常工作,当他们把飞蛾移除之后,计算机又恢复了正常运转。于是他们将这只飞蛾贴在了他们当时记录的日志上,对这件事情进行了详细的记录,并在日志最后写了这样一句话:First actual case of bug being found。这是他们发现的第一个真正意义上的 bug,这也是人类计算机软件历史上,发现的第一个 bug,而他们找到飞蛾的方法和过程,就是 debugging 调试技术。

图片 4

从格蕾丝调试第一个 bug 到现在,69 年的时间里,在计算机领域,硬件、软件各种调试技术都在不断的发展和演进。那么对于日新月异的前端来说,调试技术也尤其显得重要。淘宝前端团队也正在使用一些创新的技术和手段来解决无线页面调试的问题。今天先跟大家分享下浏览器远程调试技术,本文将用 Chrome/Webview 来作为案例。

立体

Node

使用命令行

调试原理

前言

相信有很多人和我一样,习惯了使用chrome调试js程序,然而node刚开始提供的调试方式只用Debugger,只能通过node --debug xxx.js启动命令行调试工具,及其的不方便。当然也有一些插件在此基础上,使用websocket进行通信,使其可以在chrome浏览器中调试。体验和直接在chrome上进行调试还是差了很多。

安装

通过npm进行安装

npm install chrome-remote-interface

调试方式与权限管理

图片 5

目前常规浏览器调试目标分为两种:Chrome PC 浏览器和 Chrome Mobile(Android 4.4 以后,Android WebView 其实就是 Chromium WebView)。

为了丰富动态性,移动app一般会嵌入H5页面来实现实时操作,例如:支付,运营活动等。H5技术在扩展app功能方面占据重要地位。对于一个H5的产品,除了其实现功能是重点外,性能同样是用户体验中不可或缺的一环。H5性能的测试工具一般分为两类,一类是抓包工具,如Fiddler、Charles等;另一类是平台型工具,如PageSpeed、Chrome Devtools等。抓包类工具对于数据的展示不够直观,操作比较复杂。本文通过探索平台型工具的Chrome Devtools的实现机制,提供了基于远程调试协议的H5性能测试解决方案。

然而在Node v7.x.x后,Node有提供了一个Inspector,可以直接和Chrome DevTools进行通信,下面来详细介绍进入调试的步骤,以及在使用过程中,我遇到的问题及解决办法。

启动调试目标

chrome-remote-interface基于chrome调试协议,因此其支持调试chrome浏览器和node运行环境。
无论哪种调试目标,其启动时都应该指定调试端口。
如果要调试chrome浏览器,应该在启动chrome时添加--remote-debugging-port参数,如下:

goole-chrome --remote-debugging-port=9222

如果调试node,在启动时添加--inspect参数,如下:

node --inspect=9222 app.js

此时,node会输出:

Debugger listening on port 9222.
Warning: This is an experimental feature and could change at any time.
To start debugging, open the following URL in Chrome:
    chrome-devtools://devtools/remote/serve_file/@60cd6e859b9f557d2312f5bf532f6aec5f284980/inspector.html?experiments=true&v8only=true&ws=localhost:9222/2894362d-f2d1-4f3b-a9e8-4e27da5714ef

题外话:如果只是希望调试node,并不打算开发一个调试node的工具的话,根据提示中的url,在chrome中打开就直接可以用chrome的开发工具调试了。

Chrome PC 浏览器

对于调试 Chrome PC 浏览器,可能大家经常使用的是用鼠标右键或者快捷方式(mac:option command

  • J),唤起 Chrome 的控制台,来对当前页面进行调试。其实还有另外一种方法,就是使用一个 Chrome 浏览器调试另一个 Chrome 浏览器。Chrome 启动的时候,默认是关闭了调试端口的,如果要对一个目标 Chrome PC 浏览器进行调试,那么启动的时候,可以通过传递参数来开启 Chrome 的调试开关:

JavaScript

# for mac sudo /Applications/Google Chrome.app/Contents/MacOS/Google Chrome --remote-debugging-port=9222

1
2
# for mac
sudo /Applications/Google Chrome.app/Contents/MacOS/Google Chrome --remote-debugging-port=9222

远程调试协议

目录

  1. 具体调试步骤详细介绍
  2. 问题及解决方法
  3. 其他工具
  4. 相关文档

体验命令行

  • 查看全部命令

    在终端输入chrome-remote-interface并回车,可看到如下输出:

Usage: chrome-remote-interface [options] [command]

Commands:

inspect [options] [<target>] inspect a target (defaults to the current tab)
list                   list all the available tabs
new [<url>]            create a new tab
activate <id>          activate a tab by id
close <id>             close a tab by id
version                show the browser version
protocol [options]     show the currently available protocol descriptor

Options:

-h, --help         output usage information
-t, --host <host>  HTTP frontend host
-p, --port <port>  HTTP frontend port
```

其中,new和close是针对浏览器的tab的命令,不要针对node来运行。
  • 查看所有页面实例

    chrome-remote-interface -t 127.0.0.1 -p 9222 list

    输出如下:

    [ { description: 'node.js instance',
    devtoolsFrontendUrl: 'https://chrome-devtools-frontend.appspot.com/serve_file/@60cd6e859b9f557d2312f5bf532f6aec5f284980/inspector.html?experiments=true&v8only=true&ws=localhost:9222/2894362d-f2d1-4f3b-a9e8-4e27da5714ef',
    faviconUrl: 'https://nodejs.org/static/favicon.ico',
    id: '2894362d-f2d1-4f3b-a9e8-4e27da5714ef',
    title: 'app.js',
    type: 'node',
    url: 'file:///Users/renbaogang/git/enzyme.node/app.js',
    webSocketDebuggerUrl: 'ws://localhost:9222/2894362d-f2d1-4f3b-a9e8-4e27da5714ef' } ]
    

    其中devtoolsFrontendUrl可以在chrome地址栏中回车该url,打开chrome的开发工具,可以直接用该工具调试。和启动node时,node提示的url是一个用途。
    id是当前页面标识。
    url是当前页面url。
    webSocketDebuggerUrl是node提供的ws协议的调试服务,调试客户端需要通过该url连接到调试服务。

  • 查看调试目标版本

    chrome-remote-interface -t 127.0.0.1 -p 9222 version

    输出:

    [ { Browser: 'node.js/v7.0.0', 'Protocol-Version': '1.1' } ]
    
  • 查看调试目标支持的调试协议

    chrome-remote-interface -t 127.0.0.1 -p 9222 protocol

    输出:

    { 
    remote: false,
    descriptor: {
        { version: { major: '1', minor: '2' },
        domains:[略]
    }
    }
    

    remote:false说明协议内容是工具自带的协议文件,并不是来自调试目标。
    如果要获取调试目标的协议内容要添加-r选项,如:

    chrome-remote-interface -t 127.0.0.1 -p 9222 protocol -r

    输出:

    { 
    remote: true,
    descriptor: {
        { version: { major: '1', minor: '1' },
        domains:[略]
    }
    }
    
  • 调试命令

    chrome-remote-interface -t 127.0.0.1 -p 9222 inspect -r
    -r 表示应用调试目标提供的协议描述文件
    通过inspect可以调试node或chrome所支持的各个域。

    Console域体验

Console.enable()
{ result: {} }
Console.clearMessages()
{ result: {} }
```

Debugger域体验

```

Debugger.enable()
{ result: {} }
Debugger.setBreakpointsActive({active:true})
{ result: {} }
```

HeapProfiler域体验

```

HeapProfiler.enable()
{ result: {} }
HeapProfiler.startTrackingHeapObjects({trackAllocations:true})
{ result: {} }
```

Profiler域体验

```

Profiler.enable()
{ result: {} }
Profiler.start()
{ result: {} }
Profiler.stop()
{ result:
{ profile:
{ nodes:
[ { id: 1,
callFrame:
{ functionName: '(root)',
scriptId: '0',
url: '',
lineNumber: -1,
columnNumber: -1 },
hitCount: 0,
children: [ 2 ] },
...

      ]
    }
 }

}
```

Runtime域体验  

>>> Runtime.evaluate({expression:"1 1"})
{ result: { result: { type: 'number', value: 2, description: '2' } } }

Schema域体验

>>> Schema.getDomains()
{ result: 
   { domains: 
      [ { name: 'Runtime', version: '1.1' },
        { name: 'Debugger', version: '1.1' },
        { name: 'Profiler', version: '1.1' },
        { name: 'HeapProfiler', version: '1.1' },
        { name: 'Schema', version: '1.1' } ] } }
    ```

    体验事件处理

    scriptParsed是Debugger域提供的脚本解析事件

    ```
>>> Debugger.scriptParsed(params=>params.url)
{ 'Debugger.scriptParsed': 'params=>params.url' }
{ 'Debugger.scriptParsed': '' }
{ 'Debugger.scriptParsed': '/Users/renbaogang/git/enzyme.node/node_modules/negotiator/lib/encoding.js' }
    ```

## API ##
1. module([options], [callback])

    基于chrome调试协议连接调试目标

    options object类型,具有如下属性:  
    - host 默认localhost
    - port 默认9222
    - chooseTab 决定调试哪个tab。该参数可以为三种类型:
        - function 提供一个返回tab序号的函数
        - object 正如new 和 list返回的对象一样
        - string websocket url
        默认为一个函数,返回当前激活状态的tab的序号,如:function(tabs){return 0;}
    - protocol 协议描述符,默认由remote选项来定是否使用调试目标提供的协议描述符
    - remote 默认false,如果protocol设置了,该选项不会起作用

  callback   
    如果callback是函数类型,则该函数在connect事件发生后会得到回调,并返回EventEmitter对象,如果不是,则返回Promise对象。

    EventEmitter对象支持如下事件:  

    - connect

        ```
    function (chrome) {}
    ```
    当websocket连接建立后触发,chrome是Chome类型的实例。

    - disconnect

        ```
        function () {}
        ```
        关闭websocket连接时触发

    - error

        ```
        function (err) {}
        ```
    当通过host:port/json无法到达,或websocket连接无法建立时触发  
        err,Error类型的错误对象

  使用样例,针对chrome浏览器:

    ```
const Chrome = require('chrome-remote-interface');
Chrome(function (chrome) {
    with (chrome) {
        Network.requestWillBeSent(function (params) {
            console.log(params.request.url);
        });
        Page.loadEventFired(function () {
            close();
        });
        Network.enable();
        Page.enable();
        once('ready', function () {
            Page.navigate({'url': 'https://github.com'});
        });
    }
}).on('error', function (err) {
    console.error('Cannot connect to Chrome:', err);
});
    ```

2. module.Protocol([options],[callback])

    获取chrome调试协议描述符

    options object类型,具有如下属性:  
    - host 默认localhost
    - port 默认9222
    - remote 默认false

    callback 回调函数,在获取到协议内容后调用,函数接收如下参数:  
    - err Error一个错误对象,如果有错误的话
    - protocol 包含以下属性
        - remote 是否远程协议
        - descriptor chrome调试协议描述符

  使用样例:

    ```
const Chrome = require('chrome-remote-interface');
Chrome.Protocol(function (err, protocol) {
    if (!err) {
        console.log(JSON.stringify(protocol.descriptor, null, 4));
    }
});
    ```

3. module.List([options], [callback])

    获取所有的tabs,当然在node中只会有一个  

    options object类型,具有如下属性:  
    - host 默认localhost
    - port 默认9222
    - remote 默认false

    callback 回调函数,在获取到tabs内容后调用,函数接收如下参数:  
    - err Error一个错误对象,如果有错误的话
    - tabs 获取到的tab数组

  使用样例:

    ```
const Chrome = require('chrome-remote-interface');
Chrome.List(function (err, tabs) {
    if (!err) {
        console.log(tabs);
    }
});
    ```

4. module.New([options], [callback])

    创建新的tab

    options object类型,具有如下属性:  
    - host 默认localhost
    - port 默认9222
    - url 新tab加载的url 默认about:blank

    callback 回调函数,在新tab创建后调用,函数接收如下参数:  
    - err Error一个错误对象,如果有错误的话
    - tab 新增的tab

  使用样例:  

    ```
const Chrome = require('chrome-remote-interface');
Chrome.New(function (err, tab) {
    if (!err) {
        console.log(tab);
    }
});
  1. module.Activate([options], [callback])

    激活指定tab

    options object类型,具有如下属性:

    • host 默认localhost
    • port 默认9222
    • id 目标tab的id

    callback 回调函数,在新tab创建后调用,函数接收如下参数:

    • err Error一个错误对象,如果有错误的话

使用样例:

```

const Chrome = require('chrome-remote-interface');
Chrome.Activate({'id': 'CC46FBFA-3BDA-493B-B2E4-2BE6EB0D97EC'}, function (err) {
if (!err) {
console.log('success! tab is closing');
}
});
```

  1. module.Close([options], [callback])

    关闭指定tab

    options object类型,具有如下属性:

    • host 默认localhost
    • port 默认9222
    • id 目标tab的id

    callback 回调函数,在新tab创建后调用,函数接收如下参数:

    • err Error一个错误对象,如果有错误的话

使用样例:

```

const Chrome = require('chrome-remote-interface');
Chrome.Close({'id': 'CC46FBFA-3BDA-493B-B2E4-2BE6EB0D97EC'}, function (err) {
if (!err) {
console.log('success! tab is closing');
}
});
```

  1. module.Version([options], [callback])

    获取版本信息

    options object类型,具有如下属性:

    • host 默认localhost
    • port 默认9222

    callback 回调函数,在新tab创建后调用,函数接收如下参数:

    • err Error一个错误对象,如果有错误的话
    • info json object

使用样例:

```

const Chrome = require('chrome-remote-interface');
Chrome.Version(function (err, info) {
if (!err) {
console.log(info);
}
});
```

  1. Chrome 类

    支持的事件:

    • event

      在远程调试目标发送通知时触发,一般是远程对象执行了客户端提交的方法后

      function (message) {}
      

      message包含如下属性:

      • method 通知内容,方法名 如:'Network.requestWillBeSent'
      • params 参数内容

    使用样例:

     ```
     chrome.on('event', function (message) {
         if (message.method === 'Network.requestWillBeSent') {
             console.log(message.params);
         }   
     });
     ```
    
    • <method>

      调试目标通过websocket发送了一个指定方法名的通知

      function (params) {}
      

      使用样例:

      chrome.on('Network.requestWillBeSent', console.log);
      
    • ready

      每次没有调试命令等待调试目标返回时触发

      function () {}
      

      使用样例:

      只在Network和Page激活后加载一个url

      chrome.Network.enable();
      chrome.Page.enable();
      chrome.once('ready', function () {
          chrome.Page.navigate({'url': 'https://github.com'});
      });
      

支持的方法

  • chrome.send(method, [params], [callback])

      发送一个调试命令
    
      method 命令名称  
      params 参数  
      callback 当远程对象对该命令发送一个应答后调用,函数具有以下参数:  
      - error boolean 是否成功
      - response 如果error===true,返回一个error对象,{error:...},否则返回一个应答,{result:...}
    
      注意:在chrome调试规范里提到的id字段,在这里被内部管理不会暴露给用户
    

    使用样例:

      ```
      chrome.send('Page.navigate', {'url': 'https://github.com'}, console.log);
      ```
    
    • chrome..([params], [callback])

      是chrome.send('.', params, callback);的一种变形

      例如:

        chrome.Page.navigate({'url': 'https://github.com'}, console.log);
      
    • chrome..(callback)

      是chrome.on('.', callback)的变形

      例如:

        chrome.Network.requestWillBeSent(console.log);
      
    • chrome.close([callback])

      关闭与远程调试目标的连接
      callback会在websocket关闭成功后调用

Chrome Android 浏览器

对于调试 Android 上的 Chrome 或者 WebView 需要连接 USB 线。打开调试端口的方法如下:

JavaScript

adb forward tcp:9222 localabstract:chrome_devtools_remote

1
adb forward tcp:9222 localabstract:chrome_devtools_remote

跟 Chrome PC 浏览器不同的是,对于 Chrome Android 浏览器,由于数据传输是通过 USB 线而不是 WIFI,实际上 Chrome Android 创建的一个 chrome_devtools_remote 这个 path 的 domain socket。所以,上面一条命令则是通过 Android 的 adb 将 PC 的端口 9222 通过 USB 线与 chrome_devtools_remote 这个 domain socket 建立了一个端口映射。

Chrome Devtools是通过远程调试协议(Remote debugging protocol)与浏览器页面(pages)交互和调试。采用webSocket来与页面建立通信通道,由发送给页面的commands和它所产生的events组成。这个协议是开放的,第三方开发者也可以调用这个协议来与页面交互调试。

具体调试步骤详细介绍

权限管理

Google 为了限制调试端口的接入范围,对于 Chrome PC 浏览器,调试端口只接受来自 127.0.0.1 或者 localhost 的数据请求,所以,你无法通过你的本地机器 IP 来调试 Chrome。对于 Android Chrome/WebView,调试端口只接受来自于 shell 这个用户数据请求,也就是说只能通过 USB 进行调试,而不能通过 WIFI。

WebSocket通信协议介绍

1. Chrome DevTools和Node版本要求

  1. Chrome DevTools: 55
    在地址栏中输入chrome://settings/help,查看Chrome版本

    图片 6

    Chroem版本

  2. Node.js: v7.x.x
    在命令行中输入node --version进行查看

    图片 7

    Node.js版本

开始调试

通过以上的调试方式的接入以及调试端口的打开,这个时候在浏览器中输入:

JavaScript

1
http://127.0.0.1:9222/json

将会看到类似下面的内容:

JavaScript

[ { "description": "", "devtoolsFrontendUrl": "/devtools/inspector.html?ws=127.0.0.1:9222/devtools/page/ebdace60-d482-4340-b622-a6198e7aad6e", "id": "ebdace60-d482-4340-b622-a6198e7aad6e", "title": "揭秘浏览器远程调试技术.mdown—/Users/harlen/Documents", "type": "page", "url": "", "webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/ebdace60-d482-4340-b622-a6198e7aad6e" } ]

1
2
3
4
5
6
7
8
9
10
11
[
  {
    "description": "",
    "devtoolsFrontendUrl": "/devtools/inspector.html?ws=127.0.0.1:9222/devtools/page/ebdace60-d482-4340-b622-a6198e7aad6e",
    "id": "ebdace60-d482-4340-b622-a6198e7aad6e",
    "title": "揭秘浏览器远程调试技术.mdown—/Users/harlen/Documents",
    "type": "page",
    "url": "http://127.0.0.1:51004/view/61",
    "webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/ebdace60-d482-4340-b622-a6198e7aad6e"
  }
]

其中,最重要的 2 个参数分别是 id 和 webSocketDebuggerUrl。Chrome 会为每个页面分配一个唯一的 id,作为该页面的唯一标识符。几乎对目标浏览器的所有操作都是需要带上这个 id。

Chrome 提供了以下这些 http 接口控制目标浏览器

JavaScript

# 获取当前所有可调式页面信息 # 获取调试目标 WebView/blink 的版本号 # 创建新的 tab,并加载 url # 关闭 id 对应的 tab

1
2
3
4
5
6
7
8
9
10
11
# 获取当前所有可调式页面信息
http://127.0.0.1:9222/json
 
# 获取调试目标 WebView/blink 的版本号
http://127.0.0.1:9222/json/version
 
# 创建新的 tab,并加载 url
http://127.0.0.1:9222/json/new?url
 
# 关闭 id 对应的 tab
http://127.0.0.1:9222/json/close/id

webSocketDebuggerUrl 则在调试该页面需要用到的一个 WebSocket 连接。chrome 的 devtool 的所有调试功能,都是基于 Remote Debugging Protocol 使用 WebSocket 来进行数据传输的。那么这个 WebSocket,就是上面我们从 http://127.0.0.1:9222/json 获取的 webSocketDebuggerUrl,每一个页面都有自己不同的 webSocketDebuggerUrl。这个 webSocketDebuggerUrl是通过 url 的 query 参数传递给 chrome devtool 的。

chrome 的 devtool 可以从 Chrome 浏览器中进行提取 devtool 源码或者从 blink 源码中获取。在部署好自己的 chrome devtool 代码之后,下面既可以开始对 Chrome 进行调试, 浏览器输入一下内容:

JavaScript

1
http://path_to_your_devtool/devtool.html?ws=127.0.0.1:9222/devtools/page/ebdace60-d482-4340-b622-a6198e7aad6e

其中 ws 这个参数的值就是上面出现的 webSocketDebuggerUrl。Chrome 的 devtool 会使用这个 url 创建 WebSocket 对该页面进行调试。

WebSocket Protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信。HTTP通信协议中,服务器需要接受到客服端的请求后被动作出响应。不同于传统通信协议,WebSocket协议在建立第一次握手协议后,服务器不需要等待请求在作出响应,而是在有新数据时就主动推送给服务端,实现了客服端与服务器,客户端与客户端之间的实时通信,更好的节省了服务器资源和带宽。

2. 运行脚本,并访问调试页面

总体步骤:先根据具体情况,选用不同的参数运行脚本;然后访问相应的url获取调试页面的访问地址;最后访问这个地址,进入调试页面。

在命令行中运行相应脚本,使用--inspect,或者--inspect-brk开启调试开关,如node --inspect path/xxx.js 或者node --inspect-brk path/xxx.js。下面根据不同情况进行具体分析。

如何实现 JavaScript 调试

在进入 Chrome 的 devtool 之后,我们可以调出控制台,来查看 devtool 的 WebSocket 数据。这个里面有很多数据,我这里只讲跟 JavaScript 调试相关的。
图片 8

图中,对于 JavaScript 调试,有一条非常重要的消息,我蓝色选中的那条消息:

JavaScript

{"id":6,"method":"Debugger.enable"}

1
{"id":6,"method":"Debugger.enable"}

然后选中要调试的 JavaScript 文件,然后设置一个断点,我们再来看看 WebSocket 消息:
图片 9

devtool 像目标 Chrome 发送了 2 条消息

JavaScript

{ "id": 23, "method": "Debugger.getScriptSource", "params": { "scriptId": "103" } }

1
2
3
4
5
6
7
{
  "id": 23,
  "method": "Debugger.getScriptSource",
  "params": {
    "scriptId": "103"
  }
}

JavaScript

{ "id": 24, "method": "Debugger.setBreakpointByUrl", "params": { "lineNumber": 2, "url": "", "columnNumber": 0, "condition": "" } }

1
2
3
4
5
6
7
8
9
10
{
  "id": 24,
  "method": "Debugger.setBreakpointByUrl",
  "params": {
    "lineNumber": 2,
    "url": "https://g.alicdn.com/alilog/wlog/0.2.10/??aplus_wap.js,spm_wap.js,spmact_wap.js",
    "columnNumber": 0,
    "condition": ""
  }
}

那么收到这几条消息之后,V8 做了些什么呢?
我们先来简单的看下 V8 里面的一小段源码片段:

JavaScript

// V8 Debugger.cpp DispatcherImpl(FrontendChannel* frontendChannel, Backend* backend) : DispatcherBase(frontendChannel), m_backend(backend) { m_dispatchMap["Debugger.enable"] = &DispatcherImpl::enable; m_dispatchMap["Debugger.disable"] = &DispatcherImpl::disable; m_dispatchMap["Debugger.setBreakpointsActive"] = &DispatcherImpl::setBreakpointsActive; m_dispatchMap["Debugger.setSkipAllPauses"] = &DispatcherImpl::setSkipAllPauses; m_dispatchMap["Debugger.setBreakpointByUrl"] = &DispatcherImpl::setBreakpointByUrl; m_dispatchMap["Debugger.setBreakpoint"] = &DispatcherImpl::setBreakpoint; m_dispatchMap["Debugger.removeBreakpoint"] = &DispatcherImpl::removeBreakpoint; m_dispatchMap["Debugger.continueToLocation"] = &DispatcherImpl::continueToLocation; m_dispatchMap["Debugger.stepOver"] = &DispatcherImpl::stepOver; m_dispatchMap["Debugger.stepInto"] = &DispatcherImpl::stepInto; m_dispatchMap["Debugger.stepOut"] = &DispatcherImpl::stepOut; m_dispatchMap["Debugger.pause"] = &DispatcherImpl::pause; m_dispatchMap["Debugger.resume"] = &DispatcherImpl::resume; m_dispatchMap["Debugger.searchInContent"] = &DispatcherImpl::searchInContent; m_dispatchMap["Debugger.setScriptSource"] = &DispatcherImpl::setScriptSource; m_dispatchMap["Debugger.restartFrame"] = &DispatcherImpl::restartFrame; m_dispatchMap["Debugger.getScriptSource"] = &DispatcherImpl::getScriptSource; m_dispatchMap["Debugger.setPauseOnExceptions"] = &DispatcherImpl::setPauseOnExceptions; m_dispatchMap["Debugger.evaluateOnCallFrame"] = &DispatcherImpl::evaluateOnCallFrame; m_dispatchMap["Debugger.setVariableValue"] = &DispatcherImpl::setVariableValue; m_dispatchMap["Debugger.setAsyncCallStackDepth"] = &DispatcherImpl::setAsyncCallStackDepth; m_dispatchMap["Debugger.setBlackboxPatterns"] = &DispatcherImpl::setBlackboxPatterns; m_dispatchMap["Debugger.setBlackboxedRanges"] = &DispatcherImpl::setBlackboxedRanges; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// V8 Debugger.cpp
DispatcherImpl(FrontendChannel* frontendChannel, Backend* backend) : DispatcherBase(frontendChannel), m_backend(backend) {
    m_dispatchMap["Debugger.enable"] = &DispatcherImpl::enable;
    m_dispatchMap["Debugger.disable"] = &DispatcherImpl::disable;
    m_dispatchMap["Debugger.setBreakpointsActive"] = &DispatcherImpl::setBreakpointsActive;
    m_dispatchMap["Debugger.setSkipAllPauses"] = &DispatcherImpl::setSkipAllPauses;
    m_dispatchMap["Debugger.setBreakpointByUrl"] = &DispatcherImpl::setBreakpointByUrl;
    m_dispatchMap["Debugger.setBreakpoint"] = &DispatcherImpl::setBreakpoint;
    m_dispatchMap["Debugger.removeBreakpoint"] = &DispatcherImpl::removeBreakpoint;
    m_dispatchMap["Debugger.continueToLocation"] = &DispatcherImpl::continueToLocation;
    m_dispatchMap["Debugger.stepOver"] = &DispatcherImpl::stepOver;
    m_dispatchMap["Debugger.stepInto"] = &DispatcherImpl::stepInto;
    m_dispatchMap["Debugger.stepOut"] = &DispatcherImpl::stepOut;
    m_dispatchMap["Debugger.pause"] = &DispatcherImpl::pause;
    m_dispatchMap["Debugger.resume"] = &DispatcherImpl::resume;
    m_dispatchMap["Debugger.searchInContent"] = &DispatcherImpl::searchInContent;
    m_dispatchMap["Debugger.setScriptSource"] = &DispatcherImpl::setScriptSource;
    m_dispatchMap["Debugger.restartFrame"] = &DispatcherImpl::restartFrame;
    m_dispatchMap["Debugger.getScriptSource"] = &DispatcherImpl::getScriptSource;
    m_dispatchMap["Debugger.setPauseOnExceptions"] = &DispatcherImpl::setPauseOnExceptions;
    m_dispatchMap["Debugger.evaluateOnCallFrame"] = &DispatcherImpl::evaluateOnCallFrame;
    m_dispatchMap["Debugger.setVariableValue"] = &DispatcherImpl::setVariableValue;
    m_dispatchMap["Debugger.setAsyncCallStackDepth"] = &DispatcherImpl::setAsyncCallStackDepth;
    m_dispatchMap["Debugger.setBlackboxPatterns"] = &DispatcherImpl::setBlackboxPatterns;
    m_dispatchMap["Debugger.setBlackboxedRanges"] = &DispatcherImpl::setBlackboxedRanges;
}

你会发现,V8 有 m_dispatchMap 这样一个 Map。专门用来处理所有 JavaScript 调试相关的处理。
其中就有本文即将重点讲述的:

  • Debuggger.enable
  • Debugger.getScriptSource
  • setBreakpointByUrl

这些都需要在 V8 的源码中找到答案。顺便给大家推荐一个查看 Chromium/V8 最正确的方式是使用 https://cs.chromium.org,比 SourceInsight 还要方便。

一张图带你体会高效的WebSocket Protocol:

情况1

首先启动脚本,如果你的脚本搭建http或者net服务器,你可以直接使用--inspect。如

let net = require('net');

// 创建一个net服务器
const server = net.createServer();

// 连接回调函数
server.on('connection', (conn) => {
    console.log('connection');
});

// 监听8080端口
server.listen(8080);

// 监听回调函数
server.on('listening', () => {
    console.log('listening')
});

图片 10

使用--inspect后显示结果

注:

  1. 上图是在v8.9.1版本时显示的结果,后面会一次列举出其他版本下的结果。
  2. Debugger listening on ws://127.0.0.1:9229/890b7b49-c744-4103-b0cd-6c5e8036be95,其中给定的url并不是提供给我们在Chrome浏览器中访问的地址,而是Node.js和Chrome之间进行通信的的地址,它们通过websocket通过指定的端口进行通信,从而将调试结果实时展示在Chrome浏览器中。

然后,访问http://IP:port/json/list(其中IP就是主机的IP地址,通常为127.0.0.1,port则是端口号,默认为9229,这是Node提供给任意调试工具链接调试的协议,可以通过命令行参数进行修改参数文档地址),会返回相应http请求的元数据,包括WebSocket URL,UUID,Chrome DevTools URL。其中,WebSocket URL就是Node.js和Chrome之间的通信地址;UUID是一个特定的标识,每一个进程都会分配一个uuid,因此每一次调用会有出现不同的结果;Chrome DevTools URL就是我们的调试页面的url。

如访问http://127.0.0.1:9229/json/list,我们将会得到一下结果,其中devtoolsFrontendUrl就是我们需要访问的地址。

图片 11

屏幕快照 2017-11-22 14.34.13.png

最后,访问这个地址后显示页面:

图片 12

调试页面

Debugger.enable

JavaScript

void V8Debugger::enable() { if (m_enableCount ) return; DCHECK(!enabled()); v8::HandleScope scope(m_isolate); v8::Debug::SetDebugEventListener(m_isolate, &V8Debugger::v8DebugEventCallback, v8::External::New(m_isolate, this)); m_debuggerContext.Reset(m_isolate, v8::Debug::GetDebugContext(m_isolate)); compileDebuggerScript(); }

1
2
3
4
5
6
7
8
9
void V8Debugger::enable() {
    if (m_enableCount ) return;
    DCHECK(!enabled());
    v8::HandleScope scope(m_isolate);
    v8::Debug::SetDebugEventListener(m_isolate, &V8Debugger::v8DebugEventCallback,
    v8::External::New(m_isolate, this));
    m_debuggerContext.Reset(m_isolate, v8::Debug::GetDebugContext(m_isolate));
    compileDebuggerScript();
}

这个接口的名称叫 Debugger.enable,但是收到这条消息,V8 其实就干了两件事情事情:

  • SetDebugEventListener:
    给 JavaScript 调试安装监听器,并设置 v8DebugEventCallback 这个回调函数。JavaScript 所有的调试事件,都会被这个监听器捕获,包括:JavaScript 异常停止,断点停止,单步调试等等。
  • compileDebuggerScript:
    编译 V8 内置的 JavaScript 文件 debugger-script.js。由于这文件比较长,我这里就不贴出来了,感兴趣的同学点击这个链接进行查看源码。debugger-script.js 主要是定义了一些针对 JavaScript 断点进行操作的函数,例如设置断点、查找断点以及单步调试相关的函数。那么这个 debugger-script.js 文件,被 V8 进行编译之后,保存在 global 对象上,等待对 JavaScript 进行调试的时候,被调用。

    #### Debugger.getScriptSource

    在 Chrome 解析引擎解析到 ` 标签之后,Chrome 将会把 script 标签对应的 JavaScript 源码扔给 V8 编译执行。同时,V8 将会对所有的 JavaScript 源码片段进行编号并保存。所以,当 chrome devtool 需要获取要调试的 JavaScript 文件的时候,只需要通过Debugger.getScriptSource,给 V8 传递一个 scriptId,V8 将会把 JavaScript 源码返回。我们再回头看看这个图中的消息: ![](http://jbcdn2.b0.upaiyun.com/2016/10/cc8205b6b73c6aa787046a0a6c634ae7.png) 上面 id 为 23 的scriptSource` 就是 V8 返回的 JavaScript 源码,如此以来,我们就可以在 devtool 中看到我们要调试的 JavaScript 源码了。

图片 13

情况2(重要)

如果你的脚本运行完之后直接结束进程,那么你需要使用--inspect-brk来启动调试器,这样使得脚本可以代码执行之前break,否则,整个代码直接运行到代码结尾,结束进程,根本无法进行调试。如:

function sayHi(name) {
    console.log('Hello, '   name);
}

sayHi('yyp');
  1. 使用--inspect:

    图片 14

    直接使用--inspect

直接使用--inspect,代码执行完毕后,进程直接结束,因此没有办法在进行调试。

  1. 使用--inspect-brk:

    图片 15

    使用--inspect-brk

图片 16

访问127.0.0.1:9229

图片 17

调试页面

从上面三张图中可以看出,此时该Node进程并没有直接退出,并且我们可以通过访问http://127.0.0.1:9229/json/list获取页面url,并且在调试页面中我们也发现,程序在第一行break。

细心的你,可能还发现,与我们所写的程序不同的是,调试页面中,将我们的程序包裹在一个函数中,因为浏览器中不存在相应的对象(实际原因可以自行去研究)。

Debugger.setBreakpointByUrl

所有准备工作都做好了,现在就可以开始设置断点了。从上面的几个图中,已经可以很清楚的看到,Debugger.setBreakpointByUrl 给目标 Chrome 传递了一个 JavaScript 的 url 和断点的行号。

首先,V8 会去找,是否已经存在了该 URL 对应的 JavaScript 源码了:

JavaScript

for (const auto& script : m_scripts) { if (!matches(m_inspector, script.second->sourceURL(), url, isRegex)) continue; std::unique_ptr<protocol::Debugger::Location> location = resolveBreakpoint( breakpointId, script.first, breakpoint, UserBreakpointSource); if (location) (*locations)->addItem(std::move(location)); } *outBreakpointId = breakpointId;

1
2
3
4
5
6
7
8
9
for (const auto& script : m_scripts) {
  if (!matches(m_inspector, script.second->sourceURL(), url, isRegex))
    continue;
  std::unique_ptr<protocol::Debugger::Location> location = resolveBreakpoint(
    breakpointId, script.first, breakpoint, UserBreakpointSource);
  if (location) (*locations)->addItem(std::move(location));
}
 
*outBreakpointId = breakpointId;

V8 给所有的断点,创建一个 breakpointObject。并将这些 braekpointObject 以 的形式存放在一个 Map 里面,而这个 Key,就是这个 JavaScript 文件的 URL。看到这里,已经可以解释很多同学在调试 JavaScript 遇到的一个问题:,>

有些同学为了防止页面的 JavaScript 文件不更新,对于一些重要的 JavaScript 文件的 URL 添加访问时间戳,对于这些添加了访问时间戳的 JavaScript 文件进行设置断点然后刷新调试的时候,Chrome 会打印一个 warnning,告诉你断点丢失。

原因很简单,在调试的时候,V8 发现这个 breakpointMap 里面找不到对应的 breakpointObject,因为 URL 发生了变化,这个 brakpointObject 就丢失了,所以 V8 就找不到了,无法进行断点调试。

根据我们的正常思维,你可能会认为 V8 会将断点设置在 C 中,其实一开始我也是这么认为。随着对 V8 的探索,让我看到了我时曾相识的一些函数名:

JavaScript

v8::Local<v8::Function> setBreakpointFunction = v8::Local<v8::Function>::Cast( m_debuggerScript.Get(m_isolate) ->Get(context, toV8StringInternalized(m_isolate, "setBreakpoint")) .ToLocalChecked()); v8::Local<v8::Value> breakpointId = v8::Debug::Call(debuggerContext(), setBreakpointFunction, info) .ToLocalChecked();

1
2
3
4
5
6
7
v8::Local<v8::Function> setBreakpointFunction = v8::Local<v8::Function>::Cast(
    m_debuggerScript.Get(m_isolate)
    ->Get(context, toV8StringInternalized(m_isolate, "setBreakpoint"))
      .ToLocalChecked());
v8::Local<v8::Value> breakpointId =
  v8::Debug::Call(debuggerContext(), setBreakpointFunction, info)
    .ToLocalChecked();

其中,m_debuggerScript,就是我前面提到的 debugger-script.js。随着对 V8 Debugger 的进一步探索,我发现,V8 实际上对这个对这个 breakpointObject 设置了 2 次。一次是通过在 C 中调用 m_debuggerScript 的 setBreakpoint 设置到 JavaScript 的 context 里面,也就是上面这段 C 逻辑做的事情。另一次是,m_debuggerScript 反过来将断点信息设置到了 V8 的 C Runtime 中,为要调试的 JavaScript 的某一行设置一个 JavaScript 的回调函数。

以下是WebSocket Protocol的抓包实例:

其他工具

node-inspector
在使用这个工具之前,我一直在使用node-inspector,只需要使用npm install -g node-inspector安装node-inspector命令行工具,然后在使用node-inspector path/xxx.js即可启动调试。
不足之处:调试体验比较差,相对Chrome DevTools使用,感觉不够流畅,并且在较高版本中,debugger被废弃(v7.7.0 ),进而无法使用,可能会报错(v8.9.1版本直接报错)。

vsCode webStorm
提供直接调试功能(未使用过)。

断点命中

由于 V8 对 JavaScript 是及时编译执行的,没有生成 bytecode,而是直接生成的 machine code 执行的,所以这个断点回调函数也会被设置到这个 machine code 里面。

最终触发断点事件,也是 V8 的 C Runtime。当用户刷新或者直接执行 JavaScript 的逻辑的时候,实际上是 V8 C Runtime 在运行 JavaScript 片段产生的 machine code,这个 machine code 已经包含了断点回调函数了。一旦这个 machine code 里面的回调函数被触发,接着就会触发之前 Debugger.enable 设置的调试事件监听器 DebugEventListener 的回调函数。并返回一条消息给 Chrome 的 devtool,告诉 Chrome devtool,当前 JavaScript 被 pause 的行号。到此为止,一个断点就被命中了。

关于 JavaScript 断点命中,其实是一个很复杂的过程。后面有时间的话,会专门讲讲 JavaScript 断点命中的详细逻辑。

图片 18

问题及解决方法

总结

浏览器的调试,最终都落脚到引擎:渲染引擎和 JavaScipt 引擎。那么对于 JavaScript 调试来说,难点就在于 V8 如何给 JavaScript 某一行进行标记然后进行断点,这需要有一点 V8 的知识。

2 赞 3 收藏 1 评论

图片 19

引用百度百科图片

1. 网上传统的过时设置方法访问地址

图片 20

过时设置方法

出现的问题,就是按照以上步骤操作之后,我们并没有发现Node debugging的选项,此时,就不知道如何进行下去了。在这里给出的解释是,在最新版的Chrome浏览器中,Node debugging不需要手动去启动了,而是默认就是可以使用的

建立第一次的握手协议后,客户端和服务器之前就开始进行有效的实时通信了。

1. 不同Node版本出现的问题

分别在三个大版本下进行了测试(v6.9.2,v7.3.0,v8.9.1)

远程调试协议结构

v6.9.2 版本下使用情况

图片 21

命令行执行结果

图片 22

访问

此时你会发现,命令行已经给你访问Chrome的url,并且这个url和访问http://127.0.0.1:9229/json/list得到的devtoolsFrontendUrl属性值一样,但是,当你访问这个url时,浏览器却不能显示调试页面,此时,说明你的Node版本太低,需要使用更高版本。

远程调试协议基于WebSocket,利用WebSocket建立连接开发者和浏览器内核的快速数据通道。该协议把操作划分为不同的域(domain),比如Page、Rendering、Network、Memory和DOM等,通过远程调试协议发送的请求可通过域操作获取相应的信息。每个域(domain)定义了它所支持的command和它所产生的event。每个command包含了指定所要进行的操作以及操作说要的参数和描述;每个event表示了发生某个事件的监听,通过设立监听点来返回信息和进行下一步操作。command和event中可能涉及到非基本数据类型,在domain中被归为Type,比如:’LoaderId’: ,其中loaderId为非基本数据类型。下图

v7.3.0 版本下使用情况

图片 23

命令行执行结果

图片 24

访问

图片 25

调试页面

此时,命令行也提供了访问调试页面的链接。

显示了远程调试协议network域的基本数据结构:

v8.9.1 版本下使用情况

图片 26

命令行执行结果

图片 27

访问

图片 28

调试页面

此时,命令行给定的的url并不是调试页面的url,因此必须通过访问http://127.0.0.1:9229/json/list来获取调试页面url。

{

"domain": "Network",

"description": "Network domain allows tracking networkactivities of the page. It exposes information about http, file, data and otherrequests and responses, their headers, bodies, timing, etc.",

"dependencies": ["Runtime", "Security"],

"types": [{

"id": "LoaderId",

"type": "string",

"description": "Unique loader identifier."

},

..., {}

],

"commands": [{

"name": "clearBrowserCache",

"description": "Clears browser cache.",

"handlers": ["browser"]

},

..., {}

],

"events": [{

"name": "requestServedFromCache",

"description": "Firedif request ended up loading from cache.",

"parameters": [

{ "name":"requestId", "$ref": "RequestId","description": "Request identifier." }

]

},

..., {}

]

}

相关文档

Debugging Guide
Debugging Node.js Apps

事实上,远程调试协议数据结构就是一种json格式的数组,可以通过json对象的方式调用其方法或事件。例如要调用network中清除浏览器缓存的方法,可以通过WebSocket协议连接到该页面后,使用WebSocket的send方法发送一个对象来实现,

ws.send('{"id": 1,"method": "Network.clearBrowserCache", "params":{}}')

其中ws表示WebSocket与浏览器建立连接后的url,id是被链接页面的id。

应用实践

了解了远程调试协议建立的通信机制和数据结构,接下来看看实战的例子——如何获取手机中H5页面的性能数据。

图片 29

整个操作流程的系统框图

1.建立移动端和PC的连接,获取webSocketDebuggerUrl

手机端用chrome浏览器打开需调试的H5页面,通过USB和PC端连接,执行端口转发指令将页面发送的数据信息转发到9222端口,这样就可以在PC上操纵手机的页面。

adb forward tcp:9222 localabstract:chrome_devtools_remote

在PC端访问http://127.0.0.1:9222/json即可获得当服务端连接到该标签页的websocket地址,如下图:

"webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/2"即为要调试的页面

[ {

"description": "",

"devtoolsFrontendUrl": "",

"id": "2",

"title": "微微一笑很倾城,抽张油卡变男神!",

"type": "page",

"url": "",

"webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/2"

} ]

2.初始化消息

为了获取页面传递数据,首先得建立一个初始化窗口,触发客户端和网络的连接,并建立监听的事件。

var socket = new WebSocket(url);

socket.onopen = function(event) {

//发送一个初始化消息

socket.send('{"id": 6, "method":"Network.enable", "params": {}}');

socket.send('{"id": 6, "method":"Page.enable", "params": {}}');

socket.send('{"id": 6, "method":"Page.navigate", "params":{"url":"');

//监听消息

socket.onmessage = function(event) {

console.log('Client received a message', event);

var data = event.data;

var obj = new Function("return" data)();

//console.log(obj.method);

所有的数据操作要在这个初始化窗口内实现。

3.发送远程调试协议方法的消息

页面发送的消息要经过远程调试协议的“包装”来解析页面的性能数据。

function DoToMessage(message,page){

switch (message.method) {

case 'Page.domContentEventFired':

page.domContentEventFiredMs = message.params.timestamp * 1000;

break;

case 'Page.loadEventFired':

page.loadEventFiredMs = message.params.timestamp * 1000;

break;

default:

if (message.method.match(/^Network./)) {

id = message.params.requestId;

switch (message.method) {

case 'Network.requestWillBeSent':

// the first is the originalrequest

if (typeofpage.originalRequestId === 'undefined' &&

message.params.initiator.type === 'other') {

page.originalRequestMs =message.params.timestamp * 1000;

page.originalRequestId =id;

}

page.objects[id] = {

'requestMessage':message.params,

'responseMessage':undefined,

'responseLength': 0,

'encodedResponseLength': 0,

'responseFinished':undefined,

'responseBody': undefined,

'responseBodyIsBase64':undefined

};

4.获取HAR文件,并进行对象的格式整理

页面向服务端发送了远程调试协议方法下的请求,服务端会返回HAR(HTTP Archive)文件,一个用来储存HTTP请求/响应信息的通用文件格式,基于JSON格式的数据。我们可以对改文件数据进行二次数据格式整理,便于后续的对象操作。例如每个输入会返回页面参数id,开始时间,加载时间,请求参数,响应参数等。

functionfromPage(page) {

for (var requestIdin page.objects) {

entries.push({

'pageref': page.id.toString(),

'startedDateTime': newDate(object.requestMessage.wallTime * 1000).toISOString(),

'time': toMilliseconds(duration),

'request': {

'method':object.requestMessage.request.method,

'url':object.requestMessage.request.url,

'httpVersion': protocol,

'cookies': [], // TODO

'headers':requestHeaders.pairs,

'queryString': queryString,

'headersSize':requestHeaders.size,

'bodySize':object.requestMessage.request.headers['Content-Length'] || -1,

},

'response': {

'status': object.responseMessage.response.status,

'statusText':object.responseMessage.response.statusText,

'httpVersion': protocol,

'cookies': [], // TODO

'headers':responseHeaders.pairs,

'redirectURL': redirectUrl,

'headersSize':responseHeaders.size,

'bodySize': bodySize,

'_transferSize':object.encodedResponseLength,

'content': {

'size':object.responseLength,

'mimeType':object.responseMessage.response.mimeType,

'compression': compression,

'text':object.responseBody,

'encoding':object.responseBodyIsBase64 ? 'base64' : undefined,

}

........

}

}

5.从HAR文件中提取需要展示的性能参数

functionGetOverViews(har) {

var getreqstime = function(har) {

var timess = 0;

for (var i = 0; i

timess = timess har.entries[i].time;

return timess;

};

}

return {

'overview': {

'countoftime': getreqstime(har),

},

'entries': har.entries

};

}

例如通过统计har中所有entries的时间总和来获取当前页面的加载总时间,并将该结果(getreqstime(har))赋值到'contoftime'对象中,前端页面通过调用countoftime即可展示对应的数据。

6.结果展示

图片 30

总结

本文通过对远程调试协议的应用,介绍了一种简便获取H5页面性能测试数据的解决方案。通过这种方式,测试人员不需要使用第三方工具对H5页面进行调试,而是在建立移动端和PC连接后,通过刷新页面直接可获取性能参数报告,对测试效率的提升带来了极大帮助。目前,为了进一步满足测试的需求,该方案还在精细化调整中,读者有什么建议或意见欢迎留言提出。

本文由pc28.am发布于前端技术,转载请注明出处:依据远程调节和测量检验左券的H5品质测量试验解

上一篇:拾七个你或然不知晓的JavaScript调节和测量检验本 下一篇:没有了
猜你喜欢
热门排行
精彩图文