不到200行用Vue實現類似Swiper.js的輪播組件

来源:https://www.cnblogs.com/zyronon/p/18151917
-Advertisement-
Play Games

前言 大家在開發過程中,或多或少都會用到輪播圖之類的組件,PC和Mobile上使用 Swiper.js ,小程式上使用swiper組件等。 本文將詳細講解如何用Vue一步步實現的類似Swiper.js的功能,無任何第三方依賴,乾貨滿滿。 最終效果 線上預覽:https://zyronon.githu ...


前言

大家在開發過程中,或多或少都會用到輪播圖之類的組件,PC和Mobile上使用 Swiper.js ,小程式上使用swiper組件等。

本文將詳細講解如何用Vue一步步實現的類似Swiper.js的功能,無任何第三方依賴,乾貨滿滿。

最終效果

線上預覽:https://zyronon.github.io/douyin/

項目源代碼:https://github.com/zyronon/douyin

註意PC 必須將瀏覽器切到手機模式,先按 F12 調出控制台,再按 Ctrl+Shift+M才能正常預覽

Demo代碼

上面的預覽地址是最終實現的效果,下麵才是本文代碼實現的效果

為提升閱讀體驗,正文中代碼展示有部分省略處理,完整代碼可以在codesandbox上查看:
https://codesandbox.io/p/devbox/mutable-grass-zm4gl5

實現原理

佈局

我們需要用到兩個div,父元素 slide 設置 overflow: hidden 禁止滾動,子元素 slide-list 使用 flex 佈局,然後將需要滾動的頁面做為孫元素放在子元素 slide-list 中,由於子元素 slide-listflex 佈局,頁面會自然的平鋪排列

因為父元素 slideoverflow: hidden屬性會將內容裁減,不提供滾動條,也不允許用戶滾動,所以我們只能看到父元素 slide 寬高的內容。

<div class="slide">
  <div class="slide-list">
    <slot></slot>
  </div>
</div>
.slide {
  touch-action: none;
  height: 100%;
  width: 100%;
  transition: height 0.3s;
  position: relative;
  overflow: hidden;
}

 .slide-list {
    height: 100%;
    width: 100%;
    display: flex;
    position: relative;
  }

滑動

實現滾動的關鍵點在於CSS3transform: translate(0, 0) 屬性。

translate()  這個 CSS 函數在水平和/或垂直方向上重新定位元素,它的坐標定義了元素在每個方向上移動了多少。

因為子元素 slide-list 的內容是平鋪的,我們只需要在子元素 slide-list 監聽對應的事件,計算滑動的距離xy,再動態設置到子元素 slide-listtransform: translate(x, y)裡面,就可以實現頁面滑動了

總結

大家可以將整個流程理解為播放膠片電影:父元素 A 是放映機,子元素 B 是膠片,而頁面是印刷在膠片上的內容。膠片每移動一格,我們就能看到新的一幀電影

實現

監聽事件

PC 上的點擊、移動,H5 的手勢操作,都離不開 DOM 事件監聽。例如滑鼠移動事件對應 mousemove,移動端因為沒有滑鼠則對應 touchmove
我們可以通過 Pointer 事件進行多端統一的事件監聽,實現觸屏和 PC 端通用

<div class="slide horizontal">
  <div
      class="slide-list"
      ref="wrapperEl"
      @pointerdown="onPointerDown"
      @pointermove="onPointerMove"
      @pointerup="onPointerUp"
  >
    <slot></slot>
  </div>
</div>

初始化

組件預設變數

//slide-list的ref引用
const wrapperEl = ref(null)

const state = reactive({
  judgeValue: 20,//一個用於判斷滑動朝向的固定值
  type: SlideType.VERTICAL,//組件類型
  name: props.name,
  localIndex: props.index,//當前下標
  needCheck: true,//是否需要檢測,每次按下都需要檢測,up事件會重置為true
  next: false,//能否滑動
  isDown: false,//是否按下,用於move事件判斷
  start: {x: 0, y: 0, time: 0},//按下時的起點坐標
  move: {x: 0, y: 0},//移動時的坐標
  wrapper: {width: 0, height: 0, childrenLength: 0}//slide-list的寬度和子元素數量
})
function slidePointerDown(e, el, state) {
  Utils.$setCss(el, 'transition-duration', `0ms`)
  //記錄起點坐標,用於move事件計算移動距離
  state.start.x = e.pageX
  state.start.y = e.pageY
  //記錄按下時間,用於up事件判斷滑動時間
  state.start.time = Date.now()
  state.isDown = true
}

