超詳細(xì)Vue實現(xiàn)導(dǎo)航欄綁定內(nèi)容錨點+滾動動畫+vue-router(hash模式可用)

最終效果

首先是一些說明

  1. 動畫效果是原生 js 實現(xiàn),不可能為了這么一個動畫引用 Jquery 這么大一個庫
  2. 因為是在 vue-router 的 hash 模式下,所以就不能用herf="#contentx"這樣的方式來跳轉(zhuǎn)了,這里用 js 來滾動到指定元素位置

文章整體思路是先布局,然后把內(nèi)容和導(dǎo)航聯(lián)動起來,最后來實現(xiàn)點擊導(dǎo)航滾動到指定內(nèi)容

布局

話不多說,上代碼

navs.vue

<template>
  <div>
    <!-- 內(nèi)容區(qū)域 -->
    <div class="content">
      <div>
        content-0
      </div>
      <div>
        content-1
      </div>
      <div>
        content-2
      </div>
      <div>
        content-3
      </div>
      <div>
        content-4
      </div>
    </div>
    <!-- 導(dǎo)航區(qū)域 -->
    <ul class="navs">
      <li :class="{active: active===0}">
        content-0
      </li>
      <li :class="{active: active===1}">
        content-1
      </li>
      <li :class="{active: active===2}">
        content-2
      </li>
      <li :class="{active: active===3}">
        content-3
      </li>
      <li :class="{active: active===4}">
        content-4
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  props: {},
  data() {
    return {
      active: 0 // 當(dāng)前激活的導(dǎo)航索引
    }
  },
  methods: {}
}
</script>

<style scoped>
  /* 內(nèi)容區(qū)的樣式 */
  .content {
    background-color: white;
    width: 500px;
  }
  .content div {
    width: 100%;
    height: 600px;
    font-size: 36px;
    padding: 20px;
    background-color: #7ec384;
  }
  .content div:nth-child(2n) {
    background-color: #847ec3;
  }
  /* 導(dǎo)航欄的樣式 */
  .navs {
    position: fixed;
    top: 80px;
    left: 700px;
    background-color: #efefef;
  }
  .navs li {
    padding: 0 20px;
    line-height: 1.6;
    font-size: 24px;
  }
  /* 當(dāng)導(dǎo)航被點亮后改變顏色 */
  .navs .active{
    color: #847ec3;
    background-color: #e2e2e2;
  }
</style>

布局很簡單,就左邊內(nèi)容,右邊導(dǎo)航。導(dǎo)航和內(nèi)容本來可以用 v-for 來生成的,但是為了看起來更簡單明了,我就沒那么寫。先來看看布局效果吧!

布局效果圖

內(nèi)容導(dǎo)航聯(lián)動

這一塊開始變得有意思了起來,首先我們要搞懂元素的兩個屬性:

  1. scrollTop
  2. offsetTop

scrollTop

先看看一句話定義吧

一個元素的 scrollTop 值是這個元素的頂部到視口可見內(nèi)容(的頂部)的距離的度量。當(dāng)一個元素的內(nèi)容沒有產(chǎn)生垂直方向的滾動條,那么它的 scrollTop 值為0

引用自 https://developer.mozilla.org/zh-CN/docs/Web/API/Element/scrollTop

如果還不理解,那么我畫張圖,大概就懂了,相信讀者也知道文檔流是個啥,我們先來個文檔流

元素文檔流

好了,現(xiàn)在這個文檔流有點長,我們的視口不夠高,于是只能顯示一部分,其余的要滾動查看,看起來像這樣

視口可以看見的內(nèi)容

這個時候我們看見的是最頂部的內(nèi)容,這個文檔流的 scrollTop 此時等于 0,讓我們把視口往下移一點,也就是滾動一下窗口,查看下面的內(nèi)容。

scrollTop值

OK,到這里我相信你一定已經(jīng)理解了 scrollTop

offsetTop

還是先一句話定義吧

HTMLElement.offsetTop 為只讀屬性,它返回當(dāng)前元素相對于其 offsetParent 元素的頂部內(nèi)邊距的距離。

引用自 https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement/offsetTop

關(guān)于 offsetParent 網(wǎng)上有的定義是

與當(dāng)前元素最近的經(jīng)過定位( position 不等于 static )的父級元素

具體情況分為以下幾種:

  1. position為fixed時,offsetParent為null,offsettop的值和top相等。此時元素是以視口來定位的。

  2. position非fixed,父級元素?zé)o定位(static)時,offsetParent為body。

  3. position非fixed,父級元素有定位時,offsetParent為最近的有定位的父級元素。

  4. body元素,offsetParent為null,offsettop為0(似乎是廢話)。

引用自 http://m.itdecent.cn/p/135731ec13f1

我們這里是屬于第二種情況。

可能到這里你有點蒙,那還是拿剛才那張圖來舉例

image

好了,關(guān)于 offsetTop 你也了解了,我們就可以開始寫代碼了。

監(jiān)聽滾動

當(dāng)元素發(fā)生滾動時,會觸發(fā) scroll事件,我們就在 vue 的 mounted 鉤子中添加監(jiān)聽好了,修改 vans.vue 文件

<script>
export default {
  props: {},
  data() {
    return {
      active: 0 // 當(dāng)前激活的導(dǎo)航索引
    }
  },
  mounted() {
    // 監(jiān)聽滾動事件
    window.addEventListener('scroll', this.onScroll)
  },
  destroy() {
    // 必須移除監(jiān)聽器,不然當(dāng)該vue組件被銷毀了,監(jiān)聽器還在就會出錯
    window.removeEventListener('scroll', this.onScroll)
  },
  methods: {
    onScroll() {
      // 滾動監(jiān)聽器
    }
  }
}
</script>

現(xiàn)在我們開始寫監(jiān)聽回調(diào)

// 滾動監(jiān)聽器
onScroll() {
  // 獲取所有錨點元素
  const navContents = document.querySelectorAll('.content div')
  // 所有錨點元素的 offsetTop
  const offsetTopArr = []
  navContents.forEach(item => {
    offsetTopArr.push(item.offsetTop)
  })
  // 獲取當(dāng)前文檔流的 scrollTop
  const scrollTop = document.documentElement.scrollTop || document.body.scrollTop
  // 定義當(dāng)前點亮的導(dǎo)航下標(biāo)
  let navIndex = 0
  for (let n = 0; n < offsetTopArr.length; n++) {
    // 如果 scrollTop 大于等于第 n 個元素的 offsetTop 則說明 n-1 的內(nèi)容已經(jīng)完全不可見
    // 那么此時導(dǎo)航索引就應(yīng)該是 n 了
    if (scrollTop >= offsetTopArr[n]) {
      navIndex = n
    }
  }
  // 把下標(biāo)賦值給 vue 的 data
  this.active = navIndex
}

到了這里,我們的導(dǎo)航已經(jīng)可以跟著滾動來變化了,看效果圖

監(jiān)聽滾動效果圖

用JS來滾動到指定元素

scrollTop 是可讀可寫的,我們只需要把目標(biāo)元素的 offsetTop 賦值給 scrollTop 就可以讓視口自動跳過去,但是直接跳過去是很生硬的,所以我們要一點一點的跳過去,只要速度夠快,人眼看上去就是動畫效果。好了,開始碼代碼。

給導(dǎo)航添加點擊點擊事件

<!-- 導(dǎo)航區(qū)域 -->
<ul class="navs">
  <li :class="{active: active===0}" @click="scrollTo(0)">
    content-0
  </li>
  <li :class="{active: active===1}" @click="scrollTo(1)">
    content-1
  </li>
  <li :class="{active: active===2}" @click="scrollTo(2)">
    content-2
  </li>
  <li :class="{active: active===3}" @click="scrollTo(3)">
    content-3
  </li>
  <li :class="{active: active===4}" @click="scrollTo(4)">
    content-4
  </li>
</ul>

新增滾動函數(shù)

methods: {
    ...
    // 跳轉(zhuǎn)到指定索引的元素
    scrollTo(index) {
      // 獲取目標(biāo)的 offsetTop
      // css選擇器是從 1 開始計數(shù),我們是從 0 開始,所以要 +1
      const targetOffsetTop = document.querySelector(`.content div:nth-child(${index + 1})`).offsetTop
      // 獲取當(dāng)前 offsetTop
      let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
      // 定義一次跳 50 個像素,數(shù)字越大跳得越快,但是會有掉幀得感覺,步子邁大了會扯到蛋
      const STEP = 50
      // 判斷是往下滑還是往上滑
      if (scrollTop > targetOffsetTop) {
        // 往上滑
        smoothUp()
      } else {
        // 往下滑
        smoothDown()
      }
      // 定義往下滑函數(shù)
      function smoothDown() {
        // 如果當(dāng)前 scrollTop 小于 targetOffsetTop 說明視口還沒滑到指定位置
        if (scrollTop < targetOffsetTop) {
          // 如果和目標(biāo)相差距離大于等于 STEP 就跳 STEP
          // 否則直接跳到目標(biāo)點,目標(biāo)是為了防止跳過了。
          if (targetOffsetTop - scrollTop >= STEP) {
            scrollTop += STEP
          } else {
            scrollTop = targetOffsetTop
          }
          document.body.scrollTop = scrollTop
          document.documentElement.scrollTop = scrollTop
          // 屏幕在繪制下一幀時會回調(diào)傳給 requestAnimationFrame 的函數(shù)
          // 關(guān)于 requestAnimationFrame 可以自己查一下,在這種場景下,相比 setInterval 性價比更高
          requestAnimationFrame(smoothDown)
        }
      }
      // 定義往上滑函數(shù)
      function smoothUp() {
        if (scrollTop > targetOffsetTop) {
          if (scrollTop - targetOffsetTop >= STEP) {
            scrollTop -= STEP
          } else {
            scrollTop = targetOffsetTop
          }
          document.body.scrollTop = scrollTop
          document.documentElement.scrollTop = scrollTop
          requestAnimationFrame(smoothUp)
        }
      }
    }
    }
}

最終局

好了,我們看看最終效果

最終效果

最終代碼

<template>
  <div>
    <!-- 內(nèi)容區(qū)域 -->
    <div class="content">
      <div>
        content-0
      </div>
      <div>
        content-1
      </div>
      <div>
        content-2
      </div>
      <div>
        content-3
      </div>
      <div>
        content-4
      </div>
    </div>
    <!-- 導(dǎo)航區(qū)域 -->
    <ul class="navs">
      <li :class="{active: active===0}" @click="scrollTo(0)">
        content-0
      </li>
      <li :class="{active: active===1}" @click="scrollTo(1)">
        content-1
      </li>
      <li :class="{active: active===2}" @click="scrollTo(2)">
        content-2
      </li>
      <li :class="{active: active===3}" @click="scrollTo(3)">
        content-3
      </li>
      <li :class="{active: active===4}" @click="scrollTo(4)">
        content-4
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  props: {},
  data() {
    return {
      active: 0 // 當(dāng)前激活的導(dǎo)航索引
    }
  },
  mounted() {
    // 監(jiān)聽滾動事件
    window.addEventListener('scroll', this.onScroll, false)
  },
  destroy() {
    // 必須移除監(jiān)聽器,不然當(dāng)該vue組件被銷毀了,監(jiān)聽器還在就會出錯
    window.removeEventListener('scroll', this.onScroll)
  },
  methods: {
    // 滾動監(jiān)聽器
    onScroll() {
      // 獲取所有錨點元素
      const navContents = document.querySelectorAll('.content div')
      // 所有錨點元素的 offsetTop
      const offsetTopArr = []
      navContents.forEach(item => {
        offsetTopArr.push(item.offsetTop)
      })
      // 獲取當(dāng)前文檔流的 scrollTop
      const scrollTop = document.documentElement.scrollTop || document.body.scrollTop
      // 定義當(dāng)前點亮的導(dǎo)航下標(biāo)
      let navIndex = 0
      for (let n = 0; n < offsetTopArr.length; n++) {
        // 如果 scrollTop 大于等于第n個元素的 offsetTop 則說明 n-1 的內(nèi)容已經(jīng)完全不可見
        // 那么此時導(dǎo)航索引就應(yīng)該是n了
        if (scrollTop >= offsetTopArr[n]) {
          navIndex = n
        }
      }
      this.active = navIndex
    },
    // 跳轉(zhuǎn)到指定索引的元素
    scrollTo(index) {
      // 獲取目標(biāo)的 offsetTop
      // css選擇器是從 1 開始計數(shù),我們是從 0 開始,所以要 +1
      const targetOffsetTop = document.querySelector(`.content div:nth-child(${index + 1})`).offsetTop
      // 獲取當(dāng)前 offsetTop
      let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
      // 定義一次跳 50 個像素,數(shù)字越大跳得越快,但是會有掉幀得感覺,步子邁大了會扯到蛋
      const STEP = 50
      // 判斷是往下滑還是往上滑
      if (scrollTop > targetOffsetTop) {
        // 往上滑
        smoothUp()
      } else {
        // 往下滑
        smoothDown()
      }
      // 定義往下滑函數(shù)
      function smoothDown() {
        // 如果當(dāng)前 scrollTop 小于 targetOffsetTop 說明視口還沒滑到指定位置
        if (scrollTop < targetOffsetTop) {
          // 如果和目標(biāo)相差距離大于等于 STEP 就跳 STEP
          // 否則直接跳到目標(biāo)點,目標(biāo)是為了防止跳過了。
          if (targetOffsetTop - scrollTop >= STEP) {
            scrollTop += STEP
          } else {
            scrollTop = targetOffsetTop
          }
          document.body.scrollTop = scrollTop
          document.documentElement.scrollTop = scrollTop
          // 關(guān)于 requestAnimationFrame 可以自己查一下,在這種場景下,相比 setInterval 性價比更高
          requestAnimationFrame(smoothDown)
        }
      }
      // 定義往上滑函數(shù)
      function smoothUp() {
        if (scrollTop > targetOffsetTop) {
          if (scrollTop - targetOffsetTop >= STEP) {
            scrollTop -= STEP
          } else {
            scrollTop = targetOffsetTop
          }
          document.body.scrollTop = scrollTop
          document.documentElement.scrollTop = scrollTop
          requestAnimationFrame(smoothUp)
        }
      }
    }
  }
}
</script>

<style scoped>
  /* 內(nèi)容區(qū)的樣式 */
  .content {
    background-color: white;
    width: 500px;
  }
  .content div {
    width: 100%;
    height: 600px;
    font-size: 36px;
    padding: 20px;
    background-color: #7ec384;
  }
  .content div:nth-child(2n) {
    background-color: #847ec3;
  }
  /* 導(dǎo)航欄的樣式 */
  .navs {
    position: fixed;
    top: 80px;
    left: 700px;
    background-color: #efefef;
  }
  .navs li {
    padding: 0 20px;
    line-height: 1.6;
    font-size: 24px;
  }
  /* 當(dāng)導(dǎo)航被點亮后改變顏色 */
  .navs .active{
    color: #847ec3;
    background-color: #e2e2e2;
  }
</style>

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容