喽,你好啊,我是雷工!
断点调试是程序猿必备的调错,梳理逻辑的技能;当遇到程序报错,或者程序逻辑理解不了,都可以通过断点调试来辅助解决遇到的问题。
断点调试是程序猿必不可少的技能,本节学习断点调试,以下为学习笔记。
● 作用:学习时可以帮助更好地理解代码运行,工作时可以更快找到bug
● 断点调试步骤:
1.1、选运行程序;
1.2、在浏览器打开调试界面(按F12打开开发者工具)
1.3、在浏览器控制台中选中sources一栏;
1.4、单击对应的html页面;
1.5、在代码第一行位置处设置断点(在需要设置断点的对应行上点击鼠标左键);
1.6、重新刷新界面,执行程序;
1.7、手动让程序逐行执行,点击F10或者点击下一步按钮。
1.8、将鼠标放到变量上或者某个条件上就可以看到执行的结果了。
● 断点:在某句代码上加的标记就叫断点,当程序执行到这句有标记的代码时会暂停下来。
2、循环嵌套:
说明:一个循环中可以嵌套一个或多个循环。
利用断点调试,可以很好的理解循环嵌套程序。
音小程序开发者工具(https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/developer-instrument/overview)是面向字节系小程序开发者推出的桌面端集成开发环境,支持小程序开发、调试、预览、上传等基本功能,旨在帮助开发者更高效地开发小程序,我也是负责本地开发能力的建设。
因为工作原因最近对断点调试进行一些研究,百度了一下,遗憾的是发现网络上大部分内容都是在教学如何使用调试工具,并没有扩展到具体的细节,譬如通信逻辑,基本原理等。因此,为了尝试去弄懂一些断点调试的底层逻辑,特意去找了一些英文文档并实践。
作为一个前端开发,前端调试的方式一般有如下几种:
相比于 console,debugger 可以看到代码实际的执行路线以及每个变量的变化,代码可以跳着看,也可以针对某个函数步步执行。
但是 console 与 debugger 方式对代码都有侵入,在开发阶段可能要不断增加和移除来调试,如果不小心忘了,那 mr 又得打回并重新提交了…
相信很多人在提 mr 都有类似经验…
相对来说,浏览器中找到 source 源码打断点是一个更好的方式,但是还是需要打开 Devtools ,并在 sources 面板找到文件注入断点,操作上也是有点小麻烦。
因此第 3 种方式,可能是不错的方式,在 vscode 中直接在源码中调试,并能看到具体的变量信息和网页效果。
实际上,浏览器打断点与在 vscode 打断点本质原理都类似。下面就聊一聊浏览器断点调试和 vscode 断点调试的原理。
在了解具体场景之前,首先有一个比较重要的概念,那就是 CDP。
CDP(Chrome DevTools Protocol)是一种通过网络协议与 Google Chrome 或其他兼容的浏览器进行通信的协议。通过 CDP,开发者可以远程控制浏览器,获取浏览器状态信息,以及执行各种浏览器操作,从而实现自动化测试、性能分析、调试等应用场景。
:
CDP 最早于 2011 年在 Chrome 15 版本中引入,作为 Chrome DevTools 的核心组件之一而出现。在此之前,开发者通常需要通过浏览器插件或者第三方工具来进行调试和测试,这些工具通常不够标准化和通用,也难以实现远程控制。
就跟 Emoji 的历史差不多了,都是乱的,然后规范化,最后大力发展。
CDP 的出现解决了这些问题,使得开发者可以通过标准化的协议来远程控制浏览器,获取浏览器状态信息,以及执行各种浏览器操作。CDP 的出现和发展推动了 Web 开发和测试的发展,为开发者带来了更加高效和便捷的开发和测试方式。
CDP 通过 JSON-RPC 协议来进行通信,提供了一套完整的 API,包括 DOM、CSS、网络、调试、安全等方面的接口。实际上,可以使用各种编程语言来编写 CDP 客户端,从而实现与浏览器的交互。
上图为 CDP 的官网(https://chromedevtools.github.io/devtools-protocol),可以看到,CDP 包括很多 Domains,常见的 CDP 信息包括:
这几个也是平常开发中最常用到的几个 Domains 了。
chrome 的 Devtools (Front-End Devtools)与 Web Page 之间的调试也是通过 CDP 通信的,如下图所示:
除了调试,CDP 额外应用场景也很多,比如刚才提到的自动化测试,通过 CDP 模拟用户行为,操作页面元素等,或者 CDP 获取浏览器的性能指标生成性能报告,还可以通过 CDP 模拟浏览器行为,获取页面数据,实现爬虫等等。
带着问题出发,可能需要搞懂以下 3 点:
页面与 Devtools 是如何通信的?
断点操作逻辑通信过程是什么?
如何实现命中断点并停止代码执行的?
在浏览器中,网页的调试能力是由 Devtools 提供的。Devtools 与网页之间的通信利用的是 Websocket,而通信协议则是 CDP。
除了开发中常用到的元素高亮,日志打印和网络审查,上面也提到了还可以在 sources 面板中使用 debugger。
如下图所示,找到一行 js 代码,在代码中点击断点调试,可以看到 Protocol Monitor 中有一些 CDP 消息,下面就来具体分析一下相关 CDP 信息。
为什么会发送多次,我也不理解,内容基本上是一致的。
点击断点以后,主要有以下一些 CDP 消息在页面与 Devtools 之间通信:
setBreakpointsActive 表示告诉页面要设置一个调试断点了;setBreakpointByUrl 则是告诉页面设置的具体信息;getPossibleBreakpoints 表示设置以后获取正确的断点位置,并展示蓝色小块。
有时候可能会发现设置了某一行为断点,但是断点的位置并不是指向的位置,而是另外的位置。比如上面截图,如果在 15 行设置断点,则最后展示断点位置为 18 行。
整体流程如下图:
除了在 sources 面板增加断点,还可以取消断点。取消断点的 CDP 非常简单, Devtools 会给 Web Page 发送一个 Debugger.removeBreakpoint 来移除断点。
当点击完断点以后,页面会走到断点所在的代码位置,同时 Devtools 会接收到一些 CDP 消息,通知它当前断点的状态和上下文信息。
我写了一个实例,是关于数字的增减逻辑,并在数字增加的时候,走到断点位置(不需要刷新页面)。
可以看到,当点击 + 号以后,页面就进入断点调试逻辑,此时 Devtools 会收到 Debugger.paused消息:
此时表示页面已经暂停了代码执行,Devtools 可以通过 Debugger.paused事件中的参数,获取当前断点的上下文信息,如断点所在的函数、变量值、堆栈信息等。
具体信息没有对应看
点击“Step Over next function call”(按钮 1),Devtools 会收到 Debugger.resumed rɪˈzuːm d 消息,通知继续执行代码。
随后代码跳到下一行,此时又会收到 Debugger.paused消息。
点击“Resume Script Execution” (按钮 2)按钮,Devtools 会收到 Debugger.resumed消息,如果还存在断点,则此时也会收到 Debugger.paused消息。
此外这里还有一个 Overlay.setPausedInDebuggerMessage 消息,为 Devtools 发送给页面,其信息主要是让页面展示代码停止状态下应该展示的消息,默认为 {"message":"Paused in debugger"},也就是如下图展示的内容:
除了上面两个按钮,还有几个调试按钮,如下图绿色区域内:
分别是:Step into next function call、Step out of current function、Step、Deactivate breakpoints。
:
Step into next function call:这个按钮用于进入当前行代码所在的函数内部,即单步进入函数中执行。
Step out of current function:这个按钮用于跳出当前函数,即单步跳出当前函数执行。
Step:这个按钮用于单步执行代码,即逐行执行代码。
Deactivate breakpoints:这个按钮用于禁用所有的断点,即暂停调试器的所有断点。
点击“Step into next function call”,Devtools 会发送 Debugger.stepInto 消息,并收到 Debugger.resumed和 Debugger.paused消息,进入到函数内部。
点击“Step out of current function”,Devtools 会发送 Debugger.stepOut消息,并收到 Debugger.resumed和 Debugger.paused消息,跳出该函数。
点击 “Step” 按钮,Devtools 则发送 Debugger.stepInto,代码执行到下一行,每次点击,都会发送 Debugger.stepInto消息。
点击 “Deactivate (/ˌdiˈæk.tɪ.veɪt/) breakpoints”,Devtools 则发送 Debugger.setBreakpointsActive 消息。如果当前断点状态为执行状态,则参数为 active: false,同时设置蓝色小块颜色为透明色。
重新执行代码,断点调试能力失效。
再点击一次,则参数为 active: true,断点调试能力生效。
了解完相关断点操作流程以后,再分析一下相关逻辑的源码。
首先,Devtools 的源码就是 Front-End Devtools,UI 上的逻辑这里就不多分析。关于页面的调试通信逻辑在 DebuggerModel 中:https://source.chromium.org/chromium/chromium/src/+/main:out/Debug/gen/third_party/devtools-frontend/src/front_end/core/sdk/DebuggerModel.js;l=280;drc=f09c12c84b39d13189a7039a05253ca3766d4751;bpv=0;bpt=0
async stepInto() {
const skipList = await this.computeAutoStepSkipList("StepInto" /* StepInto /); void this.agent.invoke_stepInto({ breakOnAsyncCall: false, skipList }); } async stepOver() { this.#autoSteppingContext = this.#debuggerPausedDetailsInternal?.callFrames[0]?.functionLocation() ?? null; const skipList = await this.computeAutoStepSkipList("StepOver" / StepOver /); void this.agent.invoke_stepOver({ skipList }); } async stepOut() { const skipList = await this.computeAutoStepSkipList("StepOut" / StepOut */);
if (skipList.length !== 0) {
void this.agent.invoke_stepOver({ skipList });
} else {
void this.agent.invoke_stepOut();
}
}
pause() {
this.#isPausingInternal = true;
this.skipAllPauses(false);
void this.agent.invoke_pause();
}
很清晰的看到,上面提到的各种操作逻辑的函数,譬如 pause、stepXXX等 API。
这里列举几个操作按钮通信较多的 API。
pause() 的主要逻辑为 2 点:
stepInto() 的主要逻辑为:
其他 API 逻辑类似。
再分析一下 chromium /ˈkroʊ.mi.əm/ 中的断点调试代码逻辑。chromium 中发送 CDP 消息到 Devtools 的逻辑在 devtools_agent_host_impl中,而断点调试逻辑在devtools_session文件中,通过 agent 的 DispatchProtocolMessage最后调用到 session 的 shoulSendOnIO函数。
具体来说,这个函数接收一个包含 CDP 方法的 span 参数,然后检查该方法是否属于一组特定的方法,如果是,则返回 true,表示该 CDP 消息需要转发。
DevToolsSession 是 Chromium 源码中的一个类,代表一个 DevTools 会话。DevToolsSession 负责管理与 DevTools 和页面之间的通信,包括上面提到的调试。
bool ShouldSendOnIO(crdtp::span<uint8_t> method) {
static auto* kEntries = new std::vector<crdtp::span<uint8_t>>{
crdtp::SpanFrom("Debugger.getPossibleBreakpoints"),
crdtp::SpanFrom("Debugger.getScriptSource"),
crdtp::SpanFrom("Debugger.getStackTrace"),
crdtp::SpanFrom("Debugger.pause"),
crdtp::SpanFrom("Debugger.removeBreakpoint"),
crdtp::SpanFrom("Debugger.resume"),
crdtp::SpanFrom("Debugger.setBreakpoint"),
crdtp::SpanFrom("Debugger.setBreakpointByUrl"),
crdtp::SpanFrom("Debugger.setBreakpointsActive"),
crdtp::SpanFrom("Emulation.setScriptExecutionDisabled"),
crdtp::SpanFrom("Page.crash"),
crdtp::SpanFrom("Performance.getMetrics"),
crdtp::SpanFrom("Runtime.terminateExecution"),
};
...
}
可以看到,这里定义了所有发送到 Devtools 的 API。在 chromium 的各种断点调试方法,最后都会调用 DispatchToAgent方法,并走到 ShouldSendOnIO逻辑。
通过上面的分析,了解到了调试器和页面之间的 CDP 通信内容和 API 的基本实现。那 chromium 又是如何停止代码到断点的呢?为何可以停止代码执行呢?
在 DevTools 中,停止代码执行到断点的核心实现是通过使用 V8 JS 引擎中的断点机制来实现的。当 chromium 执行到一个断点时,V8 会暂停 JS 代码的执行,并将控制权转交给 Devtools。这时候,Devtools 可以执行上述提到的断点调试的各种操作。
这块逻辑的代码在 chromium auction_v8_devtools_agent 和 auction_v8_devtools_session 中,看起来比较复杂,涉及到 AuctionV8DevToolsSession 和 AuctionV8DevToolsAgent 两个类,我的理解是 DevtoolsAgent 提供了一些 Devtools debugger 的服务,并找到对应的 DevtoolsSession 进行通信。V8 将 ws 格式信息转交给了 DevtoolsSession,最后通过 DevtoolsAgent 发送到了 Devtools。
大概逻辑如下:
通过 Devtools Agent,负责接收 Devtools 通信信息,并将断点信息移交给 V8,然后由 V8 来对代码进行停止操作。
V8 里面的逻辑我只能看一个大概,整体逻辑如下:
V8Debugger 是一个抽象,V8DebuggerAgentImpl 类实现了这个类,它是 Debug 类和 V8 调试协议之间的中介,负责将调试消息转换为 V8 调试协议中定义的格式。
关于 V8 断点 Debugger 更底层的逻辑是与 os、cpu 相关,os 提供了系统调用来实现可执行代码的中断。
中断则是 cpu 执行下一条指令之前,关注一下中断标记,从而判断是否需要中断执行。整体逻辑上对照着 Vue 的渲染原理即可,每次事件循环结束后最后去走一次渲染 DOM。
V8 本身也是将 JS 转为可执行语言,这也就是为何 JS 可以在浏览器中拥有断点能力了。
这里涉及到一些指令操作,没有深究。
同时,V8 中断代码执行,也会提供一些环境数据到 Devtools,譬如当前变量数值等,这时候 V8 就会将这些调试信息通过 V8 Debug Protocol 协议的格式丢给 Debug,最后丢给 Devtools,从而鼠标悬浮在 sources panel 即可看到对应的数据内容。
Debugger.evaluateOnCallFrame 和 Runtime.getProperties 可以拿到一些环境信息,前者比如一些 number 数字就可以得到。
在 Vscode 中调试代码,能让开发者专注于代码本身,一边开发运行一边断点调试查看变量信息,并减少一些脏代码的开发。如下图所示,可以看到,似乎是将浏览器的 Debugger 的逻辑照搬到了 Vscode 中。
在介绍完浏览器断点调试的逻辑以后,我们大概了解了页面与 Devtools 的通信过程和相关 CDP 信息。有了这些基础,我们再分析分析 Vscode 中是如何实现断点调试 Web 代码的。
在 Vscode 中配置调试后,会生成一个 .vscode/launch.json 文件,其主要是配置需要调试的 url 和远程调试的端口号 port。
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "针对 localhost 启动 Chrome",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}
[ˈɑrkɪˌtektʃər]
Vscode 并不只是前端开发者调试 JS 使用,还可以调试其他语言,Python 一些教程就建议使用 Vscode 调试。因此 Vscode 的调试架构高度灵活,可以支持多种编程语言和调试场景,并且可以基于该架构实现各种调试扩展。
如上图,Vscode 的调试架构中,有 3 个 Core Module:
:别忘了另外一个 Debugger,即为 launch.json 中的 type,指底层的调试目标,例如 Node.js 运行时、Chrome 浏览器等等。比如断点后的信息需要传递给 chrome,需要去暂定代码执行,并断点逐步执行等。
在了解原理之前,先看一些现象:
通过上面 3 种现象可以看出,Vscode Webpage Devtools 关系如下:
细品一下,这时候就可以知道为何需要 Debug Adapter 了。实际上,就是将 CDP 消息转为 DAP。
Vscode Chrome Debug 的工作流程如下:
这里的核心就是 Extension,其作用就是调度与控制,比如启动 Adapter 进程,发送与接收调试信息等等,属于大 BOSS,而 Adapter 只是下属。
上面提到,chromium 内部是使用 CDP 协议通信,因此 Extension 想要正确调试 Chrome WebPage,首先就得遵守 Chrome 的玩法。比如,在 Vscode 中点击 StepInto 按钮,这时候会将对应操作信息转化为 CDP 信息,然后再发送给 WebPage。
Extension 启动 Chrome 的逻辑在 companionBrowserLaunch 中:https://github.com/microsoft/vscode-js-debug/blob/main/src/ui/companionBrowserLaunch.ts#L50
await vscode.commands.executeCommand('js-debug-companion.launchAndAttach', {
proxyUri: tunnel ? 127.0.0.1:${tunnel.localAddress.port} : 127.0.0.1:${args.serverPort},
wslInfo: process.env.WSL_DISTRO_NAME && {
execPath: process.execPath,
distro: process.env.WSL_DISTRO_NAME,
user: process.env.USER,
},
...args,
});
另外,Devtools 与 WebPage 是通过 ws 通信的,这里 JavaScript Extension 内部实现与开发者工具调试器和模拟器的通信相似, Extension 与 WebPage 通信也是拿到了页面的 debug ws url,在 Extension 内部创建一个 ws client,通过该 client 监听来自于 WebPage CDP 信息,并转发到会话的 Adapter,最后再交给 Vscode。
看最新的代码,JS Debug Extension 也会负责部分调试 UI 相关逻辑。
以 StepInto举例,在 Vscode 中点击该按钮以后,会发送一个 DAP 消息:
{
"command": "stepInTo",
"seq": number,
"type": "request",
"arguments": {
"threadId": number
}
}
然后,Exetension 将该消息转为 CDP 消息,并发送给 WebPage:
{
"id": 1,
"method": "Debugger.stepInto",
"params": {
"callFrameId": number/string
}
}
WebPage 收到该消息后,返回执行结果到 Extension:
{
"id": 1,
"result": {}
}
Extension 再将该 response 通过 Debug Adapter 转给 Vscode,Vscode 调整 UI:
{
"body": {
"reason": "OK",
"threadId": number
},
"type": "response"
}
相关 DAP 格式可以在 debug-adapter-protocol 查阅:https://microsoft.github.io/debug-adapter-protocol/overview
如果要在 Vscode 中查看实时的 DAP 和 CDP 消息,可以通过如下操作:
上面给到的例子非常简单,js 代码也没有经过构建生成编译后的代码。但是实际场景中开发的项目会引入各种开源库,然后经过诸如 Webpack 等打包构建工具做编译打包,才能在浏览器中运行。编译压缩后的代码一般不具备可读性,因此在编译后代码进行调试成本比较高。
We all know,SourceMap 存储着源码和生产代码之间的映射关系。譬如我这里启动了一个 Vite 项目:
当我在源码的 main.ts 中设置断点时,可以看到 Request 中的 url 为 host:port/src/main.ts,即实际传给 WebPage 的断点文件为编译后的文件。
JS Debug Extension 亦是如此。
当在 Vscode 的源码中增加了一个断点,JS Debug Extension 会根据 sourceMap 将源代码路径映射到编译后的代码路径中,并将这个信息发送给浏览器。
所以呀,解析是前端行为。
SourceMap 虽然也是静态资源,但是其加载在 Network 面板并不能看到,而是在 Developer Resources 中。
为了启动快,我用的 Vite 来生成项目。Vite 利用了浏览器原生的 ES modules 功能,根据文件依赖关系,生成依赖树,然后各模块文件模块单独加载。Vite 文件都有单独的 SourceMap,不需要配 SourceMap 依赖。
可以看到,这里 Vite 默认是直接内嵌的 SourceMap,无需单独请求, 可以在代码文件加载完成后,就直接解析了,红框里面展示的链接就是 Base64 的形式了。
⚠️SourceMap 的解析是交给 Devtools 本身的,Debugger 只负责运行和暂停。因此,如果断点在 SourceMap 解析完成之前触发,则没法告诉 Debugger 正确的地址,可能会出现断点无效情况。
根据上面的介绍,小程序断点调试的最简单办法就是在代码中写上 debugger,然后交给 v8 处理即可。另外还有一种方式就是打开小程序调试器,在 sources panel 中打断点,如下图:
打断点,刷新小程序,即可跳转到断点位置。此时可以看到对应的 CDP 消息中的 Request。
可以看到,这里点击的是 56 行,但实际上 Request 中却不是,Devtools 通过 sourceMap 进行了处理,定位到了 64 行。根据上面提到的源码调试逻辑,这里的位置为编译后的代码位置,找到编译产物代码 app.js 即可看到 real position。
考虑到上面提到的 Vscode 有 web 断点调试能力,那 IDE Editor 或许也是可以支持断点调试能力的。
Vscode 可以直接在编辑器运行项目,然后启动自定义的调试目标(Debugger)。
IDE 为小程序运行时的载体,与 Vscode 启动 web 项目不一样,其逻辑为编译完成后生成一个编译产物目录,通过静态服务,Simulator 直接加载对应编译产物。因此,IDE 的 Editor 实际上跟 Simulator 没什么联系的。
假设借用 Devtools Debug 的逻辑,当在 Editor 打断点时,捕获所有的断点 DAP 消息,当开启调试时,刷新模拟器,将所有的断点信息转为 CDP 信息发送给模拟器,或许就可以简单实现该能力。
当然,考虑到是在源码中打断点,这里的难点应该是在于要实现 sourceMap 解析,而 Debug UI 则可以利用 Vscode JS Extension,或者通过自定义实现一个 Debug UI。
本文从抖音开发者工具支持断点调试能力需求引入,概述了浏览器断点调试的基本原理,也介绍了 Vscode Web 代码断点调试能力,详细介绍了各模块中各 CDP 消息通信逻辑。阅读本文可以掌握前端各种调试方法的基本原理。
抖音开放平台提供小程序、移动应用、网站应用、直播小玩法等多业务载体,为开发者提供丰富的能力和解决方案。抖音开放平台基于平台规则和开发者诉求,提供了两种开放模式:能力开放和行业开放。
[1]
V8 本地调试: https://zhuanlan.zhihu.com/p/568432229
[2]
Debugging over the V8 Inspector Protocol: https://v8.dev/docs/inspector
[3]
Adapter Debug Protocol: https://microsoft.github.io/debug-adapter-protocol/
[4]
SourceMap: https://zhuanlan.zhihu.com/p/615279891
作者:Rabbitzzc
来源:微信公众号:字节前端 ByteFE
出处:https://mp.weixin.qq.com/s/DGSSDEmAdj8sE_KfN3wQsg
者:陈亦涛来源:大转转FE
这篇文章将介绍如何使用断点来进行 JavaScript 调试。在读这篇文章之前,需要问一个问题:为什么要使用断点来进行调试?
我们首先需要认可使用断点的是必要的,否则下文介绍的所有断点调试方法都会是废话。console.log 是前端开发最常用的调试手段,它简单直接解决一部分问题。但当遇到十分复杂的问题,console.log 就会变得不趁手。比如:
如果你刷过 leetcode 一定深有体会,算法某个测试用例报错了,有时很难光靠目测找出有问题的那个方法。
花了10分钟好不容易复现了,但是只跟踪到某行代码,需要第二次添加 log 才能继续寻找问题。查看log -> 添加log -> 查看log... 这个过程重复几遍,今天剩下的砖就搬不完了。
有 nodejs 服务端开发经验的同学相信有过在 postman 和 ide 之间反复横跳的经历,如果光靠 log,对于一个巨大的复杂对象,控制台是不好查看全貌的。如果一个接口还涉及到数据库增删、第三方依赖,那么复原上一次请求造成的后果也是一件痛苦的事情。
在这些情况下,断点调试是非常有价值的,将 debug 的时间复杂度从 O(n) 降到 O(1),让搬砖更快乐。
这是文章的内容大纲:
最简单的断点调试,就是在代码中加一句 debugger,然后到浏览器中刷新页面,这时候浏览器就会在 debugger 语句那停止执行。
为了方便理解,引入一个简单例子,在一个文件夹中创建 index.html 和 index.js,然后在 index.html 中引入 index.js。index.js 内容如下:
// 国际惯例,hello world。
const greet = () => {
const greeting = "hello debugger";
// 浏览器执行到这里将会暂停
debugger
console.log(greeting);
};
greet();
console.log("js evaluation done");
执行命令:
npm i -g serve
serve .
然后访问 http://localhost:5000并打开开发者工具。
这时候我们的 hello world 断点就打上了,就像这样:
图中分为四个区域,蓝色区域用于文件选择,Page 一栏是指当前页面中的 JS 文件,Filesystem 会显示我们系统中的文件。通常我们使用 Page。
粉色是代码的行号和内容。代码的行号处可以通过点击来添加新的断点,再次点击后取消。
黄色区域用于控制代码的执行,只需要掌握前四个按钮的含义,就可以应付绝大多数场景。按钮1是让代码继续执行(resume),如果遇到下一个断点就会再次中断执行。按钮2可以让浏览器执行当前行(图中是第3行),然后在下一行中断代码,按钮3是进入当前函数,查看函数具体内容。假设我们当前停在第7行 greet() ,点击按钮3就会进入 greet 方法中(也就是第2行)。如果不想再看 greet 方法了,就点击按钮4,跳出这个方法,回到第8行。
绿色区域可以查看变量的内容和当前的调用栈。
debugger 是最简单粗暴的打断点方式,但是需要修改我们的代码。需要注意的是,上线前必须删除这些语句。也可以通过配置 webpack 来自动去除。不过终究还是有些不方便,所以我们来看下如何通过 vscode 来简化打断点的方式。
首先我们使用 Vite 来创建一个 Vue 应用用于演示(React步骤类似)。
# 创建 vut-ts 应用
npm init vite
cd hello-vite
npm install
# 调用 VS Code cli 打开项目,
# 或者手动在 VS Code 打开。
code .
npm run dev
然后在 VS Code 中新建一个文件 .vscode/launch.json,填入这些内容:
{
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Vue project",
// 这里填入项目的访问地址
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
},
]
}
然后使用 cmd+q 退出你正在运行的 Chrome(这步很重要,不能跳过),按 f5 启动 VS Code 的调试功能。VS Code 就会帮你启动一个 Chrome 窗口,并访问上述配置的中的 url。这时候我们的断点就生效了,可以一步一步地控制代码的运行,找出 bug 来源。
这里有一个实用的小技巧,就是在 BREAKPOINTS 中,把 Uncaught Exceptions 勾上,这样在代码报错的地方,就会自动中断执行。当我们遇到一个报错时,采用这个方法可以省去定位问题代码的时间。
另外我们可以发现,在 VS Code 断点生效时,Chrome Devtools 也会同步这个展示这个断点。
在 VS Code 中,调试有两种模式,分别是 launch 和 attach。由于真正执行代码的是 Chrome 中的 JS 引擎,所以是否中断代码的控制权是在 Chrome 手里的。那为什么 VS Code 的断点可以控制代码的中断呢?是因为 VS Code 通过 devtools-protocol 向 Chrome 发起指令,告诉 Chrome 需要在哪一行代码暂停执行。这个发送指令的过程,被称作 attach。而 launch 的过程包含 attach ,即先 launch(启动) 浏览器,然后 attach(附加) 断点信息。所以 attach 模式是 launch 模式的子集。
听起来好像 launch 模式会更方便,为我们省去了手动启动浏览器的过程。但是这存在一个问题,如果同时开发多个前端工程会怎样?每个工程启动一个调试进程,就会打开多个浏览器,那么在多个浏览器之间切换就会显得很麻烦。我们可以使用 attach 模式解决这个问题。
首先我们使用命令行启动 Chrome。使用命令行的原因是,我们需要给 Chrome 的启动传参。
# 运行这条命令前需要cmd+q退出已运行的Chrome
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222
# 如果看到这个输出,说明传参成功。
DevTools listening on ws://127.0.0.1:9222/devtools/browser/856a3533-ca5c-474f-a0cf-88b7ae94c75b
VS Code 和 Chrome 是通过 websocket 交流,--remote-debugging-port 指定了 websocket 使用的端口。然后我们将 launch.json 文件修改成这样:
{
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "attach",
"name": "Vue Application",
// 项目访问的 url
"url": "http://localhost:3000",
// websocket 端口,需要与 --remote-debugging-port 参数保持一致。
"port": 9222,
"webRoot": "${workspaceFolder}"
},
]
}
注意在启动 VS Code 调试之前,需要在 Chrome 中打开 http://localhost:3000 这个页面。然后我们在 VS Code 中打上断点,刷新浏览器,代码就成功停在断点处了。第二个、第n个工程都可以采用相同的配置,区别是 url 字段要根据项目配置进行修改。
上文讲的是如何调试页面,接下来我们聊如何调试 nodejs 应用。首先来一个最容易上手的例子,创建一个 hello world:
// debug.js 文件
const greeting = 'hello nodejs debugger'
debugger
console.log(greeting)
然后运行这个文件
node --inspect-brk debug.js
Debugger listening on ws://127.0.0.1:9229/b9a6d6bf-baaa-4ad5-8cc6-01eb69e99f0a
For help, see: https://nodejs.org/en/docs/inspector
--inspect-brk 表示运行这个 js 文件的同时,在文件的第一行打上断点。然后打开 Chrome,进入 Devtools。点击红框处的按钮,就会打开一个 nodejs 专用的调试窗口,并且代码在第一行中断了。
nodejs 调试窗口:
这个方式的实质是,Chrome Devtool 根据 v8引擎的调试协议 向 nodejs 进程发送指令,控制代码的运行。可以发现,在网页的调试中,Chrome 是接受指令的一方,而在 nodejs 调试中,Chrome 转身变为发送指令的一方。所谓从悲惨的乙方华丽转身成甲方。
node 默认的 websocket 端口是 9229,如果有需要的话(比如端口被占用了),我们可以通过一些方式改变这个端口。
node --inspect=9228 debug.js
Debugger listening on ws://127.0.0.1:9228/30f21d45-9806-47b8-8a0b-5fb97cf8bb87
For help, see: https://nodejs.org/en/docs/inspector
在我们打开 Devtool 时,Chrome 默认检查 9229 端口,但当我们改变了端口号后,就需要手动去指定 Chrome 检查的地址了。点击下图中的 Configure 按钮,输入 127.0.0.1:9228,然后点击 Done。这时候 Remote Target 中就会出现 刚才启动的 node 进程,点击 inspect 就可以进入调试了。
到此为止,我们已经达成调试 node 的目的,但还有些繁琐,不够自动化。我们可以使用 VS Code,来一键启动调试。
用 VS Code 打开刚才的工程,然后在 launch.json 中输入这些:
{
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
// ${file} 的意思是,当我们启动调试的时候,调试的程序就是当前 focus 的文件。
"program": "${file}"
}
]
}
这时候切换到 index.js 文件,按 f5 启动调试程序,当运行到第二行 debugger 语句的时候,就会自动暂停执行。也可以点击代码行数的左侧来打断点。
另外,这个配置是支持 TypeScript 的,我们只需要 index.js 重命名为 index.ts,然后正常启动调试就行。
在某些情况下,我们不希望打上的每个断点都发挥作用,而是在执行到断点那行,且满足某个条件再中断代码执行。这就是条件断点。
for (let i = 0; i < 10; i++) {
console.log("i", i);
}
比如上面的代码,假设我们在第二行 console.log 打了断点,那么这个断点总计会中断十次。这往往是我们不希望看到的,可能我们需要的仅仅是其中某一次循环而非所有。这时候可以右键点击并选择 Add Conditional Breakpoint。
这时会有一个输入框出现,我们在其中输入 i === 5。
这时候启动调试,就会跳过 i 为 0 - 4,直接在在 i 为 5 的时候中断代码执行。恢复代码执行后,会略过 i 为 6 - 9 的情况。
Conditional Breakpoint 在调试带有大量循环和 if else 判断时极为有用,特别是当某处的逻辑整体上是符合预期的,仅有个别特殊情况的输出错误,使用条件断点就可以略过这些正常的情况,只在个别特殊情况出现的时候,再中断执行,供我们查看各个变量是否计算正常。
调试是日常工作中非常重要的能力,因为除了开发新功能外,日常有很大一部分都在调整旧的代码,处理特别条件下的逻辑错误。熟练掌握调试可以很好地提升搬砖幸福感,一个复杂的 bug 卡几小时,很容易让人心里崩溃。但也不是说断点调试是任何情况下都适用的银弹,简单的逻辑还是可以愉快地 console.log 的。
文章介绍了使用 Chrome Devtools 和 VS Code 断点调试的方法,整体上还是更推荐使用 VS Code。launch.json 只需要一次配置,后续都可以 f5 一键启动调试。另外,文中提到的各种 launch.json 文件的配置,都可以使用 VS Code 自带的工具一键生成。只要打开 launch.json,编辑器的右下角就会出现 Add Configuration 按钮,点击就可以选择自己需要添加的调试配置。
*请认真填写需求信息,我们会在24小时内与您取得联系。