最近阅读自己的文章时有感,没有目录的体验真的很糟糕。
首先是汲取信息方面,缺少目录就意味着失去了一种概览全文的重要途径,想要进一步了解文章只能从头看到尾;其次是操作方面,缺少目录说明没有内容导航,如体现为无法使用锚点跳转,只能依靠滚动&滑动的方式查看文章内容。
当文章较长时,无论是从哪个方面,阅读文章似乎都会变为一种令人厌烦的事情,此乃大忌,所以还是觉得给文章加上目录为好。
本着尽量不改动代码的原则,所以决定在前端层面实现该功能,类似于插件,无非就是新增个外部js,并同步修改生成html文件的模板。
一、方法设计思路
注,本文是基于Markdown文本转换的dom实现方法。
Markdown是一种轻量级标记语言,使用纯文本格式编写,具有轻量化、易读易写的特点,如下一段的md文本。
# 节点1
## 节点2
### 节点3
### 节点4
## 节点5
示例中展示了一组标题元素,其文体结构呈扁平化,且层次分明。
规则很明显,相邻&处于同一层次的节点,必定是兄弟节点,如“节点3”与“节点4”、“节点2”与“节点5”;也必定是上一层次节点的子节点,如“节点3”与“节点4”是“节点2”的子节点,“节点2”与“节点5”是“节点1”的子节点。
因此,可利用这种具有层次关系的标题元素,作为生成目录的依据。
在转为dom后,也依然保持这样的规律。这很好理解,一般情况下,Markdown相关的转换器,只会根据md文本标题元素中“#”的数量,一比一转换为对应的heading标签,而不会做此外的任何改变,如“# 节点1”将转为<h1>节点1</h1>
,"## 节点2"将转为<h2>节点二</h2>
,以此类推。
所以,现在只需设计一个自上往下循环heading标签,并找出其全部子节点(包括子节点的子节点)的方法。
如上述md转为dom节点后,应该为:
# 省略了一些获取的步骤...
[h1, h2, h3, h3, h2]
调用方法后,应该得到如下数据:
h1是最外层的节点,其子节点为两个h2;两个h3都是第一个h2节点的子节点。
[
{
h1,
children: [
{h2, children: [{h3, h3}]},
{h2}
]
}
]
二、方法实现
分为两个步骤来完成,generateTree()用于创建最外层的节点,makeChildren()用于创建某个节点的子节点。
实现方式也较简单,只是一些朴实无华的递归和循环。
1、generateTree()
// nodes为doc文档中获取的heading元素数组
function generateTree(nodes) {
const tree = [];
while (nodes.length > 0) {
const current = nodes.pop();
current.level = 1;
tree.push({
text: current._node.textContent,
children: makeChildren(current, nodes),
...current
});
}
return tree;
}
2、makeChildren()
function makeChildren(current, nodes) {
const children = [];
while (nodes.length > 0) {
const node = nodes[nodes.length - 1];
// num为h标签的序号;当遇到大于当前节点的节点时,表明下个节点node并非上个节点current的子节点,结束循环
if (node.num <= current.num) {
return children;
}
// 移除节点
nodes.pop();
node.level = current.level + 1;
children.push({
text: node._node.textContent,
children: makeChildren(node, nodes),
...node
});
}
return children;
}
三、使用方法
1、获取dom节点
以下假设“.markdown-body > .content”是由某个markdown转换的dom元素。
const mbc = document.querySelector('.markdown-body > .content');
const children = mbc.children;
const hChildren = [];
const reg = /^H\d$/;
for (let child of children) {
// 使用正则表达式筛选出heading元素。
if (reg.test(child.tagName)) {
hChildren.push({
_node: child,
num: +child.tagName.slice(1)
});
}
}
// 将数组反转,以供后续使用pop()取出数据
hChildren.reverse();
2、根据dom节点生成树
将步骤1中得到的数据传入方法。
const tree = generateTree(hChildren);
以文章某云滑动认证码逆向为例,其部分内容截图如下:
图1 《某云滑动认证码逆向》- 截图1
图2 《某云滑动认证码逆向》- 截图2
调用方法后应该得到如下结果:
图3 树形结构数据
之后再通过该数据生成目录即可。
图4 使用树形结构数据生成的文章目录
之前总觉得文章页面缺了些什么,仔细想想目录就是其中一个,现在整体看上去充实了很多,也比较实用。
说起来,这个功能其实很早就纳入开发计划中,只是久而久之忘记了,以至于今日才落地实现。
以上。