Skip to content

Latest commit

 

History

History
317 lines (182 loc) · 19.5 KB

chapter5.md

File metadata and controls

317 lines (182 loc) · 19.5 KB

5. 前端调试

编写代码其实只是开发者的一小部分工作,调试在软件开发中所占的时间可能比你想象的时间要长。为了让工作更有效率,我们必须精通软件调试技巧。花一些时间学习新的调试技巧,往往能让我们能更快地完成工作,对我们的团队做出更大的贡献。我将从一些核心概念开始讲解,然后深入探讨一些具体的例子。

指导思想和原则

开始讲解具体的调试内容之前呢,我们先来看下调试问题的指导思想和原则。 它能让我们事半功倍。

问题隔离

隔离问题恐怕是调试中最重要的概念和技巧了。 我们的代码库是由不同的类库、框架组成的,它们有着许多的贡献者,甚至还有一些不再参与项目的人,因此我们的代码库是杂乱无章的。隔离问题可以帮助我们逐步剥离与问题无关的部分以便我们可以把注意力放在解决方案上。

前端的依赖管理目前来看大都是基于node_modules去管理。 如果你细心观察的话,你会发现项目的业务代码通常只占整体的代码的很小一部分。

如下一个实际项目的业务代码和依赖的代码分布情况。

du -s src
# output: 6
du -s node_modules
# output: 257

可以看出依赖的大小为业务代码的40多倍。而且我的这个项目本身来说还属于比较简单的, 更复杂的项目通常这个比例会更大。你也可以试试你自己的项目。 因此管理依赖要比想象中复杂,如何挑选高质量的依赖,以及做好依赖管理,依赖出现问题怎么办等变得非常重要,这在一些大型项目中尤为明显。

有时候业务代码本身也很复杂,可能有上百个模块。 出现问题,我们需要定位到尽可能小的模块,以便我们更快找到和解决问题。

在实际操作中,我有许多种方法对问题进行隔离。其中一种是在本地创建一个精简的测试用例,当然你也可以在 CodePen 创建一个私人测试用例,或者在 JSBin 创建你的用例。另一种是在代码中创建断点,这样可以让我详细地观察代码的执行情况。

保护现场

当你发现一个问题的时候或者正在解决一个BUG的时候,请尽量不要安装任何东西或者添加新的依赖。这其实和刚才讲的问题隔离有点关系的,因为一旦添加了新的依赖或者安装了新的软件,意味着BUG重现的环境发生了改变,甚至有可能影响了整体代码走向,因此你之前为了找到问题所作的努力都白费了。

全局思维

全局思维是一个很抽象的概念。 怎么才是足够全面很难有一个明确的定义。

但是假如我们发现软件的现实行为和预期不一致的时候,我们能够清晰的在大脑中呈现这个问题出现的流程或者步骤,甚至可以直接在脑中隔离问题,大概定位问题的点。 那么我们剩下的工作就是将已经缩小的范围进一步验证和缩小,从而慢慢接近答案。 毫不夸张的说,我所解决的大部分问题都是通过这种方法。

倘若不经过这一步,我们就需要茫然地大海捞针般的去隔离,定位问题,这无疑会大大增加调试时间。 更可怕的是,我们这样缺乏条理性的调试,会在问题逐渐复杂化的过程中使问题越来越难以理解。 这就好像实现需求不经思考直接写和充分思考规划的区别一样。

调试的类型

调试从通信形式上分为本地调试远程调试。 本地调试就是debug host 和 debug target都是当前浏览器实例的这种情况。 平常大家直接打开开发者工具调试当前页面就属于这种情况。

远程调试指的是debug host 和 debug target不是同一个实例的情况。

本地调试

远程调试是相对于本地调试来讲的,那么理解本地调试对于理解远程调试是很重要的。 常见的就是调试本地PC(很简单)。 比如我在本地运行了一个webpack-dev-server,端口号为8080. 那么访问8080,并且打开浏览器的开发者工具,就可以在本地进行调试了。再比如我要调试google的官网,那么我只需要 访问www.google.com, 同样打开开发者工具就可以进行本地调试了。

远程调试

那么远程调试就是调试运行在远程的APP。比如手机上访问google,我需要在PC上调试手机上运行的google APP。 这是远程调试的一个典型例子。

其实本地打开一个浏览器, 用另外一个浏览器的开发者工具调试也属于远程调试。

远程调试大概有三种类型:

调试远程PC(本质上是一个debug server 和 一个debug target,其实下面两种也是这种模型,ios中间会多一个协议转化而已) 这种类型下的debug target就是pc, debug server 也是pc。

调试android webpage/webview(很多方式,但安卓4.4以后本质都是Chrome DevTools Protocol的扩展) 这种类型下的debug target就是android webview,debug server 是pc。

调试ios webpag/webview(可以使用iOS WebKit Debug Proxy代理,然后问题便退化成上述两种场景) 这种类型下的debug target就是 代理ios webview的代理层, debug server 是pc。

但是由于android端使用的是chrome内核或者基于chrome内核的封装。 而IOS则是safari内核或者基于其的封装。因此这里也可以将其非为普通内核调试和移动端APP的webview调试(本质上是对普通内核的封装)。

普通内核调试

这里以chrome为例

chrome远程调试

提到chrome的远程调试,就不得不提chrome remote debug protocol。 它采用websocket来与页面建立通信通道,由发送给页面的command和data组成。chrome的开发者工具是这个协议主要的使用者,第三方开发者也可以调用这个协议来与页面交互调试。简而言之,有了它我们就可以和chrome中的页面进行双向通信。

chrome 启动的时候,默认是关闭了调试端口的,如果要对一个chrome PC 浏览器进行调试,那么启动的时候,可以通过传递参数来开启 Chrome 的调试开关:

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

这个时候就可以通过http://127.0.0.1:9222 查看所有远程调试目标

http://127.0.0.1:9222/json 可以查看特定的远程调试目标信息,类型为json数组。

[
  {
    "description": "",
    "devtoolsFrontendUrl": "/devtools/inspector.html?ws=127.0.0.1:9222/devtools/page/fefa.....-ffa",
    "id": "fefa.....-ffa",
    "title": "test",
    "type": "page",
    "url": "http://127.0.0.1:51004/view/:id",
    "webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/fefa.....-ffa"
  }
]

其中id是一个唯一的标示,chrome dev protocol基本都依赖这个id。

比如关闭一个页面:

http://localhost:9222/json/close/477810FF-323E-44C5-997C-89B7FAC7B158

再比如激活一个页面:

http://localhost:9222/json/activate/477810FF-323E-44C5-997C-89B7FAC7B158

具体可以查看官方信息。

webSocketDebuggerUrl是调试页面需要用到的WebSocket连接的地址。

比如我需要清空浏览器缓存,就用websocket连接到该页面之后调用send方法

const ws = new WebSocket('ws://127.0.0.1:9222/devtools/page/fefa.....-ffa')
ws.send('{"id": 1, "method": "Network.clearBrowserCache", "params": {}}')

还有很多类似的api,因此就可以构造复杂的扩展

移动端APP的webview调试

移动端APP的webview通常不能直接进行远程调试。 因为webview并没有开启一个调试的端口进行调试。 一般的移动端APP会单独打包一个开发版本, 在开发版本中设置webview可以进行调试,这样我们才能够调试。

因此如果你能够搞到开发版的APP(本质上就是webview打开了调试端口)就可以直接调试。 如果搞不到,可以采取下面的 方式间接调试。

安卓端APP封装的webview调试方法

刚才说了移动端APP的webview只是对标准内核的封装,安卓使用的chrome的内核。 因此我们可以先收集浏览器内核版本,然后在该版本浏览器中重现问题,从而去调试。 具体方法如下:

  1. 收集内核信息

navigator.userAgent可以获取浏览器内核版本信息。 然后可以使用https://ie.icoa.cn/ 这个工具快速检测内核信息

  1. 根据内核信息下载安装对应的chrome版本浏览器。

可以去apkmirror下载 https://www.apkmirror.com/apk/google-inc/chrome/chrome-40-0-2214-109-7-release/chrome-40-0-2214-109-android-apk-download/ 对应的chrome版本浏览器。

  1. adb连接手机进行调试(优点是可以调试云端设备,无需连线)
  2. 需要手机和电脑同一个子网
  3. 需要知道手机ip
  4. 需要手机点击允许调试

但是这个方式并一定能够百分百重现问题。 比如某一个APP将alert方法进行了重写。 你如果在你的代码中用alert就有问题。 但这个并不能通过围魏救赵的方式去解决。 但是事实证明它可以解决很多绝大多数问题。 实在不行的可能就需要求助于该APP的开发者中心去帮助你们调试了。