雖然我們用 Pointer事件統一了移動端和PC端的監聽事件,但 pointermove 事件在 PC 和移動端表現出來的效果卻不一樣,在 PC 上, pointermove 事件和 mousemove 事件一致,只要滑鼠在目標元素上方,就會觸發。而在移動端上卻只有按下並移動時發才會觸發

所以這裡用一個 isDown 的變數保存是否按下的狀態,pointermove事件雖然會一直觸發,但僅當 isDown 時才執行我們的代碼邏輯

移動過程

function slidePointerMove(e,el,state) {
  if (!state.isDown) return;

  //計算移動距離
  state.move.x = e.pageX - state.start.x
  state.move.y = e.pageY - state.start.y

  //檢測能否滑動
  let canSlideRes = canSlide(state)

  //是否是往下(右)滑動
  let isNext = state.type === SlideType.HORIZONTAL ? state.move.x < 0 : state.move.y < 0

  if (canSlideRes) {
    if (canNext(state, isNext)) {
      //能滑動,那就把事件捕獲,不能給父組件處理
      Utils.$stopPropagation(e)

      //獲取偏移量
      let t = getSlideOffset(state, el) + (isNext ? state.judgeValue : -state.judgeValue)
      let dx1 = 0,
        dx2 = 0
      //偏移量加當前手指移動的距離就是slide要偏移的值
      if (state.type === SlideType.HORIZONTAL) {
        dx1 = t + state.move.x
      } else {
        dx2 = t + state.move.y
      }
      Utils.$setCss(el, 'transition-duration', `0ms`)
      Utils.$setCss(el, 'transform', `translate(${dx1}px, ${dx2}px)`)
    }
  }
}

用滑鼠當前的位置,再減去滑鼠按下時的位置,就是滑鼠移動的距離
移動距離再加上當前頁面 * 每個頁面的寬或高,即子元素 slide-list 整體要偏移的量

技術難點

1. 如何判斷滑動方向?是在上下滑還是左右滑?
//檢測在對應方向上能否允許滑動,比如SlideHorizontal組件就只處理左右滑動事件,SlideVertical 
//只處理上下滑動事件 
export function canSlide(state) {
  //每次按下都需要檢測,up事件會重置為true
  if (state.needCheck) {
    //判斷move x和y的距離是否大於判斷值,因為距離太小無法判斷滑動方向
    if (Math.abs(state.move.x) > state.judgeValue || Math.abs(state.move.y) > state.judgeValue) {
      //放大再相除,根據長寬比判斷方向,angle大於1就是左右滑動,小於是上下滑動
      let angle = (Math.abs(state.move.x) * 10) / (Math.abs(state.move.y) * 10)
      //根據當前slide的類型,判斷能否滑動,並記錄下來,後續不再判斷,直接返回記錄值
      state.next = state.type === SlideType.HORIZONTAL ? angle > 1 : angle <= 1
      state.needCheck = false
    } else {
      return false
    }
  }
  return state.next
}

放大移動距離後再相除,根據結果是否大於1判斷出滑動方向

2. 如何處理嵌套組件中的事件衝突?什麼時候攔截事件和放行事件?

由於事件的冒泡機制,事件是從最裡面的元素一級一級的往上冒泡的,所以我們只需在滿足下麵兩個條件時攔截事件即可

  1. 是否在往到頭或尾滑動
    如果在第一頁,不能往左/上滑動
    如果在最後一面, 不能往右/下滑動
function canNext(state, isNext) {
  return !(
    (state.localIndex === 0 && !isNext) ||
    (state.localIndex === state.wrapper.childrenLength - 1 && isNext)
  )
}
  1. 滑動方向和組件類型相匹配
  • SlideHorizontal.vue 組件只允許向左/右滑動
  • SlideVertical.vue 組件只允許向上/下滑動

