首页 > 其他分享 >React实现一个简易版Swiper

React实现一个简易版Swiper

时间:2022-10-27 15:11:22浏览次数:86  
标签:activeIndex const string height React width 简易版 styled Swiper

背景

最近在公司内部进行一个引导配置系统的开发中,需要实现一个多图轮播的功能。到这时很多同学会说了,“那你直接用swiper不就好了吗?”。但其实是,因为所有引导的展示都是作为npm依赖的形式来进行插入的,所以我们想要做的就是:尽量减少外部依赖以及包的体积。所以,我们开始了手撸简易版swiper之路。

功能诉求

首先,由于我们所有的内容都是支持配置的,所以首先需要支持停留时间(delay)的可配置;由于不想让用户觉得可配置的内容太多,所以我们决定当停留时间(delay)大于0时,默认开启autoplay

其次,在常规的自动轮播外,还需要满足设计同学对于分页器(Pagination)的要求,也就是当前的展示内容对应的气泡(bullet)需要是一个进度条的样式,有一个渐进式的动画效果

最后,由于滑动效果实现起来太麻烦,所以就不做了,其他的基本都是swiper的常规功能了。

由此,整体我们要开发的功能就基本确定,后面就是开始逐步进行实现。

效果展示

2022-10-25 11.21.56.gif

整体思路

1、入参与变量定义

由于需要用户自定义配置整体需要展示的图片,并且支持自定义整体的宽高轮播时间(delay);同样,我们也应该支持用户自定义轮播的方向(direction)

综上我们可以定义如下的入参:

{
  direction?: 'horizontal' | 'vertical';
  speed?: number;
  width: string;
  height: string;
  urls: string[];
}

而在整个swiper运行的过程中我们同样是需要一些参数来帮助我们实现不同的基础功能,比如

2、dom结构

从dom结构上来说,swiper的核心逻辑就是,拥有单一的可视区,然后让所有的内容都在可视区移动、替换,以此来达到轮播的效果实现。

Web 1280 1.png

那么如何来实现上的效果呢?这里简单梳理一下html的实现:

// 可见区域容器
<div id="swiper">
  // 轮播的真实内容区,也就是实际可以移动的区域
  <div className="swiper-container" id="swiper-container">
    // 内部节点的渲染
    {urls.map((f: string, index: number) => (
      <div className="slide-node">
        <img src={f} alt="" />
      </div>
    ))}
  </div>
</div>

到这里一个简陋的dom结构就出现了。接下来就需要我们为他们补充一些样式

3、样式(style)

为了减少打包时处理的文件类型,并且以尽可能简单的进行样式开发为目标。所以我们在开发过程中选择了使用styled-components来进行样式的编写,具体使用方式可参考styled-components: Documentation

首先,我们先来梳理一下对于最外层样式的要求。最基本的肯定是要支持参数配置宽高以及仅在当前区域内可查看

而真正的代码实现其实很简单:

import styled from "styled-components";
import React, { FC } from "react";

const Swiper = styled.div`
  overflow: hidden;
  position: relative;
`;

const Swiper: FC<
  {
    direction?: 'horizontal' | 'vertical';
    speed?: number;
    width: string;
    height: string;
    urls: string[];
  }
> = ({
  direction = "horizontal",
  speed = 3,
  width = "",
  height = "",
  urls = []
}) => {
    return (<Swiper style={{ width, height }}></Swiper>);
}

export default Swiper;

其次,我们来进行滚动区的样式的开发。

但是这里我们要明确不同的是,我们除了单独的展示样式的开发外,我们还要主要对于过场动画效果的实现。

import styled from "styled-components";
import React, { FC } from "react";

const Swiper = styled.div`
  overflow: hidden;
  position: relative;
`;

const SwiperContainer = styled.div`
  position: relative;
  width: auto;
  display: flex;
  align-item: center;
  justify-content: flex-start;
  transition: all 0.3s ease;
  -webkit-transition: all 0.3s ease;
  -moz-transition: all 0.3s ease;
  -o-transition: all 0.3s ease;
`;

const Swiper: FC<
  {
    direction?: 'horizontal' | 'vertical';
    speed?: number;
    width: string;
    height: string;
    urls: string[];
  }
> = ({
  direction = "horizontal",
  speed = 3,
  width = "",
  height = "",
  urls = []
}) => {
  return (<Swiper style={{ width, height }}>
    <SwiperContainer
      id="swiper-container"
      style={{
        height,
        // 根据轮播方向参数,调整flex布局方向
        flexDirection: direction === "horizontal" ? "row" : "column",
      }}
    >
    </SwiperContainer>
  </Swiper>);
}

export default Swiper;

在这里,我们给了他默认的宽度为auto,来实现整体宽度自适应。而使用transition让后续的图片轮换可以有动画效果

最后,我们只需要将图片循环渲染在列表中即可。

import styled from "styled-components";
import React, { FC } from "react";

const Swiper = styled.div`
  overflow: hidden;
  position: relative;
`;

const SwiperContainer = styled.div`
  position: relative;
  width: auto;
  display: flex;
  align-item: center;
  justify-content: flex-start;
  transition: all 0.3s ease;
  -webkit-transition: all 0.3s ease;
  -moz-transition: all 0.3s ease;
  -o-transition: all 0.3s ease;
`;

const SwiperSlide = styled.div`
  display: flex;
  align-item: center;
  justify-content: center;
  flex-shrink: 0;
`;

const Swiper: FC<
  {
    direction?: 'horizontal' | 'vertical';
    speed?: number;
    width: string;
    height: string;
    urls: string[];
  }
> = ({
  direction = "horizontal",
  speed = 3,
  width = "",
  height = "",
  urls = []
}) => {
  return (<Swiper style={{ width, height }}>
    <SwiperContainer
      id="swiper-container"
      style={{
        height,
        // 根据轮播方向参数,调整flex布局方向
        flexDirection: direction === "horizontal" ? "row" : "column",
      }}
    >
     {urls.map((f: string, index: number) => (
        <SwiperSlide style={{ ...styles }}>
          <img src={f} style={{ ...styles }} alt="" />
        </SwiperSlide>
      ))}
    </SwiperContainer>
  </Swiper>);
}

export default Swiper;

至此为止,我们整体的dom结构样式就编写完成了,后面要做的就是如何让他们按照我们想要的那样,动起来

4、动画实现

既然说到了轮播动画的实现,那么我们最先想到的也是最方便的方式,肯定是我们最熟悉的setInterval,那么整体的实现思路是什么样的呢?

先思考一下我们想要实现的功能:

1、按照预设的参数实现定时的图片切换功能;

2、如果没有预设delay的话,则不自动轮播;

3、每次轮播的距离,是由用户配置的图片宽高决定;

4、轮播至最后一张后,停止轮播。

首先,为了保证元素可以正常的移动,我们在元素身上添加refid便于获取正确的dom元素。

import React, { FC, useRef } from "react";

const swiperContainerRef = useRef<HTMLDivElement>(null);
...
<SwiperContainer
  id="swiper-container"
  ref={swiperContainerRef}
  style={{
    height,
    // 根据轮播方向参数,调整flex布局方向
    flexDirection: direction === "horizontal" ? "row" : "column",
  }}
>
...
</SwiperContainer>
...

其次,我们需要定义activeIndex这个state,用来标记当前展示的节点;以及用isDone标记是否所有图片都已轮播完成(所以反馈参数)。

import React, { FC, useState } from "react";

const [activeIndex, setActiveIndex] = useState<number>(0);
const [isDone, setDone] = useState<boolean>(false);

然后,我们还需要进行timer接收参数的定义,这里我们可以选择使用useRef来进行定义。

import React, { FC, useRef } from "react";

const timer = useRef<any>(null);

在上面的一切都准备就绪后,我们可以进行封装启动方法的封装

  // 使用定时器,定时进行activeIndex的替换
  const startPlaySwiper = () => {
    if (speed <= 0) return;
    timer.current = setInterval(() => {
      setActiveIndex((preValue) => preValue + 1);
    }, speed * 1000);
  };

但是到此为止,我们只是进行了activeIndex的自增,并没有真正的让页面上的元素动起来,为了实现真正的动画效果,我们使用useEffect对于activeIndex进行监听。

import React, { FC, useEffect, useRef, useState } from "react";

useEffect(() => {
  const swiper = document.querySelector("#swiper-container") as any;
  // 根据用户传入的轮播方向,决定是在bottom上变化还是right变化
  if (direction === "vertical") {
    // 兼容用户输入百分比的模式
    swiper.style.bottom = (height as string)?.includes("%")
      ? `${activeIndex * +(height as string)?.replace("%", "")}vh`
      : `${activeIndex * +height}px`;
  } else {
    swiper.style.right = (width as string)?.includes("%")
      ? `${activeIndex * +(width as string)?.replace("%", "")}vw`
      : `${activeIndex * +width}px`;
  // 判断如果到达最后一张,停止自动轮播
  if (activeIndex >= urls.length - 1) {
    clearInterval(timer?.current);
    timer.current = null;
    setDone(true);
  }
}, [activeIndex, urls]);

截止到这里,其实简易的自动轮播就完成了,但是其实很多同学也会有疑问❓,是不是还缺少分页器(Pagination)

5、分页器(Pagination)

分页器的原理其实很简单,我们可以分成两个步骤来看。

1、渲染与图片相同个数的节点;

2、根据activeIndex动态改变分页样式。

import React, { FC } from "react";
import styled from "styled-components";

const SwiperSlideBar = styled.div`
  margin-top: 16px;
  width: 100%;
  height: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
`;

const SwiperSlideBarItem: any = styled.div`
  cursor: pointer;
  width: ${(props: any) => (props.isActive ? "26px" : "16px")};
  height: 4px;
  background: #e6e6e6;
  margin-right: 6px;
`;

const SlideBarInner: any = styled.div`
  width: 100%;
  height: 100%;
  background: #0075ff;
  animation: ${innerFrame} ${(props: any) => `${props.speed}s`} ease;
`;

{urls?.length > 1 ? (
  <SwiperSlideBar>
    {urls?.map((f: string, index: number) => (
      <SwiperSlideBarItem
        onClick={() => slideToOne(index)}
        isActive={index === activeIndex}
      >
        {index === activeIndex ? <SlideBarInner speed={speed} /> : null}
      </SwiperSlideBarItem>
    ))}
  </SwiperSlideBar>
) : null}

细心的同学可能看到我在这里为什么还有一个SlideBarInner元素,其实是在这里实现了一个当前所在分页停留时间进度条展示的功能,感兴趣的同学可以自己看一下,我这里就不在赘述了。

6、整体实现代码

最后,我们可以看到完整的Swiper代码如下:

import React, { FC, useEffect, useRef, useState } from "react";
import styled, { keyframes } from "styled-components";

const innerFrame = keyframes`
  from {
    width: 0%;
  }
  to {
    width: 100%;
  }
`;

const Swiper = styled.div`
  overflow: hidden;
  position: relative;
`;

const SwiperNextTip = styled.div`
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  right: 24px;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: #ffffff70;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
  opacity: 0.7;
  user-select: none;
  :hover {
    opacity: 1;
    background: #ffffff80;
  }
`;

const SwiperPrevTip = (styled as any)(SwiperNextTip)`
  left: 24px;
`;

const SwiperContainer = styled.div`
  position: relative;
  display: flex;
  align-item: center;
  justify-content: flex-start;
  transition: all 0.3s ease;
  -webkit-transition: all 0.3s ease;
  -moz-transition: all 0.3s ease;
  -o-transition: all 0.3s ease;
`;

const SwiperSlide = styled.div`
  display: flex;
  align-item: center;
  justify-content: center;
  flex-shrink: 0;
`;

const SwiperSlideBar = styled.div`
  margin-top: 16px;
  width: 100%;
  height: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
`;

const SwiperSlideBarItem: any = styled.div`
  cursor: pointer;
  width: ${(props: any) => (props.isActive ? "26px" : "16px")};
  height: 4px;
  background: #e6e6e6;
  margin-right: 6px;
`;

const SlideBarInner: any = styled.div`
  width: 100%;
  height: 100%;
  background: #0075ff;
  animation: ${innerFrame} ${(props: any) => `${props.speed}s`} ease;
`;

const Swiper: FC<
  {
    direction?: 'horizontal' | 'vertical';
    speed?: number;
    width: string;
    height: string;
    urls: string[];
  }
> = ({
  direction = "horizontal",
  speed = 3,
  width = "",
  height = "",
  urls = []
}) => {
  const [activeIndex, setActiveIndex] = useState<number>(0);
  const [isDone, setDone] = useState<boolean>(false);
  const [swiperStyle, setSwiperStyle] = useState<{
    width: string;
    height: string;
  }>({
    width: (width as string)?.replace("%", "vw"),
    height: (height as string)?.replace("%", "vh"),
  } as any);
  
  const timer = useRef<any>(null);
  const swiperContainerRef = useRef<HTMLDivElement>(null);

  const styles = {
    width: isNaN(+swiperStyle.width)
      ? swiperStyle!.width
      : `${swiperStyle!.width}px`,
    height: isNaN(+swiperStyle.height)
      ? swiperStyle.height
      : `${swiperStyle.height}px`,
  };

  const startPlaySwiper = () => {
    if (speed <= 0) return;
    timer.current = setInterval(() => {
      setActiveIndex((preValue) => preValue + 1);
    }, speed * 1000);
  };

  const slideToOne = (index: number) => {
    if (index === activeIndex) return;
    setActiveIndex(index);
    clearInterval(timer?.current);
    startPlaySwiper();
  };

  useEffect(() => {
    if (swiperContainerRef?.current) {
      startPlaySwiper();
    }
    return () => {
      clearInterval(timer?.current);
      timer.current = null;
    };
  }, [swiperContainerRef?.current]);

  useEffect(() => {
    const swiper = document.querySelector("#swiper-container") as any;
    if (direction === "vertical") {
      swiper.style.bottom = (height as string)?.includes("%")
        ? `${activeIndex * +(height as string)?.replace("%", "")}vh`
        : `${activeIndex * +height}px`;
    } else {
      swiper.style.right = (width as string)?.includes("%")
        ? `${activeIndex * +(width as string)?.replace("%", "")}vw`
        : `${activeIndex * +width}px`;
    }

    if (activeIndex >= urls.length - 1) {
      clearInterval(timer?.current);
      timer.current = null;
      setDone(true);
    }
  }, [activeIndex, urls]);

  return (<>
      <Swiper style={{ width, height }}>
        <SwiperContainer
          id="swiper-container"
          ref={swiperContainerRef}
          style={{
            height,
            // 根据轮播方向参数,调整flex布局方向
            flexDirection: direction === "horizontal" ? "row" : "column",
          }}
        >
         {urls.map((f: string, index: number) => (
            <SwiperSlide style={{ ...styles }}>
              <img src={f} style={{ ...styles }} alt="" />
            </SwiperSlide>
          ))}
        </SwiperContainer>
      </Swiper>

      // Pagination分页器
      {urls?.length > 1 ? (
        <SwiperSlideBar>
          {urls?.map((f: string, index: number) => (
            <SwiperSlideBarItem
              onClick={() => slideToOne(index)}
              isActive={index === activeIndex}
            >
              {index === activeIndex ? <SlideBarInner speed={speed} /> : null}
            </SwiperSlideBarItem>
          ))}
        </SwiperSlideBar>
      ) : null}
  </>);
}

export default Swiper;

总结

其实很多时候,我们都会觉得对于一个需求(功能)的开发无从下手。可是如果我们耐下心来,将我们要实现的目标进行抽丝剥茧样的拆解,让我们从最最简单的部分开始进行实现和设计,然后逐步自我迭代,将功能细化、优化、深化。那么最后的效果可能会给你自己一个惊喜哦。

妙言至径,大道至简。

标签:activeIndex,const,string,height,React,width,简易版,styled,Swiper
From: https://www.cnblogs.com/marui01/p/16832313.html

相关文章

  • React高级特性之Render Props
    renderprop是一个技术概念。它指的是使用值为function类型的prop来实现Reactcomponent之间的代码共享。如果一个组件有一个render属性,并且这个render属性的值为一个返......
  • React高级特性之Context
    Context提供了一种不需要手动地通过props来层层传递的方式来传递数据。正文在典型的React应用中,数据是通过props,自上而下地传递给子组件的。但是对于被大量组件使用的......
  • 项目实战:在线报价采购系统(React +SpreadJS+Echarts)
    小伙伴们对采购系统肯定不陌生,小到出差路费、部门物资采购;大到生产计划、原料成本预估都会涉及到该系统。管理人员可以通过采购系统减少管理成本,说是管理利器毫不过分,对于......
  • vue跨域简易版
    当后端接口没有跨域功能时且无法去修改(比如调用第三方接口),就需要前端自己实现跨域功能。vue-cli项目1.在根目录的vue.config.js中配置哪些请求需要转发到没有跨域功能的接......
  • 前端一面高频react面试题(持续更新中)
    如何避免组件的重新渲染?React中最常见的问题之一是组件不必要地重新渲染。React提供了两个方法,在这些情况下非常有用:React.memo():这可以防止不必要地重新渲染函数组......
  • 老生常谈React的diff算法原理-面试版
    第一次发文章notonly(虽然)版式可能有点烂butalso(但是)最后赋有手稿研究finally看完他你有收获diff算法:对于update的组件,他会将当前组件与该组件在上次更新是对应的......
  • ReactJS单页面应用之项目搭建
    初衷因接手的项目前端采用reactjs+antd,为把控项目中的各个细节,所以想做一些整理,以免后期遗忘。创建及启动项目#全局安装create-react-app#如果曾经安装过,可先移除:n......
  • react实战笔记134:redux简介
      ......
  • react实战笔记136:HelloRedux的实现
     第一步创建函数 第二步设置初始值 第三布直接赋值......
  • react实战笔记137:解析redux的HelloWorld
    增加加的数据不一定直接通过参数传递 传入对象的一个数据格式 对象增加更多属性 redux的最初形态......