背景

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

功能诉求

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

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

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

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

效果展示

整体思路

1、入参与变量定义

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

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

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

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

2、dom结构

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

那么如何来实现上的效果呢?这里简单梳理一下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;

总结

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

妙言至径,大道至简。

React实现一个简易版Swiper的更多相关文章

  1. 使用 js 和 Beacon API 实现一个简易版的前端埋点监控 npm 包

    使用 js 和 Beacon API 实现一个简易版的前端埋点监控 npm 包 前端监控,埋点,数据收集,性能监控 Beacon API https://caniuse.com/beacon 优点,请 ...

  2. .NET Core的文件系统[5]:扩展文件系统构建一个简易版“云盘”

    FileProvider构建了一个抽象文件系统,作为它的两个具体实现,PhysicalFileProvider和EmbeddedFileProvider则分别为我们构建了一个物理文件系统和程序集内嵌文 ...

  3. 依赖注入[5]: 创建一个简易版的DI框架[下篇]

    为了让读者朋友们能够对.NET Core DI框架的实现原理具有一个深刻而认识,我们采用与之类似的设计构架了一个名为Cat的DI框架.在<依赖注入[4]: 创建一个简易版的DI框架[上篇]> ...

  4. 依赖注入[4]: 创建一个简易版的DI框架[上篇]

    本系列文章旨在剖析.NET Core的依赖注入框架的实现原理,到目前为止我们通过三篇文章(<控制反转>.<基于IoC的设计模式>和< 依赖注入模式>)从纯理论的角度 ...

  5. .NET CORE学习笔记系列(2)——依赖注入[4]: 创建一个简易版的DI框架[上篇]

    原文https://www.cnblogs.com/artech/p/net-core-di-04.html 本系列文章旨在剖析.NET Core的依赖注入框架的实现原理,到目前为止我们通过三篇文章从 ...

  6. 手动实现一个简易版SpringMvc

    版权声明:本篇博客大部分代码引用于公众号:java团长,我只是在作者基础上稍微修改一些内容,内容仅供学习与参考 前言:目前mvc框架经过大浪淘沙,由最初的struts1到struts2,到目前的主流框 ...

  7. 如何实现一个简易版的 Spring - 如何实现 Setter 注入

    前言 之前在 上篇 提到过会实现一个简易版的 IoC 和 AOP,今天它终于来了...相信对于使用 Java 开发语言的朋友们都使用过或者听说过 Spring 这个开发框架,绝大部分的企业级开发中都离 ...

  8. 如何实现一个简易版的 Spring - 如何实现 Constructor 注入

    前言 本文是「如何实现一个简易版的 Spring」系列的第二篇,在 第一篇 介绍了如何实现一个基于 XML 的简单 Setter 注入,这篇来看看要如何去实现一个简单的 Constructor 注入功 ...

  9. 如何实现一个简易版的 Spring - 如何实现 @Component 注解

    前言 前面两篇文章(如何实现一个简易版的 Spring - 如何实现 Setter 注入.如何实现一个简易版的 Spring - 如何实现 Constructor 注入)介绍的都是基于 XML 配置文 ...

随机推荐

  1. bash脚本里的-h是什么意思?

    问题描述 我在看脚本的时候,看到了下面代码 其中的-h "$PRG"我一时没明白是在判断什么东西.然后翻阅了一下菜鸟教程和其他教程,都没有说. 问题解决 -h其实是在判断这个文件是 ...

  2. Digester解析xml原理

    Tomcat内部是使用Digester来解析xml文件的,将xml转化为java对象. digester底层是基于SAX+事件驱动+栈的方式来搭建实现的,SAX主要用来解析xml,事件驱动主要是在解析 ...

  3. 我开源了一个Go学习仓库|笔记预览

    前言 大半个月前我参与了字节后端面试,未通过第四面,面试总结写在了这篇文章: https://juejin.cn/post/7132712873351970823 在此文的末尾,我写到为了全面回顾Go ...

  4. C#.NET ORM FreeSql 读取使用 US7ASCII 的 Oracle 数据库中文显示乱码问题

    前言 关于 Oracle US7ASCII 中文乱码的问题,Ado.Net 和 Odbc 无法解决.包括最新的.Net Core..NET6..NET7 都无法解决这个问题. FreeSql 对 Or ...

  5. 探秘:TriCore处理器中断机制

    1. TriCore与中断的简介 TriCore是德国英飞凌科技公司旗下的第一个为实时嵌入式系统而优化的统一的.32位的微控制器-DSP(Digital Signal Processing)处理器架构 ...

  6. Mac系统下Datagrip打不开、点击没反应?

    有没有可能是因为你从网上下载了一些破解软件导致的? 背景 Mac系统下JB公司家的IDEA. Datagrip.PyCharm 或 Goland 打不开点击没反应-- 分析 大概率是之前安装过 汉化插 ...

  7. 第三十一篇:vue3和vue2的不同

    好家伙 1.为什么会有vue3? Vue2和Vue3的区别 - 简书 (jianshu.com) 貌似是因为他的对手太优秀,所以他也必须进步 2.什么是api? 从文件操作开始谈API. 以C语言为例 ...

  8. maven执行跳过测试

    -Dmaven.test.skip=true 例子 mvn clean install -Dmaven.test.skip=true

  9. 在 Kubernetes 中部署 Redis 集群

    在 Kubernetes 中部署 Redis 集群 在Kubernetes中部署Redis集群面临挑战,因为每个 Redis 实例都依赖于一个配置文件,该文件可以跟踪其他集群实例及其角色.为此,我们需 ...

  10. csrf跨站请求伪造、csrf相关装饰器、auth认证模块、基于django中间件设计项目功能

    目录 csrf跨站请求网站 什么是csrf跨站请求网站 经典例子-钓鱼网站 模拟 如何避免这种现象(预防) 如何在django中解决这个问题 form表单 ajax csrf相关装饰器 FBV CBV ...