Featured image of post React和CSS实现自动轮播组件

React和CSS实现自动轮播组件

手写轮播图组件

前言

最近在尝试使用React重构网站,尽量减少库的使用,只使用React和CSS手写一个简单实用的轮播图组件。

先看效果预览:

预览图

实现

Setup

1
2
3
4
npx create-react-app slider-app
cd slider-app
npm install react-icons
npm start

CSS样式

设置容器和轮播图的样式

 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
.sectionCenter {
  margin: 0 auto;
  margin-top: 4rem;
  width: 80vw;
  /* have to have a height */
  height: 450px;
  max-width: 800px;
  text-align: center;
  position: relative;
  display: flex;
  /* hiding overflow content */
  overflow: hidden;
}
.article {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  /* Each slide, hidden by default */
  opacity: 0;
  transition: var(--transition);
}
.article.activeSlide {
  opacity: 1;
  transform: translateX(0);
}
/* Styles for the previous slide, positioning it to the left */
.article.lastSlide {
  transform: translateX(-100%);
}
.article.nextSlide {
  /* Move slide out of view to the right */
  transform: translateX(100%);
}

Slider组件

初始化状态
1
2
3
4
5
import { useState, useEffect } from "react";

const Slider = ({ data }) => {
    const [people, setPeople] = useState(data);
    const [index, setIndex] = useState(0);
处理索引边界值
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 确保轮播图继续循环 当超过边界时
useEffect(() => {
  const lastIndex = people.length - 1;
  if (index < 0) {
    setIndex(lastIndex);
  }
  if (index > lastIndex) {
    setIndex(0);
  }
}, [index, people]);
自动轮播效果
1
2
3
4
5
6
useEffect(() => {
  let slider = setInterval(() => {
    setIndex(index + 1);
  }, 3000);
  return () => clearInterval(slider); // index改变时要清除定时器
}, [index]);
页面渲染
 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
  return (
    <div className={styles.main}>
      <section className={styles.section}>
        <div className={styles.sectionCenter}>
          {people.map((person, personIndex) => {
            const { id, image, name, title, quote } = person;
            let position = "nextSlide"; // 默认所有轮播图叠加在右边
            // 根据当前索引去定位左边和中间active的 轮播图
            if (personIndex === index) {
              position = "activeSlide";
            }
            if (
              personIndex === index - 1 ||
              (index === 0 && personIndex === people.length - 1)
            ) {
              // 条件1:把一个slide挪到左边
              // 条件2: index为0时 把数组最后一个挪到最左边
              position = "lastSlide";
            }
            return (
              <article
                className={`${styles.article} ${styles[position]}`}
                key={id}
              >
                <img src={image} alt={name} className={styles.personImg} />
                <h4>{name}</h4>
                <p className={styles.title}>{title}</p>
                <p className={styles.text}>{quote}</p>
                <FaQuoteRight className={styles.icon} />
              </article>
            );
          })}
          <button className={styles.prev} onClick={() => setIndex(index - 1)}>
            <FiChevronLeft />
          </button>
          <button className={styles.next} onClick={() => setIndex(index + 1)}>
            <FiChevronRight />
          </button>
        </div>
      </section>
    </div>
  );

整体代码

 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import React, { useState, useEffect } from "react";
import { FiChevronRight, FiChevronLeft } from "react-icons/fi";
import { FaQuoteRight } from "react-icons/fa";
import data from "./data";

import styles from ".index.module.css";

const Slider = () => {
  const [people, setPeople] = useState(data);
  const [index, setIndex] = useState(0);

  useEffect(() => {
    const lastIndex = people.length - 1;
    if (index < 0) {
      setIndex(lastIndex);
    }
    if (index > lastIndex) {
      setIndex(0);
    }
  }, [index, people]);

  useEffect(() => {
    let slider = setInterval(() => {
      setIndex(index + 1);
    }, 3000);
    return () => clearInterval(slider);
  }, [index]);
  return (
    <div className={styles.main}>
      <section className={styles.section}>
        <div className={styles.title}>
          <h2>
            <span>/</span>reviews
          </h2>
        </div>
        <div className={styles.sectionCenter}>
          {people.map((person, personIndex) => {
            const { id, image, name, title, quote } = person;
            let position = "nextSlide"; 
            if (personIndex === index) {
              position = "activeSlide";
            }
            if (
              personIndex === index - 1 ||
              (index === 0 && personIndex === people.length - 1)
            ) {
              // 条件1:把一个slide挪到左边
              // 条件2: index为0时 把数组最后一个挪到最左边
              position = "lastSlide";
            }
            return (
              <article
                className={`${styles.article} ${styles[position]}`}
                key={id}
              >
                <img src={image} alt={name} className={styles.personImg} />
                <h4>{name}</h4>
                <p className={styles.title}>{title}</p>
                <p className={styles.text}>{quote}</p>
                <FaQuoteRight className={styles.icon} />
              </article>
            );
          })}
          <button className={styles.prev} onClick={() => setIndex(index - 1)}>
            <FiChevronLeft />
          </button>
          <button className={styles.next} onClick={() => setIndex(index + 1)}>
            <FiChevronRight />
          </button>
        </div>
      </section>
    </div>
  );
};

export default Slider;
  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
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/* section */
.section {
  width: 90vw;
  margin: 5rem auto;
  max-width: var(--max-width);
}

@media screen and (min-width: 992px) {
  .section {
    width: 95vw;
  }
}

.main {
  background: var(--clr-grey-10);
}
/*
=============== 
Slider
===============
*/
.title {
  text-align: center;
  margin-bottom: 2rem;
}
.title h2 {
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 500;
}
.title span {
  font-size: 0.85em;
  color: var(--clr-primary-5);
  margin-right: 1rem;
  font-weight: 700;
}
.sectionCenter {
  margin: 0 auto;
  margin-top: 4rem;
  width: 80vw;
  /* have to have a height */
  height: 450px;
  max-width: 800px;
  text-align: center;
  position: relative;
  display: flex;
  /* hiding overflow content */
  overflow: hidden;
}
.personImg {
  border-radius: 50%;
  margin-bottom: 1rem;
  width: 150px;
  height: 150px;
  object-fit: cover;
  border: 4px solid var(--clr-grey-8);
  box-shadow: var(--dark-shadow);
}
.article h4 {
  text-transform: uppercase;
  color: var(--clr-primary-5);
  margin-bottom: 0.25rem;
}
.title {
  text-transform: capitalize;
  margin-bottom: 0.75rem;
  color: var(--clr-grey-3);
}
.text {
  max-width: 35em;
  margin: 0 auto;
  margin-top: 2rem;
  line-height: 2;
  color: var(--clr-grey-5);
}
.icon {
  font-size: 3rem;
  margin-top: 1rem;
  color: var(--clr-primary-5);
}
.prev,
.next {
  position: absolute;
  top: 200px;
  transform: translateY(-50%);
  background: var(--clr-grey-5);
  color: var(--clr-white);
  width: 1.25rem;
  height: 1.25rem;
  display: grid;
  place-items: center;
  border-color: transparent;
  font-size: 1rem;
  border-radius: var(--radius);
  cursor: pointer;
  transition: var(--transition);
}
.prev:hover,
.next:hover {
  background: var(--clr-primary-5);
}
.prev {
  left: 0;
}
.next {
  right: 0;
}
@media (min-width: 800px) {
  .text {
    max-width: 45em;
  }
  .prev,
  .next {
    width: 2rem;
    height: 2rem;
    font-size: 1.5rem;
  }
}
.article {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  /* Each slide, hidden by default */
  opacity: 0;
  transition: var(--transition);
}
.article.activeSlide {
  opacity: 1;
  transform: translateX(0);
}
/* Styles for the previous slide, positioning it to the left */
.article.lastSlide {
  transform: translateX(-100%);
}
.article.nextSlide {
  /* Move slide out of view to the right */
  transform: translateX(100%);
}

思路总结

实现思路是

  • 设置一个sectionCenter,在窗口内展示当前的幻灯片,通过overflow:hidden隐藏超出窗口的部分
  • 设置article样式,将每一张幻灯片都定位在页面中间并隐藏,这时所有的幻灯片是重叠在一起的
  • 在JS中,初始化时将slider item堆积放在右边(类似后台)
  • 根据当前索引和轮播图数组索引的关系,确定activeSlide, lastSlide该绑定到数组中哪个元素上

这个方法能够一次性加载所有图片文本内容,并通过CSS动画实现幻灯片移动,适合于展示用户推荐内容或者个人网站上的项目。优点在于幻灯片可实现无缝轮播,并且在有限的显示空间内展示多张图像。缺点是隐藏内容不利于SEO,无法被搜索引擎有效抓取。

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy