开发一个UI框架项目[9]-Carousel

本文最后更新于:2020年10月31日 晚上

设计细节

  1. 轮播分为有缝轮播和无缝轮播,这里采用的是无缝轮播。
  2. 轮播功能主要基于vue过渡&动画实现,建议对这方面做一定的了解,很好用。
  3. 加上touchStarttouchEnd事件处理,增加对移动端的支持。
  4. 本次父子组件通信使用$parent$children
  5. 使用到了updated生命周期来处理轮播的选中更新等操作。

功能细节

  1. 无缝轮播设计的坑。这里实现无缝轮播的思路是将所有要轮播的元素都放在同一行的第一个位置,这就有两种考虑方案,一种是使用绝对定位,另一种就是父元素使用flex布局,显然是绝对定位方案比较好。然后使用了绝对定位方案之后又有坑:原先父级元素是没有设置高度由轮播元素撑起,但是使用绝对定位之后,子元素脱离了文档流就造成了父元素高度塌陷。那么首先想到使用JS去获取第一个元素的高度,但是如果是图片的话,图片是异步获取一般几百毫秒后才能拿到宽高,显然不行;还有一种是常见的让用户设置宽高,但如果是响应式的网站,让用户基于多种情况去设置是一件很不现实的事情。那最后的解决方案,还是要充分利用vue所提供的东西,在轮播动画离开(对应的类名leave-active)的时候,才让元素绝对定位,这样的话先前的元素就能撑起父元素的高度了,再配合vue提供的其它动画来实现轮播效果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <transition name="slide">
    <div class="yv-carousel-item__content" v-if="visible">
    <slot></slot>
    </div>
    </transition>

    <style scoped lang="scss">
    .slide-leave-active {
    position: absolute;
    left: 0; top: 0;
    width: 100%;
    height: 100%;
    }
    .slide-enter-active, .slide-leave-active {
    transition: all 0.5s;
    }
    .slide-enter {
    transform: translateX(100%);
    }
    .slide-leave-to {
    transform: translateX(-100%);
    }
    </style>
  2. 处理子组件选中状态。在父组件Carousel挂载后进行子组件选中状态的处理,以及处理后轮播跳转到前一个轮播的后退动画处理:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    <!-- Carousel.vue -->
    <script>
    export default {
    mounted() {
    this.updateChildren()
    if (this.autoPlay) {
    this.playAutomatically()
    }
    this.childrenLength = this.items.length
    },
    methods: {
    updateChildren() {
    let selected = this.getSelected()
    this.items.forEach((vm) => {
    let reverse = this.selectedIndex <= this.lastSelectedIndex
    if (this.timerId) {
    if (this.lastSelectedIndex === this.items.length - 1 && this.selectedIndex === 0) {
    reverse = false
    }
    if (this.lastSelectedIndex === 0 && this.selectedIndex === this.items.length - 1) {
    reverse = true
    }
    }
    vm.reverse = reverse
    this.$nextTick(() => {
    vm.selected = selected
    })
    })
    },
    getSelected() {
    let first = this.items[0]
    return this.selected || first.name
    }
    }
    }
    </script>

    <!-- CarouselItem.vue -->
    <style scoped lang="scss">
    // ...
    .slide-enter.reverse {
    transform: translateX(-100%);
    }
    .slide-leave-to.reverse {
    transform: translateX(100%);
    }
    </style>

    上面使用了$nextTick来解决更新不及时的问题,同时更新的时候也要通知,这里用到了update生命周期:

    1
    2
    3
    4
    5
    6
    7
    <script>
    export default {
    updated() {
    this.updateChildren()
    },
    }
    </script>
  3. 点击圆点、前后箭头显示对应的轮播。点击事件主要做了记录点击前轮播的索引,以及后面自动轮播时边界情况处理,如最后一个轮播时下一个是显示第一个轮播的处理等:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <script>
    export default {
    methods: {
    onClickPrev() {
    this.select(this.selectedIndex - 1)
    },
    onClickNext() {
    this.select(this.selectedIndex + 1)
    },
    select(newIndex) {
    this.lastSelectedIndex = this.selectedIndex
    if (newIndex === -1) {
    newIndex = this.names.length - 1
    }
    if (newIndex === this.names.length) {
    newIndex = 0
    }
    this.$emit('update:selected', this.names[newIndex])
    }
    }
    }
    </script>
  4. 自动轮播以及暂停处理。鼠标移入或移出时也要进行相应的处理,主要通过定时器控制:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    <script>
    export default {
    methods: {
    onMouseEnter() {
    this.pause()
    },
    onMouseLeave() {
    this.playAutomatically()
    },
    playAutomatically() {
    if (this.timerId) {
    return
    }
    let run = () => {
    let index = this.names.indexOf(this.getSelected())
    let newIndex = index + 1
    this.select(newIndex)
    this.timerId = setTimeout(run, this.autoPlayDelay)
    }
    this.timerId = setTimeout(run, this.autoPlayDelay)
    },
    pause() {
    window.clearTimeout(this.timerId)
    this.timerId = undefined
    },
    getSelected() {
    // ...
    },
    select(newIndex) {
    // ...
    }
    }
    }
    </script>
  5. 加上touchStarttouchEnd事件处理,增加对移动端的支持:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    <script>
    export default {
    methods: {
    onTouchStart(e) {
    this.pause()
    if (e.touches.length > 1) {
    return
    }
    this.startTouch = e.touches[0]
    },
    onTouchEnd(e) {
    let endTouch = e.changedTouches[0]
    let {clientX: x1, clientY: y1} = this.startTouch
    let {clientX: x2, clientY: y2} = endTouch
    let distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
    let deltaY = Math.abs(y2 - y1)
    let rate = distance / deltaY
    if (rate > 2) {
    if (x2 > x1) {
    this.select(this.selectedIndex - 1)
    } else {
    this.select(this.selectedIndex + 1)
    }
    }
    this.$nextTick(() => {
    this.playAutomatically()
    })
    },
    }
    }
    </script>

vuepress配置

docs/.vuepress/components文件夹下增加carousel-demovue文件,内容就是我们要展示的carousel示例,然后在docs/components文件夹下增加carousel的md文件,内容就是放置整个carousel组件说明。

具体内容请访问这里