IOS端APP封装的webview调试方法

和前面类似:

  1. 收集内核信息

navigator.userAgent获取浏览器内核版本信息

  1. 根据内核信息启动不同的模拟器调试

苹果有自带的模拟器可以使用,很方便。

  1. 打开PC上的safari进行调试

注意:通过 UA 探测判断内核方法的准确性有待讨论,因为 APP 可以任意修改 UA,简单的测试,不同平台下差距较大。

调试服务

明白了远程调试的类型,那么对于不同的类型应该采取什么样的手段是我们最为关心的问题。 在回答这个问题之前,我们先来看下市面上的远程调试框架,他们做了什么事情,解决了什么问题。

如下是我对比较常见的远程调试框架的简单对比。

remote-debug

后面虚线里面的是除了抓包功能之外调试框架,可以看出灰色部分是他们不支持的。 这时候就需要专门的抓包工具来代替。通常来说专门的抓包工具功能包括但不限于请求拦截和修改,https支持,重放和构造请求,(web)socket。

抓包工具的原理非常简单,本质上它就是一个正向代理,所有的请求经过它,就可以将其记录下来,甚至可以重放构造请求等。

对于抓包工具,步骤基本就是三部曲。

第一步:手机和PC保持在同一网络下(比如同时连到一个Wi-Fi下)

第二步:设置手机的HTTP代理,代理IP地址设置为PC的IP地址,端口为代理的启动端口。

Android设置代理步骤:设置 - WLAN - 长按选中网络 - 修改网络 - 高级 - 代理设置 - 手动 iOS设置代理步骤:设置 - 无线局域网 - 选中网络 - HTTP代理手动

对于https请求需要多一步:

第三步:手机安装证书。

对于第二步,我们可以通过连接USB的方式,然后设置端口转发和虚拟主机映射,建立tcp连接,这样就ip就可以设置为localhost,以后ip变动,代理设置也不必变动,但是却需要连接USB,可谓各有千秋。

远程调试服务

通过使用上面提到的框架(或者是自己写的拥有上面基本功能的框架), 我们已经可以很方便地进行调试了。 但是事实上大多数公司都是开发者在本地进行调试。这就造成了很多问题,比如

  • 如果手机在不同的开发者电脑调试,就需要在一个手机安装多个证书。

  • 开发者需要自己配置代理配置文件,共享需要手动导出导入相对麻烦。

  • 如果开发者本地的ip发生变动,那么就需要修改手机的代理ip(不使用usb走虚拟主机映射的情况)

    ......

针对以上等问题,如果搭建一个远程的调试服务(产品),那么问题就可以很好的解决。

理想的状况是,手机仅仅需要配置一次(安装证书,设置代理等),以后调试的时候就可以直接查看该手机的请求以及控制台,元素等等,并且直接指定映射到任何电脑 进行pc端调试。不同开发者之间可以方便的共享配置(比如有一个集团或公司的共有配置)。

拿搭建一个whistle的服务为例。

下载项目并启动:

npm install whistle -g --registry=https://registry.npm.taobao.org
 w2 start

设置代理:

比如设置手机的代理为x.x.x.x:8899

x.x.x.x 为部署服务的ip地址,8899为默认端口,若修改了,则对应修改为修改后的端口号

dashboard

访问http://x.x.x.x:8899/,会看到如下的页面:

whistle dashboard

我们可以在network查看远程的网络请求,可以通过其内置的weinre查看元素,控制台等

可以看到我们配置了三套配置,分别为默认配置,项目一和项目二。

简单介绍下配置做了什么。

前两个就是简单地将请求映射到本地。

第三条配置会拦截www.duiba.com.cn 的请求,并在html中注入一端weinre脚本。拦截成功就可以通过访问http://x.x.x.x:8899/weinre/client/#sword 访问对应的开发者工具进行调试。 weinre-client

其他功能:

Network:主要用来查看请求信息,构造请求,页面 console 打印的日志及抛出的js错误等

Rules:配置操作规则

Plugins:安装的插件信息,及启用或禁用插件

Weinre:设置的weinre列表

HTTPS:设置是否拦截HTTPS请求,及下载whistle根证书

我们可以进一步安装证书支持https拦截,配置账号系统,日志映射等等, 部署一个其他的远程调试框架的基本思想和步骤基本是一样的。

