前言

Youky ... 2021-11-27 其他 About 4 min

# 前言

最近在使用Swiper轮播图时,发现其官网给出的demo竟然运行不了。于是乎想到自己造个轮子试试。 最终的实现为封装的React组件。完整代码:项目源码 (opens new window)

# 功能分析

首先分析一下,一个轮播图组件需要哪些功能:

  • 基础轮播显示
  • 自定义宽高
  • 显示当前位置的分页器
  • 点击分页器跳转页面
  • 上/下一个按钮
  • 自动播放
  • 循环播放
  • 监听点击事件
  • 移动端支持

然后,对于上述功能一一进行实现。

# 结构拆分

首先对整个轮播图组件进行拆分,主要分为四部分:

  1. 最外层容器container
  2. 容纳图片的包装容器
  3. 底部的分页器
  4. 上/下一个按钮,位于两侧

为了结构的清晰,对于不同部分,拆分成了不同的JSX。 因此最后返回的JSX为:

<div className={style.swiper_container} ref={containerRef} style={{width,height}}>
            <div className={style.swiper_wrapper} style={wrapperStyle}>
                { swiperItems }
            </div>
            {slideButton && slideButtons}
            {pagination && paginationBar}
</div>
1
2
3
4
5
6
7

# 轮播功能

对于轮播功能的实现,我的思路是:

  • 最外层容器设置宽为width,并设置overflow:hidden
.swiper_container {
    position: relative;
    height: 100%;
    width: 100%;
    border-bottom: 1px solid rgb(231, 225, 225);
    overflow: hidden;
}
1
2
3
4
5
6
7
  • 每张图片宽度也为width,因此当前视口中只会显示一张图。根据传入的props.list循环生成图片列表
// 轮播图内容
const swiperItems = list.map((item, index) => (
    <div 
        key={index}
        className={style.swiper_item}
        onMouseDown={handleMouseDown}
        onMouseUp={handleMouseUp}
        onTouchStart={handleTouchStart}
        onTouchEnd={handleTouchEnd}
        onDoubleClick={() => handleDoubleClick(index)}
        style={{backgroundImage: `url(${item})`, width:containerWidth}}
    ></div>
))
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 定义当前显示图片的下标currentPage,每次滑动后更改currentPage
const [currentPage, setCurrentPage] = useState(0);
1
  • 图片包装容器宽度为N*100% ,N为图片数量。并设置flex布局,即所有图片在一横排排列。通过translateX移动wrapper容器,来控制当前显示的图片
const wrapperStyle = {
    width: `${list.length}00%`,
    transform: `translateX(-${containerWidth * currentPage}px)`
}
1
2
3
4
  • 监听滑动,在mouseDown时记录起始坐标,在mouseUp时记录终点坐标,依次判断是左滑还是右滑
// 切换页面函数
const handleSlide = (isNext) => {
    if(isNext) {
        if(currentPage < list.length-1){
            setCurrentPage(currentPage + 1);
        }
        else if(loop) {
            setCurrentPage(0);
        }
        
    }
    else{
        if(currentPage > 0){
            setCurrentPage(currentPage - 1);
        }
        else if(loop){
            setCurrentPage(list.length-1);
        }
    } 
}
// 判断滑动情况
let currentX = null;
const handleMouseDown = e => {
    currentX = e.clientX
}
const handleMouseUp = e => {
    const end = e.clientX;
    if( end - currentX > 20) {
        handleSlide(false);
    }
    else if( currentX - end > 20) {
        handleSlide(true);
    }
}
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
  • 对于移动端,则监听touchStart和touchEnd事件
// 移动端监听滑动
const handleTouchStart = e => {
    currentX = e.changedTouches[0].clientX;
}
const handleTouchEnd = e => {
    const end = e.changedTouches[0].clientX;
    if( end - currentX > 20) {
        handleSlide(false);
    }
    else if( currentX - end > 20) {
        handleSlide(true);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

至此,基本的滑动效果已经完成了。

# 分页器

# 需求分析

  • 固定显示在底部
  • 通过颜色来区分当前显示的图片的下标
  • 点击分页器会翻页到对应的图片

# 实现

  • 首先,定义分页器的JSX
const paginationBar = (
    <div className={style.swiper_pagination}>
        {list.map((item, index) => 
            <div 
                key={index}
                className={paginationItemClass(index)}
                onClick={() => handlePaginationClick(index)}
            ></div>
        )}
    </div>
)
1
2
3
4
5
6
7
8
9
10
11
  • 从上一步可以看到,分页器每个元素的class是通过一个函数来返回的,目的是区分是否是当前显示的图片
const paginationItemClass = (index) => {
        const isActive = index === currentPage ?  ' ' + style.pagination_active : '';
        return style.pagination_item + isActive;
}
1
2
3
4
  • 然后定义分页器样式
    • 通过translate将分页器容器固定到底部
    • 设置不同的背景色来体现是否当前图片
.swiper_pagination {
    width: 100%;
    height: 40px;
    transform: translate(0,-40px);
    display: flex;
    justify-content: center;
    align-items: center;
}
.pagination_item {
    width: 8px;
    height: 8px;
    border-radius: 4px;
    background-color: #eee;
    margin: 0 10px;
    cursor: pointer;
}
.pagination_item:hover {
    background-color: #999;
}
.pagination_active {
    background-color: #999;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • 定义点击函数,来完成跳转翻页的功能,其实也就是修改currentPage
const handlePaginationClick = index => {
    console.log(`${index} clicked`);
    setCurrentPage(index);
}
1
2
3
4
  • 通过props.pagination判断是否要显示
{props.pagination && paginationBar}
1

# 上/下一个按钮

# 需求分析

  • 上下居中,分别靠左、右侧
  • 点击后进行翻页
  • 为防止遮挡图片,默认透明度为0.5,悬浮是恢复1

# 实现

  • 通过absolute定位,将按钮固定到垂直居中的位置。再通过left和right来分别置于左右
.slideButton {
    height: 40px;
    width: 40px;
    border-radius: 20px;
    position: absolute;
    background-color: #eee;
    top: 50%;
    cursor: pointer;
    display: flex;
    justify-content: center;
    align-items: center;
    opacity: 0.5;
}
.slideButton:hover{
    opacity: 1;
}
.arrow_right {
    right: 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 定义按钮的JSX。翻页使用的回调函数仍是前面定义的handleSlide:
// 上/下一个按钮
const slideButtons = (
    <>
        <div 
            className={style.slideButton} 
            onClick={() => handleSlide(false)}
        >{'<'}</div>
        <div 
            className={style.slideButton + ' ' + style.arrow_right}
            onClick={() => handleSlide(true)}
        >{'>'}</div>
    </>
)
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 通过props.slideButton 判断是否显示
{props.slideButton && slideButtons}
1

# 循环播放

# 需求分析

  • 当位于最后一张图时,触发向后翻页会跳回第一张
  • 当位于第一张图时,触发向前翻页会跳回最后一张

# 实现

  • props接收loop属性,表示是否允许循环播放,默认为true
  • 在触发向翻页时,根据loop做特殊处理
// 切换页面函数
const handleSlide = (isNext) => {
    if(isNext) {
        if(currentPage < list.length-1){
            setCurrentPage(currentPage + 1);
        }
        else if(loop) { // 特殊处理
            setCurrentPage(0);
        }
    }
    else{
        if(currentPage > 0){
            setCurrentPage(currentPage - 1);
        }
        else if(loop){ // 特殊处理
            setCurrentPage(list.length-1);
        }
        
    } 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 自动播放

# 需求分析

  • autoPlay属性,表示是否进行自动播放,默认为false
  • intrval属性,表示自动播放的间隔,默认为3000ms

# 实现

  • useEffect Hook中设置定时器,并在返回的清除函数中清理定时器
// 设置自动播放
useEffect(() => {
    if(autoPlay){
        const auto = setInterval(() => {
            handleSlide(true);
        }, interval)
        return () => {
            clearInterval(auto);
        }
    }
})
1
2
3
4
5
6
7
8
9
10
11
Last update: November 27, 2021 21:16
Contributors: youky7