仿掘金项目之文章长目录滚动高亮优化 | 青训营笔记

2,200 阅读6分钟

这是我参与「第四届青训营 」笔记创作活动的第6天

仿掘金项目之实现文章预览|青训营笔记 - 掘金 (juejin.cn)

仿掘金项目之文章目录实现|青训营笔记 - 掘金 (juejin.cn)

仿掘金项目之文章目录滚动高亮实现 | 青训营笔记 - 掘金 (juejin.cn)

书接上文

前面我已经实现了基本的目录滚动高亮效果,但是还有个大bug没有解决,这个bug就是当目录内容过多,部分目录要通过滚动条的形式才能显示.也就是说有一部分目录会被隐藏起来.

那么当文章滚动到对应的目录隐藏部分时,目录会跟着文章滚动让当前高亮内容显示在可视区域吗?

答案是不会的,所以滚着滚着,高亮元素就看不到了.

所以我们必须自己实现这个功能

获取目录元素

<ul ref="nav" class="nav">
      <li v-for="(i, index) in list" :key="index" :title="i.content" @click="jump(index)" :ref="setItemRef"
        :class="activeIndex === index ? 'active' : ''">
        <div :style="{ marginLeft: size(i.id) }">
          {{ i.content }}
        </div>
      </li>
    </ul>

这里通过ref='nav'获取目录元素,通过:ref="setItemRef"这个可以获取到所以li元素,也就是每一条目录所在元素

const nav = ref(null);
//获取nav中的li元素
let itemRefs = [];
const setItemRef = (el) => {
  if (el) {
    itemRefs.push(el);
  }
};
onBeforeUpdate(() => {
  itemRefs = [];
});

image.png

判断当前高亮目录li元素是否超过目录元素总高度的一半

这里是因为观察掘金的长目录,当高亮元素滚动到总高度一半时,才让nav元素滚动起来.

    let mid = nav.value.clientHeight / 2;  //滚动元素父元素的高度的一半
    let offsetTop = itemRefs[activeIndex.value].offsetTop; //当前激活元素相对于父元素顶部的距离
     if (offsetTop > mid ) {
        nav.value.scrollBy(0, 32);
      }

scrollBy(x,y)接收两个参数,横坐标与纵坐标,是相对于当前位置滚动指定距离

注意这里是让nav元素滚动,不是widow或者document,而且注意nav得有滚动条才有效

这里的32是我一个li元素的高度

判断当前高亮元素与上一个高亮元素的差值

这一点是因为,如果我们滚动的飞快,那么目录滚动的速度会跟不上,比如我们一次滚动跨越了三四个h标签,但是目录滚动只移动了一位,这样积累之下,当前的高亮元素就会消失在可视区域内.

  if (oldValue === activeIndex.value) {
    return;
  }
  let difference = activeIndex.value - oldValue //差值
  if (offsetTop > mid ) {
    nav.value.scrollBy(0, 32 * difference);
  }
  oldValue = activeIndex.value;

在全局中定义遍历oldValue = 0

如果当前activeIndex等于oldValue,就说明当前高亮元素没改变,不需要移动

通过difference可以知道当前activeIndex和上一个activeIndex相差多少,移动对应的距离,让高亮元素尽量在目录的中间位置.

写文章的时候现场发现一个bug,当文章滚动到最后,再向上滚动时,目录也会跟着移动,这样的话,高亮元素始终在最下面,这样不好看,所以我又改了改

  if (oldValue === activeIndex.value) {
    return;
  }
  let difference = activeIndex.value - oldValue //差值
   if (offsetTop > itemRefs[itemRefs.length - 1].offsetTop - mid && !isDown) {

  }
  else if (offsetTop > mid ) {
    nav.value.scrollBy(0, 31 * difference);
  }
  oldValue = activeIndex.value;

当文章在最下面的一部分,并且是向上滚动时,nav不跟着滚动,这个和在最上面没超过nav的一半不滚动是一个道理

这里的isDown表示的是文章是向上滚动还是向下滚动

let isDown = true;
//判断滚动方向
let scrollFunc = function (e) {
  e = e || window.event;
  if (e.wheelDelta) {
    //判断浏览器IE,谷歌滑轮事件
    if (e.wheelDelta > 0) {
      //当滑轮向上滚动时
      // console.log("滑轮向上滚动");
      isDown = false;
    }
    if (e.wheelDelta < 0) {
      //当滑轮向下滚动时
      // console.log("滑轮向下滚动");
      isDown = true;
    }
  } else if (e.detail) {
    //Firefox滑轮事件
    if (e.detail > 0) {
      //当滑轮向上滚动时
      isDown = false;
      // console.log("滑轮向上滚动");
    }
    if (e.detail < 0) {
      //当滑轮向下滚动时
      isDown = false;
      // console.log("滑轮向下滚动");
    }
  }
};
const mouseWheel = () => {
  if (document.addEventListener) {
    //火狐使用DOMMouseScroll绑定
    document.addEventListener("DOMMouseScroll", scrollFunc, false);
  }
  //其他浏览器直接绑定滚动事件
  document.addEventListener("mousewheel", scrollFunc);
};

到这里长目录跟随滚动的功能基本实现了.

test.gif

解决点击跳转带来的bug

如果我们不点击跳转,单纯的跟随滚动高亮已经没问题了,但是如果点击跳转会如何?

看过前面内容的应该知道,点击之后高亮点击的目录内容,同时将当前的index值赋值给activeIndex.

前面我们写了oldValueactiveIndex做比较,如果不同就会滚动目录元素.

想象一下,如果我们滚动目录之后点击跳转,此时我们肯定可以看到自己点击的内容高亮了,但是此时activeIndex已经改变,而且因为我们滚动了nav目录,那么这两个值的差就会很大,造成的影响就是高亮内容直接被滚动到非可视区域了,我们看不见了.

这肯定不是我们想要的效果.

我用了一个比较简单的方法解决这个问题

//点击目录跳转
let isJump = false
const jump = (index) => {
  activeIndex.value = index;
  let target = document.getElementById(index).offsetTop;
  if (target) {
    window.scrollTo({
      top: target - 80,
    });
    isJump = true
  }
};

这是之前写的点击跳转的代码

我在这里加了个flag:isJamp,当我点击之后就设置成true

const watchActive = () => {
  if (oldValue === activeIndex.value) {
    return;
  }
  let difference = activeIndex.value - oldValue //差值
  let mid = nav.value.clientHeight / 2;  //滚动元素父元素的高度的一半
  let offsetTop = itemRefs[activeIndex.value].offsetTop; //当前激活元素相对于父元素顶部的距离
  oldValue = activeIndex.value;
  if (isJump) {
    isJump = false
    return
  }
  if (offsetTop > itemRefs[itemRefs.length - 1].offsetTop - mid && !isDown) {

  }
  else if (offsetTop > mid ) {
    nav.value.scrollBy(0, 31 * difference);
  }

  if (activeIndex.value === 0) {//后面解释这个
    nav.value.scrollTo({
      top: 0
    })
  }
};

这里是前面写的代码的整体内容,在scroll事件的回调函数中调用就行了.

这里我通过isJump的值来判断是不是点击跳转导致的activeIndex变化,如果是,后面滚动nav的代码就不会执行.这样就解决了bug.

但是不够完美,如果我点击的是偏下的目录,那么高亮的元素就一直在偏下的位置,不会在滚动中移动到中间位置

这里有个想法,还是根据高亮元素的位置改变移动距离的大小,让其始终在中间位置. 偷懒不写了.感觉有点麻烦

解决点击回到顶部带来的bug

前面提到点击回到顶部的功能也是我写的,所以我在测试时发现,如果我点击回到顶部之后,nav目录可视区域还是在点击回到顶部之前所对应的区域,也就是我看不到当前的高亮元素. image.png 这个bug是怎么造成的呢.

 if (offsetTop > mid ) {
    nav.value.scrollBy(0, 31 * difference);
  }

只有滚动元素所在位置大于nav总高度的一半才能执行滚动的代码.

点击回到顶部时,activeIndex = 0,对应的是第一个li标签,这肯定小于nav的高度的一半,所以这个滚动就不会执行

解决办法就是我前面写的

 if (activeIndex.value === 0) {
    nav.value.scrollTo({
      top: 0
    })
  }

回到顶部之后,activeIndex = 0,这个时候执行一下scrollTo,将nav滚动到顶就行了.

最后看看效果吧,特意找了个做动图的工具展示效果

test.gif