当你为freeCodeCamp、Hashnode、Medium或DEV.to创建文章时,可以通过添加目录来帮助读者更好地理解文章内容。在这篇文章中,我将介绍如何利用JavaScript和浏览器的开发者工具来制作目录。虽然本文以Google Chrome开发者工具为例进行讲解,但同样的方法也适用于其他现代浏览器。
本文中的步骤需要针对每个平台分别操作一次。一旦你掌握了相应的代码,以后每次创建目录时都可以使用这些代码。需要注意的是,如果某个平台的设置发生了变化,可能就需要调整脚本了。
目录
浏览器开发者工具
浏览器开发者工具是浏览器的一个扩展功能,它允许你查看和操作DOM结构(文档对象模型),DOM是浏览器将HTML内容以树形结构存储在内存中的形式。此外,该工具还提供了JavaScript控制台,你可以在其中编写简短的代码片段来进行测试。虽然它还有很多其他功能,但在这里我们只使用这两项。
要打开Google Chrome中的开发者工具,可以按F12键,或者右键点击页面然后选择“检查”。

上图是DevTools的截图,其中展示了本文的内容预览。在右侧,你可以看到被选中的H1 HTML标签(即页面标题)以及应用于该标签的CSS样式。你所看到的这种树状结构其实就是DOM。
JavaScript控制台
我们需要能够使用JavaScript控制台。在Google Chrome浏览器中,可以通过按F12键、右键点击页面然后选择“检查”选项,或者使用快捷键CTRL+SHIFT+C(Windows/Linux系统)/CMD+OPTION+C(Mac系统)来打开控制台。
在Chrome DevTools中,你可以在工具栏顶部选择“控制台”标签页,但这样会隐藏DOM树结构。更好的方法是打开底部的抽屉式界面:只需点击右上角的三个点,然后选择“显示控制台抽屉”即可。

DevTools的工具界面如下所示:

控制台实际上是一种所谓的“读-执行-打印循环”界面。在这种经典的设计中,你可以输入一些命令(通常是JavaScript代码),按下回车键后,这些代码就会在DevTools所连接的页面环境中被执行。

上图中,你可以看到通过控制台执行的页面警告功能。
理解DOM结构
创建目录结构的第一个步骤就是检查DOM结构并找到标题元素。这些标题元素通常表现为H1…H6标签。其中,H1标签往往代表页面的标题;在理想情况下,确实应该如此。
在我的示例中,标题元素的格式如下:
<h2 id="heading-dev-tools">Dev Tools<>/h2>
这篇文章目前只使用了H2标签来表示标题,但后续内容中我会解释如何创建嵌套的目录结构。
现在,借助DevTools,我们可以编写代码来查找所有的标题元素:
document.querySelectorAll('h2[id], h3[id], main h4[id]);
以我在freeCodeCamp上写的文章为例,这段代码会返回如下结果:
NodeList(5) [h2#heading-dev-tools, h2#heading-javascript-console, h2#heading-understanding-the-dom-structure, h2#trending-guides.col-header, h2#mobile-app.col-header]
首先,这个结果是一个NodeList对象,我们需要将其转换为数组。其次,除了我们原本想要获取的标题元素外,这个数组中还包含了网站结构中的一些其他标题元素,并非文章的主要内容。因此,我们需要找出那些真正属于我们所需标题元素的父元素。
你可以在包含这篇文章内容的空白页面上右键点击,然后选择“检查元素”。在我们的例子中,系统找到了这个元素。因此,我们可以将选择器改写为:
document.querySelectorAll('main h2[id], main h3[id], main h4[id]);
这样,它就会只返回我们需要的标题元素了。
[id]这个属性选择器。至少在freeCodeCamp平台上是不需要的。如何用Markdown格式创建目录
很多博客平台都支持Markdown格式,因此我们首先会学习如何使用它来创建目录。
首先,我们需要将得到的NodeList对象转换为数组。我们可以使用展开运算符来实现这一点:
[...document.querySelectorAll('main h2[id], main h3[id], main h4[id]')];
接着,我们可以遍历这个数组,为每个标题元素生成对应的Markdown链接。
const headers = [...document.querySelectorAll('main h2[id], main h3[id], main h4[id]')];
headers.map(function(node) {
// H2标题元素的缩进应为0
const level = parseInt(node.nodeName.replace('H', '')) - 2;
const hash = node.getAttribute('id');
const indent = ' '.repeat(level * 2);
return `\({indent}* [\){node.innerText}](#${hash})`;
});
最终生成的代码如下:
(4) ['* [Dev Tools](#heading-dev-tools)', '* [JavaScript Console](#heading-javascript-console)', '* [Understanding the DOM Structure](#heading-understanding-the-dom-structure)', '* [What to do if I don’t have headers?](#heading-what-to-do-if-i-dont-have-headers)']
如果想要获取这些链接对应的文本,我们可以将数组中的所有元素用换行符连接起来,然后使用console.log来显示结果。如果不使用换行符,输出的结果会包含多个连续的换行字符。
const headers = [...document.querySelectorAll('main h2[id], main h3[id], main h4[id]')];
console.log(headers.map(function(node) {
// H2标题元素的缩进应为0
const level = parseInt(node.nodeName.replace('H', '')) - 2;
const hash = node.getAttribute('id');
const indent = ' '.repeat(level * 2);
return `\({indent}* [\){node.innerText}](#${hash})`;
}).join('\n'));
这篇文章的输出结果将会是这样的:
* [开发工具](#heading-dev-tools)
* [JavaScript控制台](#heading-javascript-console)
* [理解DOM结构](#heading-understanding-the-dom-structure)
* [用Markdown创建目录结构](#heading-creating-toc-in-markdown)
* *这是一个虚拟的标题栏*
我添加了一个虚拟的子标题。虽然某些平台在编写文章时不支持Markdown格式,但当用户将Markdown内容复制粘贴后,这些平台通常能够正常显示这些内容。文章开头的目录结构就是通过复制并粘贴上面那段JavaScript代码生成的Markdown内容来创建的。
如何创建HTML目录结构
如果你的平台不支持Markdown格式(比如Medium),你可以先生成HTML代码,预览后将其复制到剪贴板中,然后粘贴到你正在使用的平台的编辑器中,这样格式应该会保持不变。
标签内的,因此选择器也需要相应地进行调整。要将Markdown转换为HTML,你可以使用任何在线工具,但下面这段代码会教你如何自己编写这样的转换脚本。一旦你掌握了这个方法,以后操作起来就会更快了。
const headers = [...document.querySelectorAll('main h2[id], main h3[id], main h4[id]')]
function indent(state) {
return ' '.repeat((state.level - 1) * 2);
}
function closeUlTags(state, targetLevel) {
while (state.level > targetLevel) {
state.level--;
state_lines.push(`${indent(state)}
`);
}
}
function openUlTags(state, targetLevel) {
while (state.level < targetLevel) {
state_lines.push(`${indent(state)}
{
const level = parseInt(node.nodeName.replace('H', ''));
closeUlTags(state, level);
openUlTags(state, level);
const hash = node.getAttribute('id');
state_lines.push `\({indent(state)}${node.innerText} `);
return state;
}, { lines: [], level: 1 });
closeUlTags(result, 1);
console.log(result(lines.join('\n'));
这就是这篇文章中提到的代码所生成的HTML输出结果:
我在文末添加了一些标题,这样你们就可以看到,这种方法适用于任何层级的嵌套标题。需要注意的是,列表中的第一个元素也是目录。
将生成的HTML代码复制到编辑器中
大多数所谓的WYSIWYG编辑器都是使用HTML格式的,因此你应该可以将带有格式设置的HTML代码复制并粘贴到这些编辑器中。最简单的方法就是将代码保存为文件,然后打开该文件并选中所需的文本。

如果我没有标题该怎么做?
你需要找到那些可以通过CSS进行操作的元素。如果这些元素是带有特定类的p标签(比如“header”类),那么你可以使用p.header代替h2。
如何为DEV.to创建目录
如果你使用的DOM结构不同,也可以使用不同的DOM方法来提取所需的元素。例如,在DEV.to上,标题的格式是这样的:
<h2>
<a name="overview" href="#overview">
</a>
Overview
</h2>
因此,选择器应该写作main h2。但是当你执行这段代码时:
[...document.querySelectorAll('main h2, main h3, main h4')];
你会发现,页面上的标题数量远远超过了正文内容。幸运的是,CSS中提供了一个新的选择器:has()。因此,针对一个特定标题的选择器可以写成main h2:has(a[name])。
以下是完整的代码:
const selector = 'main h2:has(a[name]), main h3:has(a[name]), main h4:has(a[name])';
const headers = [...document.querySelectorAll(selector)];
console.log(headers.map(function(node) {
// H2标题的缩进应为0
const level = parseInt(node.nodeName.replace('H', '')) - 2;
// 这种方法可以获取链接地址
// 你也可以直接访问标签的href属性,并从结果字符串中删除#符号
const hash = node.querySelector('a').getAttribute('name');
const indent = ' '.repeat(level);
return `\({indent}* [\){node.innerText}](#${hash})`;
}).join('\n'));
总结
创建目录可以帮助读者更好地理解文章内容。由于大多数人并不会通读整篇文章,他们只会浏览自己感兴趣的部分。此外,也有很多研究指出目录对SEO效果有积极影响。因此,对于篇幅较长的文章来说,添加目录绝对是值得的。
如您所见,只要具备一些网页开发方面的知识,创建目录结构其实并不困难。
如果您喜欢这篇文章,不妨在社交媒体上关注我:(Twitter/X、GitHub和/或LinkedIn)。您也可以访问我的个人网站,以及我的新博客。