滿足上述兩個條件時攔截事件,不滿足放行事件,交給上一級組件處理

//檢測在對應方向上能否允許滑動
let canSlideRes = canSlide(state)
//是否是往下(右)滑動
let isNext = state.type === SlideType.HORIZONTAL ? state.move.x < 0 : state.move.y < 0
if (canSlideRes) {
  if (canNext(state, isNext)) {
    //能滑動,那就把事件捕獲,不能給父組件處理
    Utils.$stopPropagation(e)
    ...
    滑動邏輯
    ...
  }
}

結束滑動

function slidePointerUp(e, state) {
  if (!state.isDown) return;
  let isHorizontal = state.type === SlideType.HORIZONTAL
  let isNext = isHorizontal ? state.move.x < 0 : state.move.y < 0
  if (state.next) {
    if (canNext(state, isNext)) {
      //結合時間、距離來判斷是否成功滑動
      let endTime = Date.now()
      let gapTime = endTime - state.start.time
      let distance = isHorizontal ? state.move.x : state.move.y
      let judgeValue = isHorizontal ? state.wrapper.width : state.wrapper.height
      //1、距離太短,直接不通過
      if (Math.abs(distance) < 20) gapTime = 1000
      //2、距離太長,直接通過
      if (Math.abs(distance) > judgeValue / 3) gapTime = 100
      //3、若不在上述兩種情況,那麼只需要判斷時間即可
      if (gapTime < 150) {
        if (isNext) state.localIndex++
        else state.localIndex--
      }
    }
  } 
  // 重置變數
  Utils.$setCss(el, 'transition-duration', `300ms`)
  let t = getSlideOffset(state, el)
  let dx1 = 0,dx2 = 0
  if (state.type === SlideType.HORIZONTAL) dx1 = t
  else dx2 = t
  Utils.$setCss(el, 'transform', `translate3d(${dx1}px, ${dx2}px, 0)`)
  ...
}

技術難點

  • 如何讓滑動結束時的動畫更絲滑?
    結合滑動時間、滑動距離來判斷滑動下一條還是保持當前條
    1、距離太短,直接不通過
    2、距離太長,直接通過
    3、若不在上述兩種情況,那麼只需要判斷時間即可,小於150毫秒以內就算是成功滑動

其他問題

PC 上滑動有圖片的頁面,圖片“分叉”了:我們開始拖動它的“克隆”

這是因為瀏覽器有自己的對圖片和一些其他元素的拖放處理。它會在我們進行拖放操作時自動運行,並與我們的拖放處理產生了衝突

禁用它:

@dragstart="(e) => Utils.$stopPropagation(e)"

PC 上滑動結束後觸發了click事件

問題分析

首先我們滑動是利用 pointerdown, pointermove, pointerup 三個事件組合形成的,但是 pointerup 執行之後, click 是一定會執行的,是無法避免的,是無法用preventDefault , stopPropagation , stopImmediatePropagation 阻止的, 因為pointer 事件和 click 事件本身就不是一個系列的,因此沒有關係,所以當發生滑動之後,pointerup 一定會執行,click 也會在 pointerup 執行後執行

解決方案

我們設置一個全局變數

window.isMoved = false

pointermove 事件中,將 window.isMoved 設為 true。然後在 pointerup 事件中,我們用一個定時器讓這個變數在200毫秒之後發生改變為 false,因為 pointerup 之後 click 很快就觸發了,不到200ms,因此可以保證變數還沒有發生變化,click 事件裡面去檢測這個變數,如果是變化之前,那麼不執行

如果 click 事件少還好說,直接複製幾遍無所謂。
但是一般來說 click 事件在項目中使用還是挺多的,有沒有什麼一勞永逸的辦法呢?
大部分監聽 click 事件都是用 Vue@click 添加的,我們無法插手

