背景

最近在公司内部进行一个引导配置系统的开发中,需要实现一个多图轮播的功能。到这时很多同学会说了,“那你直接用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. 非常全的一份Python爬虫的Xpath博文

    非常全的一份Python爬虫的Xpath博文 Xpath 是 python 爬虫过程中非常重要的一个用来定位的一种语法. 一.开始使用 首先我们需要得到一个 HTML 源代码,用来模拟爬取网页中的源代 ...

  2. JUC源码学习笔记5——线程池,FutureTask,Executor框架源码解析

    JUC源码学习笔记5--线程池,FutureTask,Executor框架源码解析 源码基于JDK8 参考了美团技术博客 https://tech.meituan.com/2020/04/02/jav ...

  3. flutter系列之:用来管理复杂状态的State详解

    目录 简介 StatefuWidget和State State的生命周期 总结 简介 Flutter的基础是widget,根据是否需要跟用户进行交互,widget则可以分为StatelessWidge ...

  4. 免杀手法-tcp套字节传递shellcode学习

    免杀手法-tcp套字节传递shellcode学习

  5. 【HTML】学习路径5-预格式标签和字体标签

    <!DOCTYPE html> <html> <head> <title>我是标题</title> <meta charset=&qu ...

  6. Android Kotlin Annotation Processer

    Annotation Processer 注解处理器(Annotation Processer)是javac内置的注解处理工具,可以在编译时处理注解,让我们自己做相应的处理.比如生成重复度很高的代码, ...

  7. x64dbg 插件开发环境配置

    x64dbg 是一款开源的应用层反汇编调试器,旨在对没有源代码的可执行文件进行恶意软件分析和逆向工程,同时 x64dbg 还允许用户开发插件来扩展功能,插件开发环境的配置非常简单,如下将简单介绍x64 ...

  8. git reset总结

    git reset git 的重置操作 有三种模式:hard.mixed(默认).soft 1. hard 用法 hard会重置stage区和工作区,和移动代码库上HEAD 和branch的指针所指向 ...

  9. Nginx几种负载均衡方式介绍

    Nginx几种负载均衡方式介绍 前言 负载均衡就是Nginx将请求分摊到不同的服务器中,保证服务的可用性,缓解服务压力,保证服务的响应速度,即使某一个应用服务不可用,也可以保证业务的正常进行,并且方便 ...

  10. 微信公众号商城、小程序商城、H5商城 实例 前后端源码

    CRMEB客户管理+电商营销系统  https://gitee.com/ZhongBangKeJi/CRMEB 演示站后台: http://demo.crmeb.net/admin 账号:demo 密 ...