背景
最近在公司內部進行一個引導配置系統的開發中,需要實作一個多圖輪播的功能,到這時很多同學會說了,“那你直接用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=https://www.cnblogs.com/marui01/archive/2022/10/27/{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=https://www.cnblogs.com/marui01/archive/2022/10/27/{f} style={{ ...styles }} alt="" />
</SwiperSlide>
))}
</SwiperContainer>
</Swiper>);
}
export default Swiper;
至此為止,我們整體的dom結構與樣式就撰寫完成了,后面要做的就是如何讓他們按照我們想要的那樣,動起來,
4、影片實作
既然說到了輪播影片的實作,那么我們最先想到的也是最方便的方式,肯定是我們最熟悉的setInterval,那么整體的實作思路是什么樣的呢?
先思考一下我們想要實作的功能:
1、按照預設的引數實作定時的圖片切換功能;
2、如果沒有預設delay的話,則不自動輪播;
3、每次輪播的距離,是由用戶配置的圖片寬高決定;
4、輪播至最后一張后,停止輪播,
首先,為了保證元素可以正常的移動,我們在元素身上添加ref和id便于獲取正確的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=https://www.cnblogs.com/marui01/archive/2022/10/27/{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;
總結
其實很多時候,我們都會覺得對于一個需求(功能)的開發無從下手,可是如果我們耐下心來,將我們要實作的目標進行抽絲剝繭樣的拆解,讓我們從最最簡單的部分開始進行實作和設計,然后逐步自我迭代,將功能細化、優化、深化,那么最后的效果可能會給你自己一個驚喜哦,
妙言至徑,大道至簡,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/521881.html
標籤:其他
上一篇:CSS 漸變鋸齒消失術