這時給大家介紹一下 Proxy 這個對象了,Vue3 的雙向綁定就用到了 Proxy 對象。
在項目入口,我們直接代理 HTMLElement.prototype.addEventListener 這個事件,代理了之後,Vue@click 語法糖添加事件時就會通知我們,這時再進行判斷是不是 click 事件,是的話再判斷 window.isMoved 的狀態

window.isMoved = false
HTMLElement.prototype.addEventListener = new Proxy(HTMLElement.prototype.addEventListener, {
  apply(target, ctx, args) {
    const eventName = args[0]
    const listener = args[1]
    if (listener instanceof Function && eventName === 'click') {
      args[1] = new Proxy(listener, {
        apply(target, ctx, args) {
          if (window.isMoved) return
          try {
            return target.apply(ctx, args)
          } catch (e) {
            console.error(`[proxyPlayerEvent][${eventName}]`, listener, e)
          }
        }
      })
    }
    return target.apply(ctx, args)
  }
})

設置了 overflow: auto 的頁面在移動端不觸發 pointermove 事件

再設置一個 touch-action:pan-y 就正常了

CSS 屬性 touch-action 用於設置觸摸屏用戶如何操縱元素的區域 (例如,瀏覽器內置的縮放功能), pan-y 啟用單指垂直平移手勢

總結

核心代碼加上註釋一共217行,我們實現了一個可以在 PCMobile 上通用,並且可以無限嵌套的輪播組件

結束

以上就是文章的全部內容,感謝看到這裡,希望對你有所幫助或啟發!創作不易,如果覺得文章寫得不錯,可以點贊收藏支持一下,也歡迎關註我的公眾號,我會更新更多實用的前端知識與技巧,期待與你共同成長~


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Oracle資料庫的告警日誌中出WARNING: too many parse errors這些告警信息的話,如果遇到這個問題,我們應該如何分析呢? 下麵簡單聊一下如何分析這個錯誤。該告警信息其實是12.2版本中的一個特性增強。在以前的Oracle版本中,資料庫出現瞭解析錯誤時,資料庫的alert日 ...
  • 本文分享自華為雲社區《重磅新品發佈!雲耀資料庫HRDS,享受輕量級的極致體驗!》,作者:GaussDB 資料庫。 所謂,凡有井水處,即能歌柳詞。 大數據時代,凡有數據處,必有資料庫。 隨著業務需求的不斷擴大和數據量的激增,資料庫的使用場景滲透到了生活的方方面面,不再是大型企業或技術部門的專利,市場對 ...
  • 夏天馬上就要到了,“瘦身”不光是特定人群的需求,也是數據中心的需求。構建輕量化、低碳化、高性價比的新型數據中心,更有效地支撐經濟社會數字化轉型,已成為業界主流趨勢。 如何讓數據中心“熱辣瘦身”?輕量級存儲集群控制器——天翼雲存儲資源盤活系統HBlock必不可少! “HBlock健身房”究竟是如何發揮 ...
  • 前言 頁面轉場動畫是指在應用程式中,當用戶導航到另一個頁面時,使用動畫效果來過渡頁面之間的切換。這樣做的目的是為了提升用戶體驗,使頁面之間的切換更加平滑和有趣。 常見的頁面轉場動畫包括淡入淡出、滑動、翻轉、縮放等效果。通過使用這些動畫效果,可以給用戶一種流暢的感覺,讓頁面之間的切換更加自然。 在 ...
  • 前言 在HarmonyOS中,可以通過以下方法放大縮小視圖: 使用縮放手勢:可以使用雙指捏合手勢來放大縮小視圖。將兩個手指放在屏幕上,並向內或向外移動手指,即可進行放大或縮小操作。 使用系統提供的縮放控制項:在HarmonyOS的開發中,可以使用系統提供的縮放控制項來實現視圖的放大縮小功能。通過在布 ...
  • 要使用 MediaCodec 在 Android 上進行硬解碼,並獲取 RGBA 數據,你可以按照以下步驟進行操作: 創建 MediaExtractor 對象並設置要解碼的 MP4 文件路徑: MediaExtractor extractor = new MediaExtractor(); extr ...
  • 寫在前面 tips:點贊 + 收藏 = 學會! 我們已經介紹了radash的相關信息和部分Array相關方法,詳情可前往主頁查看。 本篇我們繼續介紹radash中Array的相關方法的剩餘方法。 本期文章發佈後,作者也會同步整理出Array方法的使用目錄,包括文章說明和腦圖說明。 因為方法較多,後續 ...
  • 前言 還是上一篇面試官:來說說vue3是怎麼處理內置的v-for、v-model等指令? 文章的那個粉絲,面試官接著問了他另外一個v-model的問題。 面試官:vue3的v-model都用過吧,來講講。 粉絲:v-model其實就是一個語法糖,在編譯時v-model會被編譯成:modelValue ...
