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

前情回顧

在上一篇文章中,咱們封裝了一個DOM庫(qnode),爲了讓你們直觀地感覺到其方便友好的自定義工廠模式,因而給你們帶來了這篇文章。javascript

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

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

思路講解

先看一張輪播佈局圖:java

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

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

目錄結構

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-buildcss3

代碼編寫

index.jsgit

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)
}
複製代碼

render/index.jsgithub

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會過濾調,因此放心大膽地寫
    ])
}
複製代碼

render/list.jsweb

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(` <a href="${data.href || 'javascript:;'}" target="${data.target || '_self'}"> <img src="${data.src}" alt="img" /> </a> `)
}

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)
}
複製代碼

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)
  }
}
複製代碼

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
複製代碼

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)
  }
}
複製代碼

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)
}
複製代碼

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;
    }
  }
}
複製代碼

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;
  }
}
複製代碼

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'
    }
  ]
})
複製代碼

使用心得

整體來講使用qnode來開發的話仍是比較方便的,文件拆分以及數據共享均可以作到,惟一有一點瑕疵的話,就是對於js執行的順序要慎重考慮。想想爲何render文件暴露出來的是函數,緣由就是由於此時數據還未儲存到qnode,所以經過函數來進行惰性加載,在合適的地方執行。

對於qnode,目前尚未錯誤提醒,調用方式不對的話沒有信息吐出,後續能夠考慮補上這個功能,畢竟其餘開發者用的話,可能並不熟悉API,調用姿式不對也是有可能發生的。

以上就是本文的所有內容了。

附:

相關文章
相關標籤/搜索