原生js系列之無限循環輪播組件

ag355o9pn7 7年前發布 | 30K 次閱讀 JavaScript開發 JavaScript

前情回顧

在上一篇文章中,我們封裝了一個DOM庫(qnode),為了讓大家直觀地感受到其方便友好的自定義工廠模式,于是給大家帶來了這篇文章。

沒有看過上一篇文章的話,可以在這里找到: 原生js系列之DOM工廠模式

那么這篇文章,我們將基于上述的 qnode ,從頭開始寫一個無限循環輪播圖的組件。

思路講解

先看一張輪播布局圖:

滑動的時候,整個輪播容器整體前進或后退一格,通過css3過渡效果的設置,來達到滑動的效果。也許你會疑惑,頭尾怎么會多出兩張圖呢?

其實無限循環輪播的核心就在于頭尾多出的兩張圖,從圖三再向后滑動,會滑到紅色圖一(我稱之為占位圖一),這個時候給用戶的感覺就是無縫從最后一張滑動到第一張的,當他滑到占位圖一時,我們再瞬間切換到粉色圖一(即真正的圖一),由于是瞬間變換,用戶是感知不到的。同理,從圖一滑到圖三也一樣。由此,周而復始,無窮無盡,給人的感覺是永遠也不會到盡頭,當然個中奧妙只有我們知道哈哈。

目錄結構

swiper
├── README.md
├── index.js
├── qnode
│   ├── index.js
│   ├── method.js
│   └── store.js
├── render
│   ├── index.js
│   ├── indicator.js
│   └── list.js
└── styles
    ├── indicator.mcss
    ├── list.mcss
    └── wrap.mcss

說明:mcss文件是通過 css-modules 來編譯的,給class名稱生成唯一標識,防止命名沖突。這里有我配置好的一套腳手架,覺得webpack配置麻煩的話,可以clone我這個項目來編譯代碼: webpack-build

代碼編寫

index.js

import qnode from './qnode'
import render from './render'

const defaults = { initIndex: 1, autoplay: { use: true, delay: 3000 }, slide: { use: true, scale: 1 / 3, speed: 0.2 }, indicator: { use: true, bottom: '', dotClass: '', dotActiveClass: '' } }