一周排行
    -Advertisement-
    Play Games
  • 基於.NET Framework 4.8 開發的深度學習模型部署測試平臺,提供了YOLO框架的主流系列模型,包括YOLOv8~v9,以及其系列下的Det、Seg、Pose、Obb、Cls等應用場景,同時支持圖像與視頻檢測。模型部署引擎使用的是OpenVINO™、TensorRT、ONNX runti... ...
  • 十年沉澱,重啟開發之路 十年前,我沉浸在開發的海洋中,每日與代碼為伍,與演算法共舞。那時的我,滿懷激情,對技術的追求近乎狂熱。然而,隨著歲月的流逝,生活的忙碌逐漸占據了我的大部分時間,讓我無暇顧及技術的沉澱與積累。 十年間,我經歷了職業生涯的起伏和變遷。從初出茅廬的菜鳥到逐漸嶄露頭角的開發者,我見證了 ...
  • C# 是一種簡單、現代、面向對象和類型安全的編程語言。.NET 是由 Microsoft 創建的開發平臺,平臺包含了語言規範、工具、運行,支持開發各種應用,如Web、移動、桌面等。.NET框架有多個實現,如.NET Framework、.NET Core(及後續的.NET 5+版本),以及社區版本M... ...
  • 前言 本文介紹瞭如何使用三菱提供的MX Component插件實現對三菱PLC軟元件數據的讀寫,記錄了使用電腦模擬,模擬PLC,直至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1. PLC開發編程環境GX Works2,GX Works2下載鏈接 https:// ...
  • 前言 整理這個官方翻譯的系列,原因是網上大部分的 tomcat 版本比較舊,此版本為 v11 最新的版本。 開源項目 從零手寫實現 tomcat minicat 別稱【嗅虎】心有猛虎,輕嗅薔薇。 系列文章 web server apache tomcat11-01-官方文檔入門介紹 web serv ...
  • 1、jQuery介紹 jQuery是什麼 jQuery是一個快速、簡潔的JavaScript框架,是繼Prototype之後又一個優秀的JavaScript代碼庫(或JavaScript框架)。jQuery設計的宗旨是“write Less,Do More”,即倡導寫更少的代碼,做更多的事情。它封裝 ...
  • 前言 之前的文章把js引擎(aardio封裝庫) 微軟開源的js引擎(ChakraCore))寫好了,這篇文章整點js代碼來測一下bug。測試網站:https://fanyi.youdao.com/index.html#/ 逆向思路 逆向思路可以看有道翻譯js逆向(MD5加密,AES加密)附完整源碼 ...
  • 引言 現代的操作系統(Windows,Linux,Mac OS)等都可以同時打開多個軟體(任務),這些軟體在我們的感知上是同時運行的,例如我們可以一邊瀏覽網頁,一邊聽音樂。而CPU執行代碼同一時間只能執行一條,但即使我們的電腦是單核CPU也可以同時運行多個任務,如下圖所示,這是因為我們的 CPU 的 ...
  • 掌握使用Python進行文本英文統計的基本方法,並瞭解如何進一步優化和擴展這些方法,以應對更複雜的文本分析任務。 ...
  • 背景 Redis多數據源常見的場景: 分區數據處理:當數據量增長時,單個Redis實例可能無法處理所有的數據。通過使用多個Redis數據源,可以將數據分區存儲在不同的實例中,使得數據處理更加高效。 多租戶應用程式:對於多租戶應用程式,每個租戶可以擁有自己的Redis數據源,以確保數據隔離和安全性。 ...