为你的博文构建目录

1. 写在前面

这要从前几天看Hibernate的用户手册说起。

这份手册确实挺长,而且还是全英文,对于刚过四级的我来说,看着确实有些“吃力”。这种“吃力”表现在,我经常看着看着就忘了自己看到哪了,对于前面看过的内容的印象特别模糊,于是我就在思考为什么会这样。

是有许多不认识的单词或不懂的语法吗?应该不是。事实上,一般技术文档除了一些专业术语外基本都是一些常见词汇,除了一些约定的或者是有典故的句子外基本上也没有比较复杂的语法;

是文章内容专业太强了吗?好像也不是,我一路读过来并没有感到有无法理解的地方。事实上,就我自己在学习过程中看到过的一些技术或项目的官方文档与相关的一些中文博客对比来看,往往这些“官方文档”讲解的更加简单,内容更加丰富。

是文章太长了吗?这个好像有点关系。本来自己目前刚过四级的英语水平还是在9年义务教育加3年高中教育的煎熬中获得的,这导致对英语产生了天然的排斥心理。看简短的英文文章还好,因为自己知道再坚持坚持就看完了;但是当看到一大段一大段,需要拉动滚动条好一会才能见底的英语文章时,我的内心是复杂的。

最后想了想,我觉得最关键的原因还是不会用英语去思考。就是说不能直接去领会一句话的意思,必须要将一句话在脑袋中翻译成中文才能去思考它传达的意思。再加上我翻译的速度还慢,虽然每句话在读的时候确实是明白了它的意思,但是当遇到较长文章的文章时,我需要在大脑中启动大量翻译操作,这就导致了阅读英语文章时很大的精力花费在了“翻译”上,对文章整体的逻辑无法把控。

于是我在知乎上搜索了相关问题,确实很多人都有这种疑问,并且也有人做了些解释,给了些解决办法。比如这个这个

好吧,上面扯了半天废话,我其实是想说,对一个写作者而言,除了将自己表达的内容清晰有条理的展现外,为文章构建出一个目录是非常重要的,并且这个目录最好是固定在屏幕的某个地方的,不会随着页面的滚动而滚动,它最好还能指示读者当前读到了哪一章节。这样能够让读者不会在“浩瀚”的文章中“迷失”。

于是,昨天我花了一两个小时的时间为我的博文做了个目录,你稍微向下滚动一下这篇博文,应该就能看到。

在此将整个过程记录分享出来,希望能对有相同需求的你有所帮助。

2. JS权限申请

目录需要用JS生成,首先当然是要先申请JS权限。这个需要在自己的管理页面点击申请,我这里已经申请成功了:

3. 根据文章生成目录

下面介绍如何根据文章生成目录。这个的大致思路就是先提取出文章内的各级标题,然后根据标题的级数(比如h1标签的级数是1)在目录中使用对应的“缩进”。

提取标题很简单,直接使用document.querySelectorAllAPI,即:

var aTitle = document.querySelectorAll('h1, h2, h3, h4, h5, h6');

返回的是各个h标签对应的dom对象组成的数组,并且顺序正好与我们文章中各标题出现的顺序一致。

有了“标题数组”,我们可以遍历它,在遍历的每趟中,比较当前标题与上一趟的标题的级数:

  1. 若当前标题级数较大,便“向右缩进”一次;
  2. 若相等,便“直接插入”;
  3. 若当前标题级数级数较小,便“向左缩进”一次;

这里向右“缩进”反映在代码中就是,我们在当前ul标签里面插入一个li标签,并且在它里面会再嵌套一个ul标签;向左“缩进”反映在代码中就是我们会“退回”到上一个ul标签;而“直接插入”的意思是,仅仅在当前ul标签里面插入一个li标签。

举个栗子:假如我们有一个文章是这样子的:

<h1>白夜行</h1>
<h2>第一章</h2>
<h3>第一节</h3>
<h3>第二节</h3>
<h3>第三节</h3>
<h2>第二章</h2>
<h3>第一节</h3>
<h3>第二节</h3>
<h2>第三章</h2>
<h3>第一节</h3>
<h3>第二节</h3>

渲染出来是这个样子:

因此,我们需要得到这样的一个目录:

图中,一种颜色的矩形代表一个ul,为了说明和编写代码的方便,我们为每一个ul赋予一个level属性,其值等于它里面的标题的级数。比如上图中红色的ullevel应为1,因为它里面的标题"白夜行"的级数是1;而最里面的粉色的ul的级数应为3,因为它里面的标题"第一节"和"第二节"是3级标题。

根据上面的分析,我们会从上至下依次遍历各个标题。但在遍历之前我们应该提前创建一个根ul,遍历的第一个标题由于没有“上一个标题”,因此做特殊处理,直接插入到“当前ul”,也就是根ul中;然后遍历到“第一章”,由于"第一章"的级数(为2)大于“当前ul”(红色ul)的级数(为1),因此需要“向右缩进”标题,即创建一个新的lili里面创建一个新的ul,将"第一章"放入这个新的ul中,最后我们需要更新“当前ul”为颜色是绿色的那个ul ;接下来遍历到"第一节",和“第一章”一样,也需要创建新的liul,并将“当前ul”更新为蓝色的ul;再接着是“第二节”,由于“第二节”的级数(为3)和“当前ul”(蓝色的ul)的级数一样,因此直接将"第二节"插入到“当前ul”中;当遍历到“第二章”(级数为2)时,此时的“当前ul”是蓝色ul(级数为3),该ul级数较大,因此需要“向左缩进”标题,即将“当前ul”“回退”到上一“当前ul”(绿色ul),然后向“当前ul”中插入标题。以上就是3种所有的情况,后面的遍历过程都类似。

有了上面的分析,我们可以很快写出如下的构建目录的代码:

function buildContents(rootId) {
    var aTitle = document.getElementById(rootId).querySelectorAll('h1, h2, h3, h4, h5, h6');
    var stack = [];
    var oRoot = document.createElement('ul');
    stack.push(oRoot);
    document.getElementById('contents').appendChild(oRoot);
    for (var i = 0; i < aTitle.length; i++) {
        var oTitle = aTitle[i];
        var level = parseInt(oTitle.nodeName.substring(1, 2)); // 当前标题的级数
        var oTop = stack[stack.length - 1]; // 得到栈顶元素
        if (i == 0)
            oTop.level = level;

        while (level < oTop.level) {
            // 向左缩进
            stack.pop();
            oTop = stack[stack.length - 1];
        }

        var oLi = document.createElement('li');
        oTop.appendChild(oLi);
        if (level == oTop.level) {
            // 直接插入
            oLi.textContent = oTitle.textContent;
            continue;
        } else if (level > oTop.level) {
            // 向右缩进
            var oUl = document.createElement('ul');
            oUl.level = level;
            oLi.appendChild(oUl);
            oLi.style.listStyle = 'none';
            var oChildLi = document.createElement('li');
            oChildLi.textContent = oTitle.textContent;
            oUl.appendChild(oChildLi);
            stack.push(oUl);
            oTop = oUl;
            continue;
        }
    }
    return oRoot;
}

事实上,上面的代码是有bug的,或者说,它只能对“标准”目录进行处理,对于“不标准”的目录,处理起来就有问题了。比如我们将上面文章中的“第一章”删除,那么用它生成的目录是这个样子的:

显然不对,因为按道理应该是这个样子:

考虑一下问题产生的原因,是因为我们默认认为下一级标题一定在“当前ul”下,但事实上这还应该参考之后出现的标题的级数。比如在1级ul后出现了一个3级标题,是否将该3级标题直接放在该ul下,取决于之后的标题有没有2级标题:没有则直接插入;否则应该新建一个空的ul,再在其内建立一个ul用来放当前标题。这就要求我们必须提前知道所有的标题的级数,因此可以这么改进我们的代码:

function buildContents(rootId) {
    var aContents = [];
    var aTitle = document.getElementById(rootId).querySelectorAll("h1, h2, h3, h4, h5, h6");
    var checkArray = [false, false, false, false, false, false]; // 各个位置表示是否有h1, h2, ..., h6标签
    // 第一次遍历,收集相关信息
    for (var i = 0; i < aTitle.length; i++) {
        var level = parseInt(aTitle[i].tagName.substring(1, 2));
        checkArray[level - 1] = true;
    }
    // 第二次遍历,构建目录树
    var oRoot = document.createElement('ul');
    for (var i = 0; i < 6; i++) {
        if (checkArray[i]) {
            oRoot.level = i + 1;
            break;
        }
    }
    var ulStack = [oRoot];
    ulStack.peek = function () {
        return this[ulStack.length - 1];
    };
    ulStack.level = function () {
        return this.peek().level;
    };
    for (var i = 0; i < aTitle.length; i++) {
        var titleLevel = parseInt(aTitle[i].tagName.substring(1, 2));
        while (titleLevel != ulStack.level()) {
            if (titleLevel > ulStack.level()) {
                var oLi = document.createElement('li');
                oLi.style.listStyle = 'none';
                var oChildUL = document.createElement('ul');
                oChildUL.level = ulStack.level() + 1;
                oLi.appendChild(oChildUL);
                if (ulStack.peek().childElementCount == 0) {
                    ulStack.peek().appendChild(document.createElement('li'));
                }
                ulStack.peek().appendChild(oLi);
                ulStack.push(oChildUL);
                while (!checkArray[oChildUL.level - 1] && titleLevel != ulStack.level()) {
                    oChildUL.level++;
                }
            } else {
                ulStack.pop();
            }
        }
        var oLi = document.createElement('li');
        var oLink = document.createElement('a');
        oLink.setAttribute('href', '#' + aTitle[i].getAttribute('id'));
        oLink.textContent = aTitle[i].textContent;
        oLi.appendChild(oLink);
        ulStack.peek().appendChild(oLi);
        aContents.push(oLink);
    }
    return oRoot;
}

以上代码和之前的代码思路都是一样的,只是代码的组织方式略有不同。另外我们为每个标题加入了链接,添加了锚点,这样可以点击某个标题直接定位到相应的内容。

4. “固定”目录

接下来要做的是“固定”目录。如果你的博客有足够的空间,可以直接将生成的目录使用position: fixed固定在某个地方。但是如果没有合适的空间,你可以像我一样,将目录放在侧边栏内的底部,然后通过监听页面的滚动,当滚动到目录栏目时,将其“固定”住,不要再滚动了,代码大概是这样:



function stickUp(){
    var stickUpOffset = oContents.offsetTop;// oContents即你的目录的最外层容器dom对象
    document.onscroll = function(){
        if(window.scrollY >= stickUpOffset){
            fix();
        }else{
            static();
        }
    };
    
    var fixed;
    function fix(){
    	if(fixed) return;
    	oSideBar.style.top = (oSideBar.offsetTop - oContents.offsetTop) + 'px'; // oSideBar为侧边栏dom对象
    	oSideBar.style.position = 'fixed';
    	fixed = true;
	}

	function static(){
    	oSideBar.style.position = 'static';
    	fixed = false;
	}  
}

5. 指示当前位置

为了能在目录中指示文章滚动到了哪儿,我们同样需要监听文章的滚动,然后将离当前滚动位置最近的之前的标题在目录中高亮显示。

先写一个高亮的css class,然后将该class加在合适的位置即可:

.active{
    color: yellow;
}

可以这么加class:

function stickUp(){
    var stickUpOffset = oContents.offsetTop;// oContents即你的目录的最外层容器DOM对象
    document.onscroll = function(){
        if(window.scrollY >= stickUpOffset){
            fix();
        }else{
            static();
        }
    };
    
    // 添加class
    var aLastActive = aContents[0]; // aContents是目录中所有标题所在的a标签DOM对象组成的数组
    for (var i = 0; i < aTitle.length; i++) {// aTitle 是文章中所有h标签DOM对象组成的数组
          if (window.scrollY < aTitle[i].offsetTop) { 
              break;
          }
    }
    aLastActive.removeAttribute('class');
    aContents[i && i - 1].setAttribute('class', 'active');
    aLastActive = aContents[i && i - 1];
}

6. 写在最后

以上便是整个生成目录的步骤,只贴了大概的代码,因为每个人的博客模板可能不同,剩余的就需要根据自己的模板去写了。

posted @ 2017-03-02 17:51  学数学的程序猿  阅读(800)  评论(4编辑  收藏  举报