❝一个人,被别人看不起,不是最痛苦的。被别人看不见,才是最惨的。 ❞
大家好,我是「柒八九」。一个「专注于前端开发技术/Rust
及AI
应用知识分享」的Coder
。
不知道大家平时在前端开发中,是如何追踪数据流向的。console.log()/console.count()/console.table()
肯定大家或多或少的使用过。 还有那debugger
也是必不可少的方式。
针对,一些简单的数据查验,上面所说的其实已经够用。但是,在面对页面结构繁杂,数据流向紊乱的应用时,上面的措施就有点捉襟见肘。
所以,今天我们来深入研究一下,如何优雅的进行数据追踪。也就是如何高效的在浏览器中进行断点的跟踪。
好了,天不早了,干点正事哇。
❝
❞
❝「前置知识点」,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。「如果大家对这些概念熟悉,可以直接忽略」 同时,由于阅读我文章的群体有很多,所以有些知识点可能「我视之若珍宝,尔视只如草芥,弃之如敝履」。以下知识点,请「酌情使用」。 ❞
我们在了不起的Base64中介绍过RFC
。
它可以算是网络协议的「圣经」。
而,针对前端的部分技术,其实我们可以在WHATWG[1]找对对应的标准描述。换句话说,我们可以里面找到最权威的解释说明和使用方式。
❝
WHATWG
(Web Hypertext Application Technology Working Group) 是一个由一群来自不同公司的 web 开发者组成的组织,致力于推动 web 标准的发展。该组织成立于2004年。WHATWG
最知名的工作之一就是 HTML Living Standard(HTML5
),该标准定义了现代 web 页面的结构和行为。 除了HTML Living Standard
,WHATWG
还参与了一些其他规范的制定,包括Web Workers
、Web Storage
、Fetch API
等。 ❞
下面是我们截取的部分技术的文档。
在Console
中,我们看到如下的结构。
看到截图中,有一个namespace console
。我们可以从截图中得知。在内置console
中包含四部分
❝
❞
在之前我们讲浏览器内核时提到过。在chrome/chromium
的内核中,其中有很多C/C++
的代码。我们可以在chromium 在线仓库[2]进行查询。
此图中展示了在Chromium内核中console实现
回到WHATWG
中,我们就大家最熟悉的console.log
来简单聊聊,如何优雅的进行日志的输出。
我们平时做log
的输出是不是,用console.log(message)
console.log(`${变量名}_一堆硬编码的字符信息`)
其实哇,在message
中可以内嵌下面的格式化说明符。用于占位并输出指定的信息。
下面是各种说明符的使用案例。
// %s - 字符串格式化
console.log("输出字符串: %s", "前端柒八九!");
// %d or %i - 整数格式化
console.log("输出整数格式: %d", 42);
console.log("输出整数格式: %i", 42);
// %f - 浮点数格式化
console.log("输出浮点数格式: %f", 3.14159);
// %o - 以可折叠的优化多行样式显示一个对象,适合复杂对象
const obj = { age: 18, b: 'string', c: { name: "前端柒八九" } };
console.log("输出对象格式: %o", obj);
// %O - 以不可折叠的 JavaScript 对象格式化
console.log("用于简单的对象表示: %O", obj);
// %c - 应用 CSS 样式到输出
console.log("%c对文本进行样式化输出.", "font-size:20px; color:blue;");
我们将其复制到Source-Snippet
中进行验证。
最常见的断点类型是代码行断点
(就是我们经常用到的方式)。但是设置代码行断点可能效率较低,特别是如果我们不确定要查找的确切位置,或者如果我们正在处理大型代码库。
断点类型 | 用途 |
---|---|
代码行 | 在代码指定区域触发断点。 |
有条件的代码行 | 只在满足限定条件时,在指定地方触发断点 |
记录点 | 在不暂停代码运行的情况下向控制台输出日志 |
DOM | 在更改或删除特定 DOM 节点或其子节点时触发断点 |
XHR | 当 XHR URL 包含某个字符串模式时触发断点 |
事件监听器 | 在指定事件触发后触发断点 |
异常 | 在抛出已捕获或未捕获异常的代码时触发断点 |
函数 | 每当调用特定函数时触发断点 |
monitorEvents
是一个在浏览器开发者工具中使用的 JavaScript
方法,用于「监控指定元素上特定类型的事件」。这个方法通常用于调试和分析事件的触发情况。
❝一旦使用
monitorEvents
监控了某个元素上的事件,当该元素上触发相应类型的事件时,浏览器会在控制台中打印相应的事件信息,包括事件类型、事件目标等。 ❞
// 监控特定元素上的一个或多个事件类型
monitorEvents(element, eventTypes);
element
: 要监控的 HTML 元素。eventTypes
: 要监控的事件类型,可以是单个事件类型的字符串,或者是事件类型组成的数组。// 监控窗口内的点击事件
monitorEvents(window, 'click');
// 监控文档body上的键盘按键事件
monitorEvents(document.body, ['keyup', 'keydown']);
然后,我们还可以在控制台的Element
中直接选中元素,然后在Console
中输入对应的指令
在特定元素触发对应的事件后,在控制台就会打印除对应的Event
的信息。
上面,我们涉及到一个$0
变量。其实这是chrome-devtool
的一种内置变量。在Elements
选中一个元素时,我们就可以在Console
中查询对应的元素引用。
我们还可以通过getEventListeners($0)
来获取该元素上绑定的事件信息。
然后,我们还可以通过$0.addEventListener
来添加对应的事件。这样我们就不用通过class/id
来现获取元素,然后再调用addEventListener
了。
monitor
方法允许你监听特定函数的调用
// 定义一个示例函数
function myFn() { }
// 进行监控
monitor(myFn)
myFn()
// function myFn called
myFn(1)
// function myFn called with arguments: 1
有时候,我们想要代码中对函数想进行监控,我们还可以使用monitor
。
下面代码中,我们用Vite
启动一个React
项目。
当我们对即将要监控的代码胸有成竹时,也就是我们知道代码的确切位置,那么我们就可以「代码行断点」,DevTools
总是在执行此代码行之前暂停。
设置 DevTools
中的代码行断点:
Sources
选项卡Sources
的左侧文件目录中进行搜索⌘ P
的快捷键,通过文件名来搜索Add breakpoint
我们还可以采用「硬编码」的方式,通过debugger
在代码中打断点。
console.log("front");
console.log("789");
debugger;
console.log("456");
这种方式,是我们平时经常用到的,这里就不在展开说明了。
想必上面的打断点的方式大家都比较熟悉,现在我们再说一个大家平时可能会遇到的问题。
❝这种方式,墙裂推荐。效果不好,你打我。 ❞
假设现在有个循环,但是我们很确定的是,在循环的前半部分数据是好的,而在后半部分数据有问题。在之前,我们可能会通过「代码行断点」,在指定地方进行断点处理。如果是这种操作的话,那我们就需要对前面的数据也需要跟踪。
如果,下次遇到这种操作,我们可以用「有条件的代码行断点」 - 这种断点在我们想要跳过与我们的不关心的数据时非常有用。
Sources
选项卡Add conditional breakpoint
。一个对话框显示在代码行的下方。Enter
激活断点。一个带有问号的「橙色图标」出现在行号列的顶部。上面的代码中表示,当i>3
时候,才会触发断点,此时我们可以通过Watch
来查询我们想知道的的数据信息,并且还可以在Block
和Local
也会显示当前断点上下文中的数据信息。
例如,我们可以在触发断点后,使用console.table()
来查验localStorage
的信息。
如果函数的调用层级比较多,我们还可以把筛选条件置换成console.trace()
在断点触发时,来查验对应的函数调用层级。
同时,我们还可以在用一种近乎癫狂的方式,用我们自己的参数来为所欲为的让代码按照我们既定的路线进行运行。
我们通过对参数进行假定,然后在触发对应的函数时,按照我们给定的参数来运行函数
在代码层面id
值为1,但是我们可以通过「有条件的代码行断点」,将其替换成我们想要探查的数值。并且还不影响函数的运行顺序。
针对一个长list的循环,我们想通过一些方式来计算它的耗时,一般我们通过硬编码的方式使用console.time()/console.timeEnd()
在循环的前后进行处理。
其实,我们可以在起始点设置一个带有条件console.time('label')
的断点,在结束点设置一个带有条件console.timeEnd('label')
的断点。
只有当当前函数以 N 个参数调用时才暂停:arguments.callee.length === N
在函数「有可选参数」的情况下很有用。
只有当当前函数以错误的参数个数调用时才暂停:(arguments.callee.length) != arguments.length
程序化切换
使用全局布尔值对一个或多个条件断点进行门控:
上面的案例中,我们使用了setTimeout
来控制enableBreakpoints
,其实我们还可以手动触发window.enableBreakpoints = true;
来控制是否对某些断点进行开启和关闭。
使用「日志代码行断点」(logpoints
)可以在「不暂停执行且不用在代码中添加console.log()
调用的情况下」,将消息输出到控制台。其实,这种情况和「有条件的代码行断点」中加入console.log()
效果差不多。
设置日志点的步骤:
Sources
选项卡。Add logpoint
。一个对话框显示在代码行的下方。console.log(message)
调用相同的语法。Enter
激活断点。一个带有「两个点的粉色图标」出现在行号列的顶部。这个示例展示了在第 9 行设置的「日志代码行断点」,将变量i
的值输出到控制台。
使用Breakpoints
面板可以禁用
、编辑
或删除
代码行断点。
Breakpoints
面板「按文件对断点进行分组,并按行和列号进行排序」。我们可以对组执行以下操作:
复选框
单独启用或禁用组或断点。当我们禁用断点时,Sources
面板会使其在行号旁边的标记「变为透明」。
组具有上下文菜单。在Breakpoints
面板中,选中一个组然后右键,然后选择:
要编辑断点:
Sources
面板会使其在行号旁边的标记「变为透明」。编辑
以编辑,点击关闭
以删除它。假设我们有如下的代码
import { useRef } from "react";
/**
* DebuggerDemo组件
*/
export default function DebuggerDemo() {
const refSection = useRef<HTMLDivElement>(null);
return (
<div>
<h1>DOM Debugger Demo</h1>
<div ref={refSection}>
<div>原有的内容</div>
</div>
<button
onClick={() => {
if (refSection.current) {
const newDiv = document.createElement("div");
const newContent = document.createTextNode("前端柒八九");
newDiv.appendChild(newContent);
refSection.current.appendChild(newDiv);
}
}}
>
修改Section的子树
</button>
</div>
);
}
在button
触发时,会在div
中插入一个新的div
。
当我们想要在更改 DOM 节点或其子节点的代码上暂停时,可以使用 「DOM 变更断点」。
设置 DOM 变更断点的步骤:
Elements
选项卡。Break on
上,然后选择Subtree modifications
、Attribute modifications
或Node removal
。我们可以在以下位置找到 DOM 变更断点列表:
Elements
> DOM Breakpoints
面板。Sources
> DOM Breakpoints
侧面板。当我们触发上面button
时候,也就是触发了,div
的子树修改的断点,在动作触发的同时,我们就会跳转到指定的代码中。
在此时,我们也可以通过Watch
来查看指定数据的信息。和在Block
和Local
中查看上下文中的信息。
这里有一个点,额外提醒一下,上面的代码是用Hook
写的,而我们之前写过,Hook
其实就是一个闭包,在上面截图右侧部分是不是有一个Scope
。有兴趣的同学,可以打开看看。这里就不展示说明了。
假设,我们有如下的页面结构
import { useEffect, useState } from "react";
/**
* DebuggerDemo组件
*/
export default function DebuggerDemo() {
const [posts, setPosts] = useState<Array<{ id: string; title: string }>>([]);
const getPosts = async () => {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts"
// {
// method: "POST",
// }
);
const data = await response.json();
if (!data) return;
setPosts(data);
} catch (err) {
console.log(err);
} finally {
console.log("接口请求完成");
}
};
useEffect(() => {}, []);
return (
<div>
<h1>XHR Debugger Demo</h1>
{posts.map((item) => (
<div key={item?.id}>{item?.title}</div>
))}
<button onClick={() => getPosts()}>接口查询</button>
</div>
);
}
当我们想在 XMLHttpRequest
(XHR)的「请求 URL 包含特定字符串时」暂停时,可以使用 「XHR/fetch 断点」。DevTools
会在 XHR
调用 send()
的代码行上暂停。
❝这种情况有助于快速找到导致页面请求错误 URL 的
AJAX
或Fetch
源代码。 ❞
设置 XHR/fetch 断点的步骤:
Sources
选项卡。XHR Breakpoints
面板。Add
(添加断点)。XHR
请求的 URL
中时,DevTools
会暂停。Enter
确认。在点击查询后,我们就可以在指定的接口查询中,进行断点处理。
还有一点,我们需要额外的说明,我们用SPA
搭建页面,此时针对异步接口处理时,Axios
是一个王者级别的解决方案。
如果大家看过Axios
源码的话,就会知道,它其实就是在XHR
和Fetch
上做了一层封装。
所以,按道理,我们也可以通过XHR/fetch 断点
进行接口断点。但是呢,由于Devtool
有一个lognore List
。
默认情况下,Know third-party scripts form source map
这项是勾选的。如果我们想要在调试Axios
中的接口,我们就需要把这项给取消掉。
在处理完后,别记得把这个关闭掉,要不然bundle
中的debugger
也会会触发。
针对XHR
我们还可以在Event Listener Breakpoints
中进行对应事件的监听。(这个我们在下面「事件监听器断点」中介绍)
❝使用「XHR/fetch 断点」时,其实在工作中能帮助我们很大,比方说你接手了一个项目,然后发现在某个接口中出现了问题,按照我们以往的排查方式的话,是不是先在控制台找到对应的
url
,然后在代码中全局搜索这个url
。通过对应的本地方法,再次向上搜索,如果嵌套层级过多,那找着找着,把原来向定位的问题都遗忘了呢。 ❞
而有了「XHR/fetch 断点」,我们可以通过url
中特定的参数进行断点处理。并且这是一种「子上而下」的搜索方式。我们可以通过调用栈就能把调用路线很清晰的把握住。
当我们希望在事件被触发后运行的事件监听器代码上暂停时,请使用事件监听器断点。我们可以选择特定的事件,比如 click
,或事件的类别,比如所有鼠标事件。
设置事件监听器断点的步骤:
Sources
选项卡。Event Listener Breakpoints
面板。DevTools
显示了一系列事件类别,比如 Animation
。假设我们存在如下的页面
import { useEffect, useState } from "react";
/**
* DebuggerDemo组件
*/
export default function DebuggerDemo() {
const [posts, setPosts] = useState<Array<{ id: string; title: string }>>([]);
useEffect(() => {
const fetchData = () => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
setPosts(data);
}
};
xhr.open("GET", "https://jsonplaceholder.typicode.com/posts", true);
xhr.send();
};
fetchData();
}, []);
const handleClick = (e: HTMLButtonElement) => {
console.log(e);
};
return (
<div>
<h1>Event Debugger Demo</h1>
<button
onClick={() => handleClick(event as unknown as HTMLButtonElement)}
>
按钮点击
</button>
{posts.map((item) => (
<div key={item?.id}>{item?.title}</div>
))}
</div>
);
}
然后,我们在选中我们想要监听的事件。
当然,如果我们想看React
内部的处理逻辑,我们可以在lgnore list
中将Know third-party scripts form source map
打开,这样的话我们在断点触发后,也能查看框架内部的处理逻辑。
当我们想在错误时进行断点跟踪时,可以使用「异常断点」。
在Sources
选项卡的Breakpoints
面板中,启用以下选项中的一个或两个,然后执行代码:
Pause on uncaught exceptions
front789
的未定义的变量,并且没执行捕获操作。Pause on caught exceptions
❝墙裂建议,在我们开发阶段,将
Pause on uncaught exceptions
打开,这样可以让浏览器来帮我们找到我们代码不正确的地方。 ❞
大家有没有遇到过,在进行log
时候,想复制某些数据,但是只能在log
输出到控制台后,才能复制。并且这些数据只是单纯的展示,想选中也不好处理。
例如:
其实,我们可以使用copy()
API 将浏览器中的特定信息「直接复制到剪贴板,而不会有任何字符串截断」。
copy(document.documentElement.outerHTML)
copy(performance.getEntriesByType("resource"))
copy(JSON.parse(blob))
copy(localStorage)
我们想检查一个只有在条件满足时才出现的 DOM 元素。
当我们在first input
悬浮时候,想查看second input
时候,鼠标移出first input
后,后者立马就消失不见了。
我们可以利用如下代码:
setTimeout(function () {
debugger;
}, 5000);
这使我们有 5 秒的时间触发 UI,然后一旦 5 秒计时器结束,JS 执行将暂停,没有任何东西会让你的元素消失。我们可以自由移动鼠标到开发工具而不失去元素:
当 JS 执行暂停时,我们就可以检查元素、编辑其 CSS、在 JS 控制台中执行命令等。
❝在检查依赖于特定光标位置、焦点等 DOM 时很有用。 ❞
(function () {
let last = document.activeElement;
setInterval(() => {
if (document.activeElement !== last) {
last = document.activeElement;
console.log("Focus changed to: ", last);
}
}, 100);
})();
[1]
WHATWG: https://spec.whatwg.org/
[2]
chromium 在线仓库: https://source.chromium.org/chromium/chromium/src;l=21