export default function swiper (node, { datas, initIndex, slide, autoplay, indicator }) { if (!node || !datas || !datas.length) return

// 儲存數據的前后順序很重要,一定要在調用前設置 qnode.setStore('datas', datas) qnode.setStore('index', (initIndex || defaults.initIndex) - 1) qnode.setStore('slide', Object.assign({}, defaults.slide, slide)) qnode.setStore('autoplay', Object.assign({}, defaults.autoplay, autoplay)) qnode.setStore('indicator', Object.assign({}, defaults.indicator, indicator))

// 渲染dom并儲存在qnode,以便后續的獲取和操作 render()

// 自動輪播 qnode.execMethod('autoplay') // 滑動翻頁 qnode.execMethod('slide')

// 掛載到真實的節點上 qnode.getNode('wrap').appendTo(node) }</code></pre>

render/index.js

import qnode from '../qnode'
import renderList from './list'
import renderIndicator from './indicator'

import mcss from '../styles/wrap.mcss'

export default function () { renderList() // 渲染列表 renderIndicator() // 渲染指示器,若沒有開啟則不會渲染

qnode.setNode('wrap', '$div') .addClass(mcss.wrap) .append([ qnode.getNode('list'), qnode.getNode('indicator') // 有可能沒有值,這一層我們的qnode會過濾調,所以放心大膽地寫 ]) }</code></pre>

render/list.js

import { isElement, isString } from '@m/utils/is'
import qnode from '../qnode'

import mcss from '../styles/list.mcss'

function getItemNode (data) { const qItem = qnode.q('$div').addClass(mcss.item)

if (isElement(data)) { return qItem.append(data) }

if (isString(data)) { return qItem.html(data) }

return qItem.html(&lt;a href="${data.href || 'javascript:;'}" target="${data.target || '_self'}"&gt; &lt;img src="${data.src}" alt="img" /&gt; &lt;/a&gt;) }

export default function () { const datas = qnode.getStore('datas') const tdTime = qnode.getStore('tdTime') const posIndex = qnode.getStore('index') + 1

const qItems = datas.map(item => getItemNode(item))

// 首位多插入一個節點,用于視覺感知,交互完成后瞬間替換到相應的節點 qItems.unshift(getItemNode(datas[datas.length - 1])) qItems.push(getItemNode(datas[0]))

qnode.setNode('list', '$div') .addClass(mcss.list) .style({ transitionDuration: tdTime + 'ms', transform: translateX(${posIndex * -100}%) }) .append(qItems) }</code></pre>

render/indicator.js

import qnode from '../qnode'

import mcss from '../styles/indicator.mcss'

export default function () { const indicator = qnode.getStore('indicator') const last = qnode.getStore('datas').length - 1 const index = qnode.getStore('index') const dotClass = indicator.dotClass || mcss.dot const dotActiveClass = indicator.dotActiveClass || mcss.dotActive

if (indicator.use) { let qDots = [] for (let i = 0; i <= last; i++) { qDots.push( qnode.q('$div').addClass(dotClass, (i === index) && dotActiveClass) ) }

qnode.setNode('dots', qDots)
qnode.setStore('dotActiveClass', dotActiveClass)
qnode.setNode('indicator', '$div')
  .addClass(mcss.indicator)
  .style('bottom', indicator.bottom)
  .append(qDots)

} }</code></pre>

qnode/index.js

import { QNode } from '@m/qnode'
import { tdTime } from './store'
import { change, autoplay, slide, indicator } from './method'

const qnode = new QNode()

qnode.setStore('tdTime', tdTime)

qnode.setMethod('change', change) qnode.setMethod('autoplay', autoplay) qnode.setMethod('slide', slide) qnode.setMethod('indicator', indicator)

export default qnode</code></pre>

qnode/store.js

// 靜態數據可以放在這里
export const tdTime = 500

qnode/method.js

import touchSlide from './touchSlide'

// 翻頁處理 export function change (isNext) { let index = this.getStore('index') let cacheIndex = index // 用于記錄上一次的索引,移除指示器激活樣式時使用 let last = this.getStore('datas').length - 1 let tdTime = this.getStore('tdTime') let qList = this.getNode('list') let isNextContinue = isNext && (index === last) let isPrevContinue = !isNext && (index === 0) let posIndex = index + (isNext ? 2 : 0)

if (isNextContinue || isPrevContinue) { // 滑到占位圖 qList.style('transform', translateX(${posIndex * -100}%)) index = isNextContinue ? 0 : last

setTimeout(() => {
  qList.style({
    transitionDuration: '0ms',
    transform: `translateX(${(index + 1) * -100}%)`
  })
}, tdTime)

} else { qList.style({ transitionDuration: tdTime + 'ms', transform: translateX(${posIndex * -100}%) }) index += isNext ? 1 : -1 }

this.setStore('index', index) this.execMethod('indicator', cacheIndex, index) }

// 自動輪播 export function autoplay () { let opt = this.getStore('autoplay')

if (!opt.use) return

let timer = setInterval(() => { this.execMethod('change', true) }, opt.delay)

this.setStore('timer', timer) }

// 滑動處理 export function slide () { let qWrap = this.getNode('wrap') let qList = this.getNode('list') let tdTime = this.getStore('tdTime') let slideData = this.getStore('slide') let self = this

if (!slideData.use) return

touchSlide(qWrap.current(), { delay: 0, start () { // 清除輪播定時器和css3過渡效果 clearTimeout(self.getStore('timer')) qList.style('transitionDuration', '0ms') }, move (info) { let posIndex = self.getStore('index') + 1 let move = info.disX / qWrap.width() 100 let total = posIndex -100 + move

  qList.style('transform', `translateX(${total}%)`)
},
end (info) {
  // 開啟輪播和css3過渡效果
  self.execMethod('autoplay')
  qList.style('transitionDuration', tdTime + 'ms')

  let posIndex = self.getStore('index') + 1
  let scale = Math.abs(info.disX) / qWrap.width()
  let speed = Math.abs(info.speedX)

  if (scale >= slideData.scale || speed >= slideData.speed) {
    self.execMethod('change', info.disX < 0) // 翻頁
  } else {
    qList.style('transform', `translateX(${posIndex * -100}%)`)
  }
}

}) }

// 修改指示器索引 export function indicator (lastIndex, currIndex) { const qDots = this.getNode('dots') const dotActiveClass = this.getStore('dotActiveClass')

if (qDots && dotActiveClass) { qDots[lastIndex].removeClass(dotActiveClass) qDots[currIndex].addClass(dotActiveClass) } }</code></pre>

touchSlide.js

// 截流
function throttle (fn, delay = 100) {
  let wait = false

return function () { if (!wait) { fn && fn.apply(this, arguments) wait = true

  setTimeout(() => {
    wait = false
  }, delay)
}

} }

/*

  • 滑動
  • @param {HTMLElement} node
  • @param {Object} {
  • delay = 100, // move截流時間
  • start, // 滑動開始
  • 參數: pageX, pageY
  • move, // 滑動中,會不斷地觸發,可以通過截流來限制觸發頻率
  • 參數:
    time, // 總時間:ms
    disX, // 總路程:px
    disY,
    addX, // 路程增量:px
    addY,
    speedX: disX / time, // 平均速度:px/ms
    speedY: disY / time
  • end, // 滑動結束,參數同move
  • } */ export default function (node, { delay = 100, start, move, end }) { if (!node) return

    let sTouch, eTouch, sTime let touch, time, disX, disY, addX, addY

    node.addEventListener('touchstart', e => { e.preventDefault()

    sTime = e.timeStamp sTouch = eTouch = e.targetTouches[0]

    start && start({ pageX: sTouch.pageX, pageY: sTouch.pageY }) }, false)

    node.addEventListener('touchmove', throttle(e => { touch = e.targetTouches[0] time = e.timeStamp - sTime disX = touch.pageX - sTouch.pageX disY = touch.pageY - sTouch.pageY addX = touch.pageX - eTouch.pageX addY = touch.pageY - eTouch.pageY

    move && move({ time, // 總時間:ms disX, // 總路程:px disY, addX, // 路程增量:px addY, speedX: disX / time, // 平均速度:px/ms speedY: disY / time })

    // 記錄上一次touch eTouch = touch }, delay), false)

    node.addEventListener('touchend', e => { touch = e.changedTouches[0] time = e.timeStamp - sTime disX = touch.pageX - sTouch.pageX disY = touch.pageY - sTouch.pageY addX = touch.pageX - eTouch.pageX addY = touch.pageY - eTouch.pageY

    end && end({ time, disX, disY, addX, addY, speedX: disX / time, speedY: disY / time }) }, false) }</code></pre>

    styles/wrap.mcss

    .wrap {
    position: relative;
    overflow: hidden;
    transform: translate3d(0, 0, 0);
    }

    styles/list.mcss

    .list {
    display: flex;
    flex-direction: row;
    transform: translateX(0);
    transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
    }

.item { flex-basis: 100%; flex-shrink: 0; box-sizing: border-box;

a { display: block; font-size: 0;

img {
  width: 100%;
  height: auto;
}

} }</code></pre>

styles/indicator.mcss

.indicator {
  position: absolute;
  bottom: 1em;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
}

.dot { width: 1em; height: 0.12em; margin: 0 0.12em; background-color: rgba(255, 255, 255, 0.5);

&-active { background-color: #fff; } }</code></pre>

README

參數

  • node: 要掛載的dom節點,必須
  • options: 如下(其中datas是必要的)
{
  initIndex: 1, // 初始化展示的索引
  autoplay: { // 自動輪播設置
    use: true, // 開關
    delay: 3000 // 間隔3s
  },
  slide: { // 手指滑動設置
    use: true, // 開關
    scale: 1/3, // 劃過總共寬度的1/3則翻頁
    speed: 0.2 // 滑動的速度超過0.2px/ms則翻頁,即快速滑動也可以翻頁
  },
  indicator: { // 索引指示器設置
    use: true, // 開關
    bottom: '', // 底部的距離
    dotClass: '', // 自定義圓點樣式
    dotActiveClass: '' // 自定義激活樣式
  },
  datas: [ // 圖片數據
    {
      src: 'xxx', // 圖片URL
      href: '/', // 圖片錨點,可以不設置
      target: '_blank' // 點擊錨點的跳轉處理(是在當前頁打開還是新建窗口)
    }
  ]
}

示例

import swiper from '@c/swiper'

import img1 from './images/1.jpg' import img2 from './images/2.jpg' import img3 from './images/3.jpg' import img4 from './images/4.jpg' import img5 from './images/5.jpg' import img6 from './images/6.jpg'

const rootNode = document.getElementById('root')

swiper(rootNode, { // initIndex: 1, // autoplay: { // use: true, // delay: 3000 // }, // slide: { // use: true, // scale: 1/3, // speed: 0.2 // }, // indicator: { // use: true, // bottom: '', // dotClass: '', // dotActiveClass: '' // }, datas: [ { src: img1, href: '/', target: '_blank' }, { src: img2, href: '/', target: '_blank' }, { src: img3, href: '/', target: '_blank' }, { src: img4, href: '/', target: '_blank' }, { src: img5, href: '/', target: '_blank' }, { src: img6, href: '/', target: '_blank' } ] })</code></pre>

使用心得

總體來說使用 qnode 來開發的話還是比較方便的,文件拆分以及數據共享都可以做到,唯一有一點瑕疵的話,就是對于js執行的順序要慎重考慮。想一想為什么render文件暴露出來的是函數,原因就是因為此時數據還未儲存到 qnode ,因此通過函數來進行惰性加載,在合適的地方執行。

對于 qnode ,目前還沒有錯誤提醒,調用方式不對的話沒有信息吐出,后續可以考慮補上這個功能,畢竟其他開發者用的話,可能并不熟悉API,調用姿勢不對也是有可能發生的。

以上就是本文的全部內容了。

附:

 

來自:https://segmentfault.com/a/1190000012432451

 

 本文由用戶 ag355o9pn7 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!