• <fieldset id="8imwq"><menu id="8imwq"></menu></fieldset>
  • <bdo id="8imwq"><input id="8imwq"></input></bdo>
    最新文章專題視頻專題問答1問答10問答100問答1000問答2000關鍵字專題1關鍵字專題50關鍵字專題500關鍵字專題1500TAG最新視頻文章推薦1 推薦3 推薦5 推薦7 推薦9 推薦11 推薦13 推薦15 推薦17 推薦19 推薦21 推薦23 推薦25 推薦27 推薦29 推薦31 推薦33 推薦35 推薦37視頻文章20視頻文章30視頻文章40視頻文章50視頻文章60 視頻文章70視頻文章80視頻文章90視頻文章100視頻文章120視頻文章140 視頻2關鍵字專題關鍵字專題tag2tag3文章專題文章專題2文章索引1文章索引2文章索引3文章索引4文章索引5123456789101112131415文章專題3
    問答文章1 問答文章501 問答文章1001 問答文章1501 問答文章2001 問答文章2501 問答文章3001 問答文章3501 問答文章4001 問答文章4501 問答文章5001 問答文章5501 問答文章6001 問答文章6501 問答文章7001 問答文章7501 問答文章8001 問答文章8501 問答文章9001 問答文章9501
    當前位置: 首頁 - 科技 - 知識百科 - 正文

    實現一個 Vue 吸頂錨點組件方法

    來源:懂視網 責編:小采 時間:2020-11-27 21:53:44
    文檔

    實現一個 Vue 吸頂錨點組件方法

    實現一個 Vue 吸頂錨點組件方法:前言 近期產品小哥哥給我提了一個新需求,在一個頁面的滾動區中添加一組錨點定位按鈕,點擊按鈕將對應的元素顯示在頁面的可視區中。當按鈕組超出頁面可視區的時候將其固定在滾動區域的頭部,當滾動區滾動時,高亮距離滾動區頂部最近的元素所匹配的錨點按鈕。
    推薦度:
    導讀實現一個 Vue 吸頂錨點組件方法:前言 近期產品小哥哥給我提了一個新需求,在一個頁面的滾動區中添加一組錨點定位按鈕,點擊按鈕將對應的元素顯示在頁面的可視區中。當按鈕組超出頁面可視區的時候將其固定在滾動區域的頭部,當滾動區滾動時,高亮距離滾動區頂部最近的元素所匹配的錨點按鈕。

    前言

    近期產品小哥哥給我提了一個新需求,在一個頁面的滾動區中添加一組錨點定位按鈕,點擊按鈕將對應的元素顯示在頁面的可視區中。當按鈕組超出頁面可視區的時候將其固定在滾動區域的頭部,當滾動區滾動時,高亮距離滾動區頂部最近的元素所匹配的錨點按鈕。

    拆分功能點

    現在我們已經明確需求了,接下來我們總結一下這個需求有哪些功能點:

  • 按鈕組要有吸頂效果
  • 點擊按鈕要有錨點定位功能
  • 滾動內容區需要找到對應的按鈕并高亮
  • 吸頂組件

    要做一個吸頂效果最簡單的方式是將 css 的 position 屬性設置為 sticky, 這樣就實現粘性布局。

    .sticky-container {
     position: sticky;
     top: 0px;
    }

    上面的示例僅僅用了兩行 css 的代碼就實現了粘性布局,但由于 IE 瀏覽器完全不支持粘性布局,而我的項目又需要支持一部分的 IE 瀏覽器,所以就需要手動去實現這樣一個功能。

    MDN 官方對粘性布局的解釋是這樣的,粘性布局元素默認是相對定位的,當粘性元素超出父元素的指定值(如 `top` 、`left` 等),例如上面的示例,當元素粘性元素改為固定定位。關于父級元素 MDN 描述的不是很精確,這里的父級元素指的是父級滾動元素,如果沒有父級滾動元素則將 `body` 元素作為父級元素。

    既然需要自己實現一個吸頂的效果,思考到其他頁面可能也會使用的吸頂的功能,所以決定將其單獨抽離成一個通用組件。首先我們知道粘性布局是對父級滾動元素定位,所以我們要先找到父級滾動元素,這個功能我們可以通過兩種方式實現,一種是向上查找,一種是通過 props 傳遞一個唯一標識的 css 選擇器。

    我覺得其他項目可能也會遇到這個功能,所以我定義組件 盡量向著開源靠攏,所以我這里同時支持兩種方案。首先我們要實現一個查找父級滾動元素的功能,如何判斷一個元素是滾動元素呢?很簡單判斷其 `overflow` 是否是 `auto` 或者 `scroll`。

    // util.js 文件
    // 判斷一個元素是否是滾動元素
    const scrollList = ['auto', 'scroll']
    
    export function hasScrollElement(el, direction = 'vertical') {
     if (!el) return
     const style = window.getComputedStyle(el)
     if (direction === 'vertical') {
     return scrollList.includes(style.overflowY)
     } else if (direction === 'horizontal') {
     return scrollList.includes(style.overflowX)
     }
    }
    
    // 獲取第一個滾動元素
    export function getFirstScrollElement(el, direction = 'vertical') {
     if (!el) return
     if (hasScrollElement(el, direction)) {
     return el
     } else {
     return getFirstScrollElement(el && el.parentElement, direction)
     }
    }

    這里說下實現吸頂效果所需要的一些基礎知識:

  • fixed 定位是相對于瀏覽器的可視區進行定位,這意味著即使頁面滾動,它還是會固定在相同的位置
  • offsetTop 是一個只讀的屬性,它返回當前元素相對于距離它最近的父級定位元素頂部的距離。
  • scrollTop 屬性可以獲取或設置一個元素的內容垂直滾動的像素值,`scrollTop` 表示這個元素達到父級滾動元素頂部的距離。
  • <template>
     <div class="cpt-sticky" :class="fixedClass" :style="{ top: top + 'px', zIndex }">
     <slot></slot>
     </div>
    </template>
    
    <script>
    export default {
     props: {
     top: Number,
     parent: String,
     zIndex: Number
     },
    
     data() {
     return {
     fixedClass: '',
     scrollElement: null
     }
     },
    
     mounted() {
     this.initScrollElement()
     },
    
     destroyed() {
     this.removeScrollEvent()
     },
    
     methods: {
     handleScroll() {
     const scrollOffsetTop = this.$el.offsetTop - this.top
     if (this.scrollElement.scrollTop >= scrollOffsetTop) {
     this.fixedClass = 'top-fixed'
     } else {
     this.fixedClass = ''
     }
     },
    
     initScrollElement() {
     const element = document.querySelector(this.parent)
     if (element) {
     this.removeScrollEvent()
     this.scrollElement = element
     this.scrollElement.addEventListener('scroll', this.handleScroll)
     }
     },
    
     removeScrollEvent() {
     if (this.scrollElement) {
     this.scrollElement.removeEventListener('scroll', this.handleScroll)
     }
     }
     }
    }
    </script>
    
    <style lang="scss">
    .cpt-sticky {
     .top-fixed {
     position: fixed;
     width: 100%;
     background: #fff;
     }
    }
    </style>

    就像上面的示例代碼一樣,短短幾十行就實現了一個吸頂組件,不過它實現了吸頂的功能,但是還有一些缺陷。

    1. 在慢速滾動頁面,吸頂組件在固定與非固定的時候有明顯的卡頓現象。
    2. 由于我的需求有一些是需要做錨點定位功能,但是直接用錨點定位會改變路由所以改為了滾動定位(后面會細說)。但是由于吸頂組件在 `fixed` 之后會脫離文檔流,導致定位的元素會有一部分(吸頂組件高度 )被卡在吸頂組件下方。就像下面這張圖的效果,右邊的錨點定位2區域的標題被隱藏了。

    這些問題也很好解決,使用一個和吸頂組件相同大小的占位元素,當吸頂組件脫離文檔流之后,占位元素插入吸頂組件原來的 DOM 位置中,然后順便帶上一些小優化。由于占位元素需要和組件高度一致,所以必須要保證 `slot` 插槽中的 DOM 元素已經被加載完成,另外放在 slot 元素中可能發生變更,所以我在吸頂狀態變更之前獲取其高度。

    <template>
     <div class="cpt-sticky">
     <div class="sticky-container" :class="fixedClass" :style="{ top: top + 'px', zIndex }">
     <slot></slot>
     </div>
     <div v-if="showPlaceholder" class="sticky-placeholder" :style="{ height: offsetHeight + 'px' }"></div>
     </div>
    </template>
    
    <script>
    import { getFirstScrollElement } from 'util.js'
    
    export default {
     props: {
     top: {
     type: Number,
     default: 0
     },
     zIndex: {
     type: Number,
     default: 0
     },
     parent: {
     type: String,
     default: ''
     }
     },
    
     data() {
     return {
     isMounted: false,
     fixedClass: '',
     offsetHeight: 0,
     scrollElement: null,
     showPlaceholder: false
     }
     },
    
     mounted() {
     this.isMounted = true
     this.initScrollElement()
     },
    
     watch: {
     parent: {
     immediate: true,
     handler: 'getScrollElement'
     },
    
     fixedClass(v) {
     if (v && !this.offsetHeight) {
     this.offsetHeight = this.$el.offsetHeight
     }
     this.showPlaceholder = !!v
     }
     },
    
     destroyed() {
     this.removeScrollEvent()
     },
    
     methods: {
     handleScroll(e) {
     const scrollOffsetTop = this.$el.offsetTop - this.top
     if (this.scrollElement.scrollTop >= scrollOffsetTop) {
     this.fixedClass = 'top-fixed'
     } else {
     this.fixedClass = ''
     }
     },
    
     initScrollElement() {
     if (!this.isMounted) return
     const parent = this.parent
     let element = null
     if (parent) {
     element = document.querySelector(parent)
     if (element === this.scrollElement) return
     } else if (this.$el) {
     element = getFirstScrollElement(this.$el)
     }
     if (element) {
     this.removeScrollEvent()
     this.scrollElement = element
     this.scrollElement.addEventListener('scroll', this.handleScroll)
     }
     },
    
     removeScrollEvent() {
     if (this.scrollElement) {
     this.scrollElement.removeEventListener('scroll', this.handleScroll)
     }
     }
     }
    }
    </script>
    
    <style lang="scss">
    .cpt-sticky {
     .top-fixed {
     position: fixed;
     width: 100%;
     background: #fff;
     }
    }
    </style>

    錨點定位

    網頁中經常會有用到錨點定位的場景,例如百度知道的目錄,我目前知道有三種方式可以實現這種功能。

    1. 使用 a 標簽定位
    2. 使用 js 定位

    使用 a 標簽定位

    先說說 a 標簽定位,這是一種最常用的定位方式。它有兩種實現方式,一種是通過 herf 屬性鏈接的指定元素的 id。另一種是添加一個 a 標簽,再將 href 屬性鏈接到這個 a 標簽的 name 屬性。

    <a href="#view1">按鈕1</a>
    <a href="#view2">按鈕1</a>
    ...
    <div id="view1">視圖1</div>
    <div><a name="view2">視圖2</a></div>

    這種定位方式很簡單,它支持任意標簽定位。不過它也存在一些問題,例如如果滾動區內有固定或絕對定位,會出現遮罩問題,還有瞬間滾動到頂部,交互不是很好,當然這些都可以通過 css 解決。但最主要問題是,a 標簽定位會改變路由的 hash,如果有相應的路由的話會進行路由跳轉。

    通過 js 模擬錨點定位

    通過 js 去操作元素的 `scrollTop` 等屬性,使其滾動到父級滾動元素指定的位置,就能實現定位效果。這里簡單提一下 `scrollIntoView()` 這個方法,根據MDN 的描述,`Element.scrollIntoView()` 方法讓當前的元素滾動到瀏覽器窗口的可視區域內。`scrollIntoView()` 還支持動畫的選項,通過 `behavior` 設置,不過遺憾的是它遇到固定定位也會出現遮蓋的問題,所以最終選擇手動去擼碼,不過 `scrollIntoView()` 倒是很適合做回到頂部這種功能。

    首先我們需要讓按鈕和滾動區內容元素建立對應關系,在按鈕的值中放入對應的內容區元素的 css 選擇器,根據點擊按鈕的值找到對應的元素。所以計算規則是這個元素距離滾動區的高度加上這個元素上邊距的高度(我在內容區加了外邊距,我希望顯示它),減去滾動區距離可視區的高度(我的頁面沒有定位,所以 offsetTop 對應可視區),再減去按鈕組件的高度,就可以得出需要滾動的位置。

    <template>
     <div class="cpt-anchor">
     <el-radio-group
     v-model="selector"
     size="mini"
     @change="handleMenuChange">
     <el-radio-button
     v-for="menu in menus"
     :key="menu.value"
     :label="menu.value">
     {{ menu.label }}
     </el-radio-button>
     </el-radio-group>
     </div>
    </template>
    
    <script>
    // 添加緩動函數
    import { tween } from 'shifty'
    // 類似 lodash.get 但處理了 null 類型
    import { get as _get } from 'noshjs'
    import { getFirstScrollElement } from 'util.js'
    
    export default {
     props: {
     // 滾動區距離可視區頂部的高度
     top: {
     type: Number,
     default: 0
     },
     menus: {
     type: Array,
     default: []
     }
     },
    
     data() {
     return {
     selector: ''
     }
     },
    
     watch: {
     menus: {
     immediate: true,
     handler(list) {
     this.selector = _get(list, [0, 'value'], '')
     }
     }
     },
    
     methods: {
     handleMenuChange(selector) {
     const scrollElement = document.querySelector(select)
     const rootScrollElement = getFirstScrollElement(scrollElement)
     if (scrollElement && rootScrollElement) {
     const offsetTop = scrollElement.offsetTop + scrollElement.clientTop
     const offsetHeight = _get(this.$el, ['parentElement', 'offsetHeight'], 0)
     const top = offsetTop - this.top - offsetHeight
    
     // 做一個緩動處理
     tween({
     from: { x: rootScrollElement.scrollTop },
     to: { x: top },
     duration: 500,
     easing: 'easeOutQuint',
     step: ({ x }) => {
     rootScrollElement.scrollTop = x
     }
     }).then(({ x }) => {
     rootScrollElement.scrollTop = x
     })
     }
     }
     }
    }
    </script>

    錨點與視圖聯動

    接下來我們來看看最后一個功能,當用戶滾動內容區時,高亮距離按鈕組件最近的那個元素所對應的按鈕。這個功能我可以看成是目錄導航,當我們查看不同內容時高亮對應的目錄。

    這個功能如何實現呢,我們來分析一下,當查看不同內容時會滾動屏幕,所以我們要給按鈕的父級滾動元素綁定 `scroll` 事件。判斷當前滾動區距離按鈕最近的元素,我們需要在這個元素上添加與按鈕中的值對應的 css 選擇器。當內容區發生滾動時根據按鈕獲取內容區中所有的元素,然后將滾動區元素的 `scrollTop` 減去按鈕元素的高度,即得出按鈕下方的滾動高度,然后再遍歷這些元素頭部和尾部是否包含了這個滾動高度,然后找到這個元素對應的按鈕。

    上面的結論已經可以完成,但存在一些問題,先說第一個問題導致按鈕導航失效,只導航到下一個按鈕邊結束。這個問題不一定會所有人都遇到,之所以我會遇到這個問題,是因為我用了 `Element` 的 `Radio` 組件,要高亮的時候變更了 v-model 的值導致。而點擊按鈕時會觸發滾動,就會和聯動高亮的事件沖突了,所以用一個 `isScroll` 變量標記當前是否是錨點定位狀態,定位狀態不觸發滾動操作。

    <template>
     <div class="cpt-anchor">
     <el-radio-group
     v-model="selector"
     size="mini"
     @change="handleMenuChange">
     <el-radio-button
     v-for="menu in menus"
     :key="menu.value"
     :label="menu.value">
     {{ menu.label }}
     </el-radio-button>
     </el-radio-group>
     </div>
    </template>
    
    <script>
    import { tween } from 'shifty'
    import { get as _get } from 'noshjs'
    import { getFirstScrollElement } from 'util.js'
    
    import TabMenus from 'components/tab-menus.vue'
    
    export default {
     props: {
     top: {
     type: Number,
     default: 0
     },
     menus: {
     type: Array,
     default: []
     },
     parent: {
     type: String,
     default: ''
     }
     },
    
     data() {
     return {
     menu: '',
     isScroll: true,
     isMounted: false,
     scrollTop: 0,
     anchorChange: false,
     rootScrollElement: ''
     }
     },
    
     mounted() {
     this.isMounted = true
     this.getScrollElement()
     },
    
     watch: {
     parent: {
     immediate: true,
     handler: 'getScrollElement'
     },
    
     menus: {
     immediate: true,
     handler(list) {
     this.menu = _get(list, [0, 'prop'], '')
     }
     },
    
     scrollTop(v) {
     if (this.anchorChange) {
     // 切換按鈕會滾動視圖,$nextTick 之后按鈕值改變了,但滾動可能還沒有結束,所以需要打個標記。
     this.isScroll = true
     }
     }
     },
    
     methods: {
     handleMenuChange(select) {
     this.isScroll = false
     this.anchorChange = false
     // 滾動高度等于元素距離可視區頭部高度減去元素自身高度與元素上邊框高度以及滾動區距離可視區頭部的高度。
     const scrollElement = document.querySelector(select)
     if (scrollElement && this.rootScrollElement) {
     const offsetTop = scrollElement.offsetTop + scrollElement.clientTop
     const offsetHeight = _get(
     this.$el,
     ['parentElement', 'offsetHeight'],
     0
     )
     const top = offsetTop - this.top - offsetHeight
    
     // 做一個緩動處理
     tween({
     from: { x: this.rootScrollElement.scrollTop },
     to: { x: top },
     duration: 500,
     easing: 'easeOutQuint',
     step: ({ x }) => {
     this.rootScrollElement.scrollTop = x
     }
     }).then(({ x }) => {
     this.rootScrollElement.scrollTop = x
     })
    
     this.$nextTick(() => {
     this.anchorChange = true
     })
     }
     },
    
     getScrollElement() {
     if (!this.isMounted) return
     // 如果沒有傳入 parent 默認取第一個父級滾動元素
     const parent = this.parent
     let element = null
     if (parent) {
     element = document.querySelector(parent)
     // mount 之后 rootScrollElement 可能已經存在了,如果和上次一樣就不做任何操作。
     if (element === this.rootScrollElement) return
     } else if (this.$el) {
     element = getFirstScrollElement(this.$el.parentElement)
     }
     if (element) {
     this.removeScrollEvent()
     this.rootScrollElement = element
     this.rootScrollElement.addEventListener('scroll', this.handleScroll)
     }
     },
    
     removeScrollEvent() {
     if (this.rootScrollElement) {
     this.rootScrollElement.removeEventListener('scroll', this.handleScroll)
     }
     },
    
     handleScroll(event) {
     const scrollTop = this.rootScrollElement.scrollTop
     this.scrollTop = scrollTop
     if (!this.isScroll) return
     const { data, top } = this
     const offsetHeight = _get(this.$el, ['parentElement', 'offsetHeight'], 0)
     const scrollList = []
     data.forEach(item => {
     const element = document.querySelector(item.prop)
     if (element) {
     const top = element.offsetTop
     const rect = {
     top: top + element.clientTop - top - offsetHeight,
     bottom: top + element.offsetHeight - top - offsetHeight
     }
     scrollList.push(rect)
     }
     })
     // 遍歷按鈕元素的 top 和 bottom,查看當前滾動在那個元素的區間內。
     scrollList.some((it, index) => {
     if (index && scrollTop >= it.top && top < it.bottom) {
     const menu = _get(data, [index, 'prop'], '')
     if (menu) this.menu = menu
     return true
     } else {
     // 當小于最小高度時,就等于最小高度
     if (scrollTop >= 0 && scrollTop < it.bottom) {
     const menu = _get(data, [index, 'prop'], '')
     if (menu) this.menu = menu
     return true
     }
     }
     })
     }
     }
    }
    </script>
    
    <style lang="scss">
    .cpt-anchor {
     padding-top: 4px;
     .cpt-tab-menus {
     margin: 0;
     .el-radio-button {
     margin-left: 10px;
     .el-radio-button__inner {
     border: none;
     border-radius: 5px 5px 0 0;
     border-bottom: 2px solid #e4e7ed;
     background-color: #f6f6f8;
     font-size: 16px;
    
     &:hover {
     border-bottom: 2px solid #409eff;
     }
     }
    
     &.is-active {
     .el-radio-button__inner {
     color: #fff;
     border: none;
     border-radius: 5px 5px 0 0;
     background-color: #409eff;
     border-bottom: 2px solid #409eff;
     box-shadow: none;
     }
     }
     }
     }
    }
    </style>

    吸頂錨點組件

    最后將上面兩個組件合并到一起就是我們所需要的吸頂錨點組件了。

    <template>
     <div class="cpt-sticky-anchor">
     <sticky :top="top" :z-index="zIndex">
     <sticky-menu :top="top" :data="menus" :parent="parent"></sticky-menu>
     </sticky>
     // 滾動區內容存放位置
     <slot></slot>
     </div>
    </template>
    
    <script>
    import Sticky from './sticky.vue'
    import StickyMenu from './menu.vue'
    
    export default {
     // 這里簡寫了,因為上面已經有了。
     props: {
     top,
     menus,
     parent,
     zIndex,
     offsetHeight
     },
    
     components: {
     Sticky,
     StickyMenu
     }
    }
    </script>

    使用示例

    <template>
     <div class="page-demo">
     ... 其他內容
     <sticky-anchor menus="menus" parent=".page-demo">
     <ul>
     <li class="button-1">視圖一</li>
     <li class="button-2">視圖二</li>
     </ul>
     </sticky-anchor>
     </div>
    </template>
    
    <script>
    import StickyAnchor from 'components/sticky-anchor.vue'
    
    export default {
     data() {
     return {
     menus: [
     { label: '按鈕一', value: '.button-1' },
     { label: '按鈕二', value: '.button-2' }
     ]
     }
     },
    
     components: {
     StickyAnchor
     }
    }
    </script>

    總結

    到這里整個功能已經全部實現了,我們來總結一下。

    吸頂效果用兩種解決方案,如果瀏覽支持 sticky 布局,使用 css 更加方便。 使用 a 標簽做錨點定位更加簡單,但遇到定位布局需要特殊處理,但會改變路由 hash。 做錨點與滾動聯動時需要注意按鈕點擊事件與滾動事件沖突。

    聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com

    文檔

    實現一個 Vue 吸頂錨點組件方法

    實現一個 Vue 吸頂錨點組件方法:前言 近期產品小哥哥給我提了一個新需求,在一個頁面的滾動區中添加一組錨點定位按鈕,點擊按鈕將對應的元素顯示在頁面的可視區中。當按鈕組超出頁面可視區的時候將其固定在滾動區域的頭部,當滾動區滾動時,高亮距離滾動區頂部最近的元素所匹配的錨點按鈕。
    推薦度:
    標簽: 一個 VUE 實現
    • 熱門焦點

    最新推薦

    猜你喜歡

    熱門推薦

    專題
    Top
    主站蜘蛛池模板: 久久国产免费观看精品| 精品国产免费人成网站| 色综合久久精品中文字幕首页| 久久精品亚洲乱码伦伦中文| 99re这里只有精品热久久| 亚洲精品无码99在线观看| 国产精品成人h片在线| 98视频精品全部国产| 亚洲精品成人久久久| 国产精品日韩欧美久久综合 | 久久精品国产91久久麻豆自制| 四虎国产精品永久免费网址| 无码人妻精品一区二区蜜桃百度 | 久久精品国产久精国产思思| 亚洲国产欧美日韩精品一区二区三区| 国产精品无码素人福利| 成人精品一区二区三区| 四虎国产精品免费观看| 国产精品爱啪在线线免费观看| 久久亚洲日韩精品一区二区三区| 中文字幕日韩精品无码内射| 日产国产精品亚洲系列| 日本一区二区三区精品国产| 欧美精品三区| 老年人精品视频在线| 精品久久久久久无码人妻蜜桃| 91精品国产成人网在线观看| 国产高清国产精品国产专区| 97在线精品视频| 国产成人精品视频播放| 黑人巨大精品欧美| 国产精品免费一区二区三区四区| 久久亚洲私人国产精品vA| 亚洲韩国精品无码一区二区三区| 久久99精品久久久久久齐齐| 国产福利精品视频自拍| 亚洲国产精品久久久久| 久久国产精品久久国产精品| 99精品国产一区二区三区2021 | 亚洲乱码精品久久久久..| 久久久久99精品成人片三人毛片|