原生js系列之无限循环轮播组件

ag355o9pn7 7年前
   <h2>前情回顾</h2>    <p>在上一篇文章中,我们封装了一个DOM库(qnode),为了让大家直观地感受到其方便友好的自定义工厂模式,于是给大家带来了这篇文章。</p>    <p>没有看过上一篇文章的话,可以在这里找到: <a href="/misc/goto?guid=4959755792897938641" rel="nofollow,noindex">原生js系列之DOM工厂模式</a> 。</p>    <p>那么这篇文章,我们将基于上述的 qnode ,从头开始写一个无限循环轮播图的组件。</p>    <h2>思路讲解</h2>    <p>先看一张轮播布局图:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/b862d5cd672a25ab30da02675626f392.jpg"></p>    <p>滑动的时候,整个轮播容器整体前进或后退一格,通过css3过渡效果的设置,来达到滑动的效果。也许你会疑惑,头尾怎么会多出两张图呢?</p>    <p>其实无限循环轮播的核心就在于头尾多出的两张图,从图三再向后滑动,会滑到红色图一(我称之为占位图一),这个时候给用户的感觉就是无缝从最后一张滑动到第一张的,当他滑到占位图一时,我们再瞬间切换到粉色图一(即真正的图一),由于是瞬间变换,用户是感知不到的。同理,从图一滑到图三也一样。由此,周而复始,无穷无尽,给人的感觉是永远也不会到尽头,当然个中奥妙只有我们知道哈哈。</p>    <h2>目录结构</h2>    <pre>  <code class="language-javascript">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</code></pre>    <p>说明:mcss文件是通过 css-modules 来编译的,给class名称生成唯一标识,防止命名冲突。这里有我配置好的一套脚手架,觉得webpack配置麻烦的话,可以clone我这个项目来编译代码: <a href="/misc/goto?guid=4959755792981550485" rel="nofollow,noindex">webpack-build</a> 。</p>    <h2>代码编写</h2>    <p>index.js</p>    <pre>  <code class="language-javascript">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>    <p>render/index.js</p>    <pre>  <code class="language-javascript">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>    <p>render/list.js</p>    <pre>  <code class="language-javascript">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)  }</code></pre>    <p>render/indicator.js</p>    <pre>  <code class="language-javascript">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>    <p>qnode/index.js</p>    <pre>  <code class="language-javascript">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>    <p>qnode/store.js</p>    <pre>  <code class="language-javascript">// 静态数据可以放在这里  export const tdTime = 500</code></pre>    <p>qnode/method.js</p>    <pre>  <code class="language-javascript">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>    <p>touchSlide.js</p>    <pre>  <code class="language-javascript">// 截流  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>    <p>styles/wrap.mcss</p>    <pre>  <code class="language-javascript">.wrap {    position: relative;    overflow: hidden;    transform: translate3d(0, 0, 0);  }</code></pre>    <p>styles/list.mcss</p>    <pre>  <code class="language-javascript">.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>    <p>styles/indicator.mcss</p>    <pre>  <code class="language-javascript">.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>    <h2>README</h2>    <h2>参数</h2>    <ul>     <li>node: 要挂载的dom节点,必须</li>     <li>options: 如下(其中datas是必要的)</li>    </ul>    <pre>  <code class="language-javascript">{    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' // 点击锚点的跳转处理(是在当前页打开还是新建窗口)      }    ]  }</code></pre>    <h2>示例</h2>    <pre>  <code class="language-javascript">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>    <h2>使用心得</h2>    <p>总体来说使用 qnode 来开发的话还是比较方便的,文件拆分以及数据共享都可以做到,唯一有一点瑕疵的话,就是对于js执行的顺序要慎重考虑。想一想为什么render文件暴露出来的是函数,原因就是因为此时数据还未储存到 qnode ,因此通过函数来进行惰性加载,在合适的地方执行。</p>    <p>对于 qnode ,目前还没有错误提醒,调用方式不对的话没有信息吐出,后续可以考虑补上这个功能,毕竟其他开发者用的话,可能并不熟悉API,调用姿势不对也是有可能发生的。</p>    <p>以上就是本文的全部内容了。</p>    <p>附:</p>    <ul>     <li><a href="/misc/goto?guid=4959755793063410099" rel="nofollow,noindex">无限循环轮播图示例</a></li>     <li><a href="/misc/goto?guid=4959755793144326842" rel="nofollow,noindex">本文源码</a></li>    </ul>    <p> </p>    <p>来自:https://segmentfault.com/a/1190000012432451</p>    <p> </p>