前情回顾
在上一篇文章中, 我们封装了一个 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)
- }
- 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 会过滤调, 所以放心大胆地写
- ])
- }
- 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(`
- <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, 调用姿势不对也是有可能发生的
来源: http://www.open-open.com/lib/view/open1513325914286.html