原生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>