背景

最近在公司内部进行一个引导配置系统的开发中,需要实现一个多图轮播的功能。到这时很多同学会说了,“那你直接用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. 从零开始实现一个MyBatis加解密插件

    作者:vivo 互联网服务器团队- Li Gang 本篇文章介绍使用MyBatis插件来实现数据库字段加解密的过程. 一.需求背景 公司出于安全合规的考虑,需要对明文存储在数据库中的部分字段进行加密, ...

  2. linux之间上传下载--SCP

    1.远程拷贝文件 [root@rhel8-client01 yum.repos.d]# scp root@192.168.72.149:/etc/yum.repos.d/* . (.表示拷贝到当前文件 ...

  3. 【java】学习路径32-绝对路径与相对路径

    获取文件路径的时候,我们发现有两个方法,getAbsolutePath和getPath两个方法. 前者是获取绝对路径,后者是相对路径. 绝对路径指的是完整路径,从盘符开始. 相对路径指的是从java当 ...

  4. 巧用 transition 实现短视频 APP 点赞动画

    在各种短视频界面上,我们经常会看到类似这样的点赞动画: 非常的有意思,有意思的交互会让用户更愿意进行互动. 那么,这么有趣的点赞动画,有没有可能使用纯 CSS 实现呢?那当然是必须的,本文,就将巧妙的 ...

  5. multiprocessing 让子进程忽略信号,手动关闭子进程

    起因 同事想要写一个代码,主进程中监听SIGINT.SIGTERM信号退出,并关闭启动的子进程,代码类似这样 import signal import sys import time from mul ...

  6. KingbaseES V8R3集群运维案例之---主库系统down failover切换过程分析

    ​ 案例说明: KingbaseES V8R3集群failover时两个cluster都会触发,但只有一个cluster会调用脚本去执行真正的切换流程,另一个有对应的打印,但不会调用脚本,只是走相关的 ...

  7. docker_NG部署前端总结

    Dockerfile 写法 FROM nginx MAINTAINER gradyjiang "jiangzhongjin@hotmail.com" ENV LANG C.UTF- ...

  8. DFS算法-求集合的所有子集

    目录 1. 题目来源 2. 普通方法 1. 思路 2. 代码 3. 运行结果 3. DFS算法 1. 概念 2. 解题思路 3. 代码 4. 运行结果 4. 对比 1. 题目来源 牛客网,集合的所有子 ...

  9. Typora自动上传超级详细教程!!

    第一步检查环境变量 打开cmd 查看以下环境变量 需要软件: Typora PicGo gitee账号 配置node 配置git 第二步创建gitee仓库 设置仓库名直接创建,因为这里不能直接修改开源 ...

  10. 华南理工大学 Python第2章课后小测-1

    1.(单选)"abc"的长度是3,"老师好"的长度是多少?(本题分数:4)A) 1B) 3C) 6D) 9您的答案:B  正确率:100%2.(单选)下面代码的 ...