经过上面的步骤我们已经得到了一个集中式的调试服务器,我们不需要在本地配置复杂的环境和配置了, 并且足够灵活,我们可以查看所有请求,随意更改请求,并且直接代理到本地尽心调试。

实际情况我们还会遇到很多其他问题,通过扩展框架的功能丰富功能是非常有必要的,这个时候框架的扩展性就很重要了。

结合前端监控

有了前端监控,理论上我们已经能够收集用户的信息,包括报错信息,性能信息,心跳信息。 这就为我们发现问题,定位问题,解决问题提供了铺垫。

前端远程调试可以帮助我们调试远程的设备。 我们可以在远程服务器上还原案发现场,然后通过调试拥有案发现场的云端设备来定位并解决问题。这里有两个条件,分别是 远程服务器和云端设备。

远程服务器指的是部署有远程调试框架的服务器。

云端设备指的是用于还原用户设备的真实设备或虚拟设备,比如PC和各种型号的手机。

一个例子

用户a线上出现了一个问题,我们通过监控系统发现了这个问题,问题属于优先级比较高的,已经邮件通知了相关人员。

前端小k收到通知后打开监控系统-朱雀,通过检索用户找到了问题,点击debug按钮。这个时候 朱雀会根据用户的硬件信息自动去云端匹配最合适的设备,然后将该设备的使用权交给开发者小k。

云端设备打开网页,并还原案发现场。 这时候小k看到连接成功。

小k又打开了远程调试服务器-天道,这个时候它看到了云端设备发送的请求,已经通过内置的weirne查看了dom信息和其他信息。

但是并没有解决问题,小k想修改下代码,看下效果。 这时候小k熟练地切换了配置,将请求转发到本地,修改代码,刷新页面。发现问题解决了。

总结

通过刚才的例子,我们已经大概知道如何结合了。这里详细讲述一下。

首先天道需要足够的信息才能还原现场,这是单独做监控系统和远程调试系统不会过多涉及的。 这个我在前端远程调试里面提到过, 包括以下信息:用户轨迹,应用数据,其他调试信息。这就需要监控系统足够灵活去增加这些信息。 收集到信息,我们需要将信息应用到网页上,还原用户的操作。

其次朱雀需要管理云端设备,这部分是增加的独立功能。 这部分是增加的独立功能,具体涉及到的有查看所有设备及类型,控制设备打开app(可以是内置的浏览器,也可以是app内部的webview)加载网页, 设备使用分配(即分配给谁,时间多久后回收,调试端口号等)

对于第一点,其实是两部分工作。

第一个是前端监控系统的客户端需要将用户的信息 收集过来,这部分相对比较容易,我在前端远程调试部分也讲过思路,这里不再赘述。

第二个是前端监控系统的服务端需要将收集的信息应用到网页上。 这部分相对比较麻烦,思路有很多。比如对于纯数据驱动的应用,我们只需要将store 应用到app就可以了。 遗憾的是这对应用要求很高,基本不现实,这可以用来做辅助调试。 前面还提到了用户轨迹,我们是否可以通过重现用户轨迹重现问题呢? 理论上收集的用户轨迹足够细,是可以做到的,具体的收集思路有很多。 最常用的就是在各个地方手动埋点记录。

对于第二部分,其实就是对设备的管理。我们可以使用虚拟设备,也可以使用真实设备。其实这部分有很多成熟的技术可以参考的。

调试的辅助手段

通过上面的方式我们已经建立了调试的准备环境。但是真正调试应用,发现问题,解决问题。还需要 其他信息来辅助。下面来讲解一些调试的辅助手段。

用户轨迹

有时候我们需要知道用户的浏览轨迹,从而方便定位问题。 浏览轨迹的粒度可以自己决定,可以是组件级,也可以是页面级。

应用数据

获取足够的信息对于调试是非常重要的。尤其是数据驱动(data driven)的应用, 知道了数据,基本上就可以还原现场,定位问题。

比如我们使用vuex或者redux这样的状态管理框架,一种方式是将中央的store挂载到window上。 这样我们就可以通过访问window的属性获取到全局的store。 如果使用其他的状态管理框架或者自制的状态管理,也可以采取类似的方式。

然而我们也可以使用现成的工具,比如使用react-dev-tools调试react应用。 比如使用redux-dev-tools调试使用redux的应用等等。

其他调试信息

比如用户的id,客户端数据,登陆的session信息等等对于调试有所帮助的,都可以将其收集起来。