背景
最近在公司內(nèi)部進行一個引導(dǎo)配置系統(tǒng)的開發(fā)中,需要實現(xiàn)一個多圖輪播的功能漂洋。到這時很多同學(xué)會說了祖乳,“那你直接用swiper
不就好了嗎逗堵?”。但其實是眷昆,因為所有引導(dǎo)的展示都是作為npm依賴的形式來進行插入的蜒秤,所以我們想要做的就是:盡量減少外部依賴以及包的體積
。所以隙赁,我們開始了手?jǐn)]簡易版swiper之路垦藏。
功能訴求
首先,由于我們所有的內(nèi)容都是支持配置的伞访,所以首先需要支持停留時間(delay)
的可配置掂骏;由于不想讓用戶覺得可配置的內(nèi)容太多,所以我們決定當(dāng)停留時間(delay)大于0時厚掷,默認(rèn)開啟autoplay
弟灼。
其次,在常規(guī)的自動輪播外冒黑,還需要滿足設(shè)計同學(xué)對于分頁器(Pagination)
的要求田绑,也就是當(dāng)前的展示內(nèi)容對應(yīng)的氣泡(bullet)
需要是一個進度條的樣式,有一個漸進式的動畫效果抡爹。
最后掩驱,由于滑動效果實現(xiàn)起來太麻煩,所以就不做了,其他的基本都是swiper的常規(guī)功能了欧穴。
由此民逼,整體我們要開發(fā)的功能就基本確定,后面就是開始逐步進行實現(xiàn)涮帘。
整體思路
1拼苍、入?yún)⑴c變量定義
由于需要用戶自定義配置整體需要展示的圖片
,并且支持自定義整體的寬高
與輪播時間(delay)
调缨;同樣疮鲫,我們也應(yīng)該支持用戶自定義輪播的方向(direction)
。
綜上我們可以定義如下的入?yún)ⅲ?/p>
{
direction?: 'horizontal' | 'vertical';
speed?: number;
width: string;
height: string;
urls: string[];
}
而在整個swiper運行的過程中我們同樣是需要一些參數(shù)來幫助我們實現(xiàn)不同的基礎(chǔ)功能弦叶,比如
2痰滋、dom結(jié)構(gòu)
從dom結(jié)構(gòu)上來說兄世,swiper的核心邏輯就是叁鉴,擁有單一的可視區(qū)
壹甥,然后讓所有的內(nèi)容都在可視區(qū)
內(nèi)移動、替換默责,以此來達到輪播的效果實現(xiàn)。
那么如何來實現(xiàn)上的效果呢咸包?這里簡單梳理一下html的實現(xiàn):
// 可見區(qū)域容器
<div id="swiper">
// 輪播的真實內(nèi)容區(qū)桃序,也就是實際可以移動的區(qū)域
<div className="swiper-container" id="swiper-container">
// 內(nèi)部節(jié)點的渲染
{urls.map((f: string, index: number) => (
<div className="slide-node">
<img src={f} alt="" />
</div>
))}
</div>
</div>
到這里一個簡陋的dom結(jié)構(gòu)就出現(xiàn)了。接下來就需要我們?yōu)樗麄冄a充一些樣式烂瘫。
3媒熊、樣式(style)
為了減少打包時處理的文件類型,并且以盡可能簡單的進行樣式開發(fā)為目標(biāo)坟比。所以我們在開發(fā)過程中選擇了使用styled-components
來進行樣式的編寫芦鳍,具體使用方式可參考styled-components: Documentation。
首先葛账,我們先來梳理一下對于最外層樣式的要求柠衅。最基本的肯定是要支持參數(shù)配置寬高
以及僅在當(dāng)前區(qū)域內(nèi)可查看
。
而真正的代碼實現(xiàn)其實很簡單:
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;
其次籍琳,我們來進行滾動區(qū)的樣式的開發(fā)菲宴。
但是這里我們要明確不同的是,我們除了單獨的展示樣式的開發(fā)外趋急,我們還要主要對于過場動畫
效果的實現(xiàn)喝峦。
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,
// 根據(jù)輪播方向參數(shù),調(diào)整flex布局方向
flexDirection: direction === "horizontal" ? "row" : "column",
}}
>
</SwiperContainer>
</Swiper>);
}
export default Swiper;
在這里呜达,我們給了他默認(rèn)的寬度為auto
谣蠢,來實現(xiàn)整體寬度自適應(yīng)。而使用transition
讓后續(xù)的圖片輪換可以有動畫效果。
最后眉踱,我們只需要將圖片循環(huán)渲染在列表中即可勋颖。
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,
// 根據(jù)輪播方向參數(shù),調(diào)整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結(jié)構(gòu)與樣式就編寫完成了饭玲,后面要做的就是如何讓他們按照我們想要的那樣,動起來叁执。
4茄厘、動畫實現(xiàn)
既然說到了輪播動畫的實現(xiàn),那么我們最先想到的也是最方便的方式谈宛,肯定是我們最熟悉的setInterval
次哈,那么整體的實現(xiàn)思路是什么樣的呢?
先思考一下我們想要實現(xiàn)的功能:
1吆录、按照預(yù)設(shè)的參數(shù)實現(xiàn)定時的圖片切換功能窑滞;
2、如果沒有預(yù)設(shè)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,
// 根據(jù)輪播方向參數(shù),調(diào)整flex布局方向
flexDirection: direction === "horizontal" ? "row" : "column",
}}
>
...
</SwiperContainer>
...
其次薪者,我們需要定義activeIndex
這個state纵苛,用來標(biāo)記當(dāng)前展示的節(jié)點;以及用isDone
標(biāo)記是否所有圖片都已輪播完成(所以反饋參數(shù))啸胧。
import React, { FC, useState } from "react";
const [activeIndex, setActiveIndex] = useState<number>(0);
const [isDone, setDone] = useState<boolean>(false);
然后赶站,我們還需要進行timer
接收參數(shù)的定義,這里我們可以選擇使用useRef
來進行定義纺念。
import React, { FC, useRef } from "react";
const timer = useRef<any>(null);
在上面的一切都準(zhǔn)備就緒后贝椿,我們可以進行封裝啟動方法的封裝
// 使用定時器,定時進行activeIndex的替換
const startPlaySwiper = () => {
if (speed <= 0) return;
timer.current = setInterval(() => {
setActiveIndex((preValue) => preValue + 1);
}, speed * 1000);
};
但是到此為止陷谱,我們只是進行了activeIndex
的自增烙博,并沒有真正的讓頁面上的元素動起來瑟蜈,為了實現(xiàn)真正的動畫效果
,我們使用useEffect
對于activeIndex
進行監(jiān)聽渣窜。
import React, { FC, useEffect, useRef, useState } from "react";
useEffect(() => {
const swiper = document.querySelector("#swiper-container") as any;
// 根據(jù)用戶傳入的輪播方向铺根,決定是在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]);
截止到這里乔宿,其實簡易的自動輪播就完成了位迂,但是其實很多同學(xué)也會有疑問?,是不是還缺少分頁器(Pagination)
详瑞。
5掂林、分頁器(Pagination)
分頁器
的原理其實很簡單,我們可以分成兩個步驟來看坝橡。
1泻帮、渲染與圖片相同個數(shù)的節(jié)點;
2计寇、根據(jù)activeIndex
動態(tài)改變分頁樣式锣杂。
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}
細(xì)心的同學(xué)可能看到我在這里為什么還有一個SlideBarInner
元素,其實是在這里實現(xiàn)了一個當(dāng)前所在分頁停留時間進度條
展示的功能番宁,感興趣的同學(xué)可以自己看一下元莫,我這里就不在贅述了。
6贝淤、整體實現(xiàn)代碼
最后柒竞,我們可以看到完整的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,
// 根據(jù)輪播方向參數(shù),調(diào)整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;
總結(jié)
其實很多時候播聪,我們都會覺得對于一個需求(功能)的開發(fā)無從下手〔几簦可是如果我們耐下心來离陶,將我們要實現(xiàn)的目標(biāo)進行抽絲剝繭樣的拆解,讓我們從最最簡單的部分開始進行實現(xiàn)和設(shè)計衅檀,然后逐步自我迭代招刨,將功能細(xì)化、優(yōu)化哀军、深化沉眶。那么最后的效果可能會給你自己一個驚喜哦。
妙言至徑杉适,大道至簡谎倔。