vue-sticky組件詳解

sticky簡介

  • sticky的本意是粘的,粘性的,使用其進行的佈局被稱爲粘性佈局。
  • sticky是position屬性新推出的值,屬於CSS3的新特性,經常使用與實現吸附效果。
  • 設置了sticky佈局的元素,在視圖窗口時,與靜態佈局的表現一致。
  • 但當該元素的位置移出設置的視圖範圍時,其定位效果將變成fixed,並根據設置的left、top等做爲其定位參數。
  • 具體效果以下,當頁面滾動至下方,本來靜態佈局的「演職員表」將變爲fixed佈局,固定在頁面頂部。

    sticky效果圖

sticky兼容性

下圖可見,除了IE之外,目前絕大部分瀏覽器都是支持sticky佈局。css

sticky兼容性

需求背景

  • 可是實際狀況並不如上圖展現的那麼美好,在360安全瀏覽器上,並不支持sticky佈局,即便使用極速模式(使用chrome內核運行)也不支持。
  • 另外,筆者在網上找過相關的vue-sticky組件。可是使用起來並非那麼順手,並且看其源碼也是一頭霧水,用着不踏實。
  • 因此本身寫了一個,但願經過本文能將組件分享出去,也但願將本組件的原理講清楚。讓其餘同窗在使用的時候能更踏實一些。遇到坑也知道該怎麼去填。但願能幫到你們。

    但願能幫到你們

面向人羣

  • 急於使用vue-sticky組件的同窗。直接下載文件,拷貝代碼便可運行。
  • 喜歡看源碼,但願瞭解組件背後原理的同窗。
    其實本sticky組件原理很簡單,看完本文,相信你必定能把背後原理看懂。
    剛接觸前端的同窗也能夠經過本文章養成看源碼的習慣。打破對源碼的恐懼,相信本身,其實看源碼並無想象中的那麼困難

    原來如此

組件完整源碼以下

<!--sticky組件-->
<template>
  <!--盒子容器-->
  <section ref="$box" class="c-sticky-box" :style="boxStyle">
    <!--內容容器-->
    <div ref="$content" class="content" :style="contentStyle">
      <slot></slot>
    </div>
  </section>
</template>

<script>

export default {
  props: {
    top: {
      type: [String],
      default: 'unset',
    },
    left: {
      type: [String],
      default: 'unset',
    },
  },

  data() {
    return {
      boxStyle: {
        position: 'static',
        top: 0,
        left: 0,
        width: 'auto', // 佔位,爲了造成數據綁定
        height: 'auto',
      },
      contentStyle: {
        position: 'static',
        top: 0,
        left: 0,
        width: 'auto',
        height: 'auto',
      },
      isFixedX: false, // 是否已經設置爲fixed佈局,用於優化性能,防止屢次設置
      isFixedY: false, // 是否已經設置爲fixed佈局,用於優化性能,防止屢次設置
      isSupport: this.cssSupport('position', 'sticky'),
      // isSupport: false,
    }
  },

  mounted() {
    if (!this.isSupport) { // 不支持sticky
      this.getContentSize() // 獲取內容寬高
      this.scrollHandler() // 主動觸發一次位置設置操做
      window.addEventListener('resize', this.onResize)
      window.addEventListener('scroll', this.scrollHandler, true)
    } else {
      this.boxStyle = {
        position: 'sticky',
        top: this.top,
        left: this.left,
      }
    }
  },

  beforeDestroy() {
    if (!this.isSupport) {
      window.removeEventListener('resize', this.onResize)
      window.removeEventListener('scroll', this.scrollHandler, true)
    }
  },

  methods: {
    // 判斷是否支持某樣式的函數
    cssSupport(attr, value) {
      let element = document.createElement('div')
      if (attr in element.style) {
        element.style[attr] = value
        return element.style[attr] === value
      } else {
        return false
      }
    },

    // 獲取dom數據
    getContentSize() {
      // 獲取內容容器寬高信息
      const style = window.getComputedStyle(this.$refs.$content)

      // 設置盒子容器的寬高,爲了後續佔位
      this.boxStyle.width = style.width
      this.boxStyle.height = style.height
    },

    // 頁面縮放重置大小時,從新計算其位置
    onResize() {
      const { $box } = this.$refs
      const { contentStyle } = this
      const boxTop = $box.getBoundingClientRect().top
      const boxLeft = $box.getBoundingClientRect().left

      if (contentStyle.position === 'fixed') {
        contentStyle.top = this.top === 'unset' ? `${boxTop}px` : this.top
        contentStyle.left = this.left === 'unset' ? `${boxLeft}px` : this.left
      }
    },

    scrollHandler() {
      const { $content, $box } = this.$refs
      const { contentStyle } = this
      const boxTop = $box.getBoundingClientRect().top
      const boxLeft = $box.getBoundingClientRect().left
      const contentTop = $content.getBoundingClientRect().top
      const contentLeft = $content.getBoundingClientRect().left

      if (this.top !== 'unset') {
        if (boxTop > parseInt(this.top) && this.isFixedY) {
          this.isFixedY = false
          contentStyle.position = 'static'
        } else if (boxTop < parseInt(this.top) && !this.isFixedY) {
          this.isFixedY = true
          contentStyle.position = 'fixed'
          this.onResize()
        }

        // 當位置距左位置不對時,從新設置fixed對象left的值,防止左右滾動位置不對問題
        if (contentLeft !== boxLeft && this.left === 'unset') {
          this.onResize()
        }
      }

      if (this.left !== 'unset') {
        if (boxLeft > parseInt(this.left) && this.isFixedX) {
          this.isFixedX = false
          contentStyle.position = 'static'
        } else if (boxLeft < parseInt(this.left) && !this.isFixedX) {
          this.isFixedX = true
          contentStyle.position = 'fixed'
          this.onResize()
        }

        // 當位置距左位置不對時,從新設置fixed對象left的值,防止左右滾動位置不對問題
        if (contentTop !== boxTop && this.top === 'unset') {
          this.onResize()
        }
      }
    },
  },

}
</script>

技術難點

sticky效果須要解決這麼幾個問題前端

  • 佔位問題,sticky實現原理,無非是在特定超出視圖時,將內容的佈局設爲fixed。但將內容設置爲fixed佈局時,內容將脫離文檔流,本來佔據的空間將被釋放掉,這將致使頁面空了一塊後其餘內容發生位移。
  • 頁面resize後位置問題。當使用fixed定位時,其定位將根據頁面進行。若頁面大小發現變化,原顯示的位置可能與頁面變化後的不一致。這時須要從新設置。
  • 橫向滾動條問題。本質上和resize是同一個問題,須要監聽scroll事件,當頁面發送無相關方向的位移時,須要從新計算其位置,例如前面的sticky效果示例中設置了「演職員表」的top值,當其fixed後,滾動X軸,須要從新設置fixed的left參數。讓元素始終位於頁面相同位置

    技術難點

實現思路

  • 組件有兩層容器vue

    • 一個是內容slot的容器$content
    • 一個是內容容器$content的sticky盒子容器$box
    • 即包圍關係爲$sticky-box($content(slot))
    <section ref="$box" class="c-sticky-box" :style="boxStyle">
      <div ref="$content" class="content" :style="contentStyle">
        <slot></slot>
      </div>
    </section>
  • 監聽vue的mounted事件git

    • 這時內容slot已經被渲染出來
    • 獲取slot容器$content的寬高,設置到$box容器上
    • 設置$box容器寬高是爲了當後續$content容器Fixed後,$box容器仍在頁面中佔據空間。
    const style = window.getComputedStyle(this.$refs.$content)
    this.boxStyle.width = style.width
    this.boxStyle.height = style.height
  • 監聽scroll事件github

    • 在事件中獲取容器$content在頁面中的位置,並將其與預設值進行大小比較,判斷$content是否應該fixed
    • 怎麼便捷地獲取$content在頁面中的位置呢?直接使用Element.getBoundingClientRect()函數,該函數將返回{left,top}分別表示dom元素距離窗口的距離。詳細可參看MDN文檔
    const { $content, $box } = this.$refs
    const { contentStyle } = this
    const boxTop = $box.getBoundingClientRect().top
    const boxLeft = $box.getBoundingClientRect().left
    const contentTop = $content.getBoundingClientRect().top
    const contentLeft = $content.getBoundingClientRect().left
    • 比較boxTop與預設值top的大小,當boxTop比預設值值要小時,即內容即將移出規定的視圖範圍。這時將內容容器$content設置爲fixed。並設置其top值(即預設的top值,吸頂距離),left值與盒子位置相同,故設置爲盒子距離的left值
    • 當boxTop比預設值值要大時,即內容從新返回的視圖範圍。則將內容容器$content從新設置會靜態佈局,讓其從新回到盒子佈局內部。因爲靜態佈局不受left和top的影響,因此不須要設置left和top
    if (boxTop > parseInt(this.top) && this.isFixedY) {
      contentStyle.position = 'static'
    } else if (boxTop < parseInt(this.topI) && !this.isFixedY) {
      contentStyle.position = 'fixed'
      contentStyle.top = this.top
      contentStyle.left = `${boxLeft}px`
    }
    • 在scroll事件中,除了Y軸方向上的滾動,還可能發生X軸方向的滾動。這些須要從新設置fixed元素的left值,讓其與盒子容器的left值一致
    // 當位置距左位置不對時,從新設置fixed對象left的值,防止左右滾動位置不對問題
    if (contentLeft !== boxLeft && this.left === 'unset') {
      const { $box } = this.$refs
      const { contentStyle } = this
      const boxTop = $box.getBoundingClientRect().top
      const boxLeft = $box.getBoundingClientRect().left
      if (contentStyle.position === 'fixed') {
        contentStyle.top = this.top
        contentStyle.left = `${boxLeft}px`
      }
    }
  • 最後,是監聽頁面的resize事件,防止頁面大小變化時,fixed相對頁面的變化。一樣的,從新設置left值chrome

    // 當位置距左位置不對時,從新設置fixed對象left的值,防止左右滾動位置不對問題
    const { $box } = this.$refs
    const { contentStyle } = this
    const boxTop = $box.getBoundingClientRect().top
    const boxLeft = $box.getBoundingClientRect().left
    
    if (contentStyle.position === 'fixed') {
      contentStyle.top = this.top === 'unset' ? `${boxTop}px` : this.top
      contentStyle.left = this.left === 'unset' ? `${boxLeft}px` : this.left
    }

須要注意的地方

  • 目前僅支持top與left值的單獨使用,暫不支持同時設置
  • 目前僅支持px單位,暫不支持rem及百分比單位
  • 設置內容樣式時須要注意,設置定位相關屬性須要設置在box容器上,例如設置'displCy: inline-block;','verticCl-Clign: top;','margin'瀏覽器

    • 設置外觀樣式,如背景,邊框等,則設置在slot內容中
    • 即內容content-box之外的設置在box容器中,content-box之內的樣式,則設置在slot內容中
  • 盒子容器不須要設置position屬性,即便有也會被沖刷掉。由於程序將內部從新設置position的值
  • 一樣的,在樣式中設置盒子容器的left和top值也是無效的,會被程序內部從新設置。只能經過dom屬性值傳遞到組件中進行設置

    技術難點

後續優化

目前本組件僅實現了基本功能,後續還將繼續優化如下功能安全

  • slot內容中,若是有圖片,若是獲取設置寬高,(監聽全部圖片的load事件,從新設置容器的高寬)dom

    • 目前僅在mounted中獲取slot的寬高,這僅僅是dom元素被渲染,可是dom內容是否加載完畢並不知道的,如img標籤,後續在slot中,監聽全部img標籤的load事件,load中,從新設置組件容器的大小
  • slot內容有變化時,設置容器函數

    • 一樣的,當slot內容變化後,從新設置$content的寬高
    • 具體如何實現,暫時尚未頭緒
  • 移動端適配

    • 目前只測試了在PC中的效果,暫未在移動端作測試。不排除移動端使用存在坑
  • 單位適配

    • 目前只支持PX單位,未支持rem,百分百等單位
  • left和top值的混合使用,目前只支持單個屬性的使用,暫不支持同時設置

項目源碼及示例

第一稿寫完了,撒花花

撒花花

相關文章
相關標籤/搜索