资讯专栏INFORMATION COLUMN

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

jimhs / 2803人阅读

摘要:没有看过上一篇文章的话,可以在这里找到原生系列之工厂模式。那么这篇文章,我们将基于上述的,从头开始写一个无限循环轮播图的组件。附无限循环轮播图示例本文源码

前情回顾

在上一篇文章中,我们封装了一个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(`
    
      
    
  `)
}

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,调用姿势不对也是有可能发生的。

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

附:

无限循环轮播图示例

本文源码

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/92137.html

相关文章

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

    摘要:没有看过上一篇文章的话,可以在这里找到原生系列之工厂模式。那么这篇文章,我们将基于上述的,从头开始写一个无限循环轮播图的组件。附无限循环轮播图示例本文源码 前情回顾 在上一篇文章中,我们封装了一个DOM库(qnode),为了让大家直观地感受到其方便友好的自定义工厂模式,于是给大家带来了这篇文章。 没有看过上一篇文章的话,可以在这里找到:原生js系列之DOM工厂模式。 那么这篇文章,我们...

    ad6623 评论0 收藏0
  • 原生js系列无限循环轮播组件

    摘要:没有看过上一篇文章的话,可以在这里找到原生系列之工厂模式。那么这篇文章,我们将基于上述的,从头开始写一个无限循环轮播图的组件。附无限循环轮播图示例本文源码 前情回顾 在上一篇文章中,我们封装了一个DOM库(qnode),为了让大家直观地感受到其方便友好的自定义工厂模式,于是给大家带来了这篇文章。 没有看过上一篇文章的话,可以在这里找到:原生js系列之DOM工厂模式。 那么这篇文章,我们...

    刘玉平 评论0 收藏0
  • JS -- 记一种用原生JS 实现轮播图的方法(非无限循环不自动切换)

    摘要:实现一个非无限循环不自动切换的轮播图只需要几张图片和两个按钮简化部分两个按钮,几张图片假如有四张图右侧按钮左侧按钮部分动态添加删除的属性部分已是最后一张图这是第一张图 实现一个非无限循环不自动切换的轮播图只需要几张图片和两个按钮(简化) HTML部分 两个按钮,几张图片(假如有四张图) 右侧按钮 左侧按钮 CSS部分 动态...

    hidogs 评论0 收藏0
  • 50行代码搞定无限滑动幻灯片

    摘要:对,滑动式幻灯片的关键就在于隐藏。在条件里我们添加一个事件相当于滑动后的回掉,依赖这个事件在幻灯片滑动执行完毕后立即执行里面的闪回操作。通过添加事件监听滑动是否结束从而迅速闪回,达到貌似无限滑动的效果。 slider轮播组件,在各类网站上出现及其频繁,有渐隐式的,滑动式的等等一系列。栗子在这: 但我当初学习写轮播时却被各种入门教程搞得焦头烂额。不是代码太复杂,就是封装太严重,初学者很难...

    yimo 评论0 收藏0
  • 原生js系列DOM工厂模式

    摘要:于是在不断的摸索和思考中,想出了工厂模式这个概念,咱们可以这么理解工厂就是取快递的地方,不管是从哪里发来的货品,统一送到这里,然后再由特定的人群来取。 写在前面 如今,在项目中使用React、Vue等框架作为技术栈已成为一种常态,在享受带来便利性的同时,也许我们渐渐地遗忘原生js的写法。 现在,是时候回归本源,响应原始的召唤了。本文将一步一步带领大家封装一套属于自己的DOM操作库,我将...

    sunny5541 评论0 收藏0

发表评论

0条评论

jimhs

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<