WSの小屋

文章目录

我们在查看各种文档时,经常能够看见侧边的目录项

解析文章目录

不难看出,目录的每一项都是正文的某个标题(h1~h6)

前端拿到文章内容时,解析出文章内的大小标题,将其作为则边栏的目录索引

解析的方法很简单,直接读取对应的 dom 元素,遍历其所有子元素,提取出 h1~h6 标签就可以了

typescript 复制代码
const doc = document.getElementById("article");

const titles = [];

doc.childNodes.forEach(e => {
	if (/h\d/i.test(e.nodeName)) {
		titles.push(;
	}
});

// #article 元素内的h1~h6标签
console.log(titles);

此时我们只需要遍历这个列表就可以生成一个简易的目录结构了

通过设置a.href为对应标题的id,还可以精确定位到对应标题的位置

根据不同的nodeName设置li的class,就可以在css中设置不同的样式了

vue 复制代码
<template>

  <ul>
    <li v-for="item in titles" :class="item.nodeName">
      <a :href="`#${item.id}`">{{ item.innerHTML }}</a>
    </li>
  </ul>
  
</template>

大功告成了,这样其实就已经足够使用了

不过这个列表是一维的,我们知道h1~h6相当于父子级的关系,我们如果想要解析成一个树形结构,该如何操作呢?

例如:

html 复制代码
<h1>一级标题</h1>
<h2>二级标题</h2>
<h2>二级标题</h2>
<h1>一级标题</h1>
<h2>二级标题</h2>
<h3>三级标题</h3>
<h3>三级标题</h3>
<h1>一级标题</h1>

转换成:

html 复制代码
<ul>
  <li>一级标题</li>
  <ul>
    <li>二级标题</li>
    <li>二级标题</li>
  </ul>
  <li>一级标题</li>
  <ul>
    <li>二级标题</li>
    <ul>
      <li>三级标题</li>
      <li>三级标题</li>
    </ul>
  </ul>
  <li>一级标题</li>
</ul>

解析文章目录-树形结构

对于树形结构,常常要用到递归

递归的精髓就在于,将问题分解为规模更小的相同问题

我们先声明一下类型:

typescript 复制代码
interface Title {
	el: HTMLElement;
	children: Title[];
}

我们用el属性存储标题元素对象,并且将他的所有子标题集存储在children属性中,children的类型等同于自己

这便有了一个初步的数据结构,接下来我们将解析出的文章一级列表,再次解析为该树形结构

如:

html 复制代码
<h1>一级标题</h1>
<h2>二级标题</h2>
<h2>二级标题</h2>
<h1>一级标题</h1>
<h2>二级标题</h2>
<h3>三级标题</h3>
<h3>三级标题</h3>
<h1>一级标题</h1>

我们把目光聚焦在第一个h1,它有两个二级标题作为子集,它转换后的结构应该类似于

typescript 复制代码
{
  el: <h1>一级标题</h1>,
  children: [
    {
      el: <h2>二级标题</h2>
      children: [],
    },
    {
      el: <h2>二级标题</h2>
      children: [],
    },
  ]  
}

也就是说,第一个h1标签与第二个h1标签之间,所有的标题元素都是h1标签的子集!

我们先确定一下入参和返回值,声明一个函数:

typescript 复制代码
// 这里的入参接受一个一维的Title数组,将会返回一个Title类型树形结构
const r = (list: { el: HtmlElement }[]): Title[] => {
  const result: Title = [];
  ...
  return result;
}

假设入参为:

typescript 复制代码
const list =[
  { el: <h1>一级标题</h1> },
  { el: <h2>二级标题</h2> },
  { el: <h2>二级标题</h2> },
  { el: <h1>一级标题</h1> },
  { el: <h2>二级标题</h2> },
  { el: <h3>三级标题</h3> },
]

那么第一个h1标签的子集就相当于:

typescript 复制代码
list.slice(1, 4);

这个slice的参数都可以计算得出,只需要预先知道这个h1的index:

  • 对于参数一,相当于这个h1的index加1

  • 对于参数二,相当于下一个h1的index,如果没有则是列表的长度

我们将截取出来的子集作为参数递归下去,便可完成树形结构的转换

接下来,上代码:

typescript 复制代码
// 这里的入参接受一个一维的Title数组,将会返回一个Title类型树形结构
const r = (list: { el: HtmlElement }[]): Title[] => {
  const result: Title = [];
  // 一级标题 > 二级标题 > 三级标题…… 所以这里用了Math.min,这里提取了标题元素标签中的第二个字符作为对比
  // 这里找出入参的最大标题级别
  const min = Math.min(...list.map(item => +item.el.nodeName.slice(1)));
  // 遍历的时候过滤了一下,这里只需要处理 直接子集 ,而孙、曾孙级等等交给函数递归下去就行了
  for (const item of list.filter(item => +item.el.nodeName.slice(1) === min)) {
  	const start = list.indexOf(item) + 1; // 当前索引加1
  	let end = list.slice(start).findIndex(sub => sub.el.nodeName == item.el.nodeName); // 下一个的索引
    end = end == -1 ? list.length : end // 如果没有则是列表的长度
  	result.push({
  		el: item.el,
  		children: r(list.slice(start, end)), // 将截取出来的子集递归下去
  	});
  }
  return result;
}

完整代码

typescript 复制代码
export const resolveArticleTitles = <T extends Element>(doc: T) => {
	interface Title {
		el: HTMLElement;
		children: Title[];
	}

	const titles: { el: HtmlElement }[] = [];

	doc.childNodes.forEach(e => {
		if (/h\d/i.test(e.nodeName)) {
			titles.push({
				el: e as HTMLElement
			});
		}
	});

	const r = (list: { el: HtmlElement }[]) => {
		const result: Title[] = [];
		const min = Math.min(...list.map(item => +item.el.nodeName.slice(1)));
		for (const item of list.filter(item => +item.el.nodeName.slice(1) === min)) {
			const start = list.indexOf(item) + 1;
			let end = list.slice(start).findIndex(sub => sub.el.nodeName == item.el.nodeName);
      end = end == -1 ? list.length : end
			result.push({
				el: item.el,
				children: r(list.slice(start, end),
			});
		}
		return result;
	};

	return r(titles);
};

Comments | 0条评论