目录生成小函数代码分析

针对文章内容页,我的博客皮肤使用了一段非常小的函数来生成文章目录。

1. 分析文章内容DOM结构

检查文章内容的DOM结构,可以发现所有的标题都带有类似b3_solo_hxxxx的id,并且所有的标题均为<h />标题标签。
因此我们定义标签与其权重之间的关系:

const priorities = {
  h1: 5,
  h2: 4,
  h3: 3,
  h4: 2,
  h5: 1,
};

2. 获取所有标题节点

使用CSS3 [attribute^=value]选择器可以很方便的获取到所有标题

const titles = $('[id^=b3_solo_h]');

关于CSS选择器,可以在这里查看更多参考。

3. 对所有标题进行处理

拿到了所有标题之后,我们期望可以根据标题的DOM的tag来进行整理从而生成目录结构。

目标DOM元素结构

h1
h2
h2
h3
h3
h4
h1
h2
h3
h1

希望生成的结构:

h1
  h2
  h2
    h3
    h3
      h4
h1
  h2
    h3
h1
    h3 # 注意这里,前面是2层缩进

所以我们对之前获取到的$titles进行遍历。首先获取它的DOM所对应的优先级,之后将当前元素的优先级与上一个元素的优先级进行比较。如果:

  1. 当前元素的优先级 < 上一个元素的优先级:开始新的缩进并插入一层目录
  2. 当前元素的优先级 = 上一个元素的优先级:在当前缩进下插入一层目录
  3. 当前元素的优先级 > 上一个元素的优先级:结束上一个缩进并插入一层目录

我们可以根据逻辑写出代码

let tpl = '<ul class="article__contents">';
let prevPriority; // 上一个元素的优先级
let firstPriority; // 第一个元素的优先级

$titles.each((index, dom) => {
  const priority = priorities[dom.tagName.toLowerCase()];
  const title = $titles.eq(index).text();
  const item = `<li><a href="javascript:void(0);">${title}</a></li>`;

  if (!priority) { // 如果没有获取到优先级的则不处理这层目录
    return;
  }

  if (!firstPriority) { // 是第一个目录的情况需要特殊处理
    tpl += item;
    firstPriority = priority;
  } else { // 不是第一个目录的情况,可以按照之前分析的逻辑处理
    if (priority === prevPriority) {
      tpl += item;
    } else if (priority < prevPriority) {
      tpl += Array(prevPriority - priority).fill('<ul>').join('') + item;
    } else if (priority > prevPriority) {
      tpl += Array(priority - prevPriority).fill('</ul>').join('') + item;
    }
  }

  prevPriority = priority;
});

注意这里用了很奇怪的Array(prevPriority - priority).fill('<ul>').join('')代码,主要是为了处理有的人可能会h1后面跟h4,如果只是单纯的使用一个<ul>的话会导致缩进混乱。

扫尾

使用上述代码,针对上述DOM结构可以生成如下HTML代码片段:

<ul>
  <li>h1</li>
  <ul>
    <li>h2</li>
    <li>h2</li>
    <ul>
      <li>h3</li>
      <li>h3</li>
      <ul>
        <li>h4</li>
      </ul>
    </ul>
  </ul>
  <li>h1</li>
  <ul>
    <li>h2</li>
    <ul>
      <li>h3</li>
    </ul>
  </ul>
  <li>h1</li>
  <ul>
    <ul>
      <li>h3</li>

我们发现还缺少了最后一级的闭标签。因此需要拿之前设置的prevPriorityfirstPriority做对比。针对上述情况,prevPriority为3,firstPriority为1,因此我们总共需要为当前的目录层级补上2层ul闭标签。除此之外还需要为整个目录补上1个闭标签。

此逻辑可以通过代码处理:

tpl += Array(firstPriority - prevPriority + 1).fill('</ul>').join('');

至此,我们成功生成了整个目录机构的DOM字符串。

完整代码

因为开发工具的问题真实代码使用了ES3的书写方式,不影响阅读。

function initContents() {
  var priorities = {
    h1: 5,
    h2: 4,
    h3: 3,
    h4: 2,
    h5: 1,
  };
  var $titles = getArticleTitles(); // 实际上就是 var titles = $('[id^=b3_solo_h]'); getArticleTitles() 这个函数做了一层缓存处理

  if ($titles.length === 0) {
    $('.J_article__contents').hide();
    return;
  }

  var tpl = '<ul class="article__contents">';
  var prevPriority;
  var firstPriority;

  $titles.each(function(index, dom) {
    var priority = priorities[dom.tagName.toLowerCase()];
    var title = $titles.eq(index).text();
    var item = '<li><a href="javascript:void(0);" data-target="#' + $titles.eq(index).attr('id') + '">' + title + '</a></li>';

    if (!priority) {
      return;
    }

    if (!firstPriority) {
      tpl += item;
      firstPriority = priority;
    } else {
      if (priority === prevPriority) {
        tpl += item;
      } else if (priority < prevPriority) {
        tpl += Array(prevPriority - priority).fill('<ul>').join('') + item;
      } else if (priority > prevPriority) {
        tpl += Array(priority - prevPriority).fill('</ul>').join('') + item;
      }
    }

    prevPriority = priority;
  });

  tpl += Array(firstPriority - prevPriority + 1).fill('</ul>').join('');
  $('.J_article__contents--container').append(tpl);
}

大家如果有更好的实现欢迎评论~