React實現(xiàn)一個簡易版Swiper

背景

最近在公司內(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、輪播至最后一張后侄柔,停止輪播共啃。

首先,為了保證元素可以正常的移動暂题,我們在元素身上添加refid便于獲取正確的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)化哀军、深化沉眶。那么最后的效果可能會給你自己一個驚喜哦。

妙言至徑杉适,大道至簡谎倔。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市猿推,隨后出現(xiàn)的幾起案子片习,更是在濱河造成了極大的恐慌捌肴,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件藕咏,死亡現(xiàn)場離奇詭異状知,居然都是意外死亡,警方通過查閱死者的電腦和手機孽查,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門饥悴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人盲再,你說我怎么就攤上這事西设。” “怎么了洲胖?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵济榨,是天一觀的道長。 經(jīng)常有香客問我绿映,道長擒滑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任叉弦,我火速辦了婚禮丐一,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘淹冰。我一直安慰自己库车,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布樱拴。 她就那樣靜靜地躺著柠衍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪晶乔。 梳的紋絲不亂的頭發(fā)上珍坊,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機與錄音正罢,去河邊找鬼阵漏。 笑死,一個胖子當(dāng)著我的面吹牛翻具,可吹牛的內(nèi)容都是我干的履怯。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼裆泳,長吁一口氣:“原來是場噩夢啊……” “哼叹洲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起晾虑,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤疹味,失蹤者是張志新(化名)和其女友劉穎仅叫,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體糙捺,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡诫咱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了洪灯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坎缭。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖签钩,靈堂內(nèi)的尸體忽然破棺而出掏呼,到底是詐尸還是另有隱情,我是刑警寧澤铅檩,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布憎夷,位于F島的核電站,受9級特大地震影響昧旨,放射性物質(zhì)發(fā)生泄漏拾给。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一兔沃、第九天 我趴在偏房一處隱蔽的房頂上張望蒋得。 院中可真熱鬧,春花似錦乒疏、人聲如沸额衙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽窍侧。三九已至,卻和暖如春转绷,著一層夾襖步出監(jiān)牢的瞬間疏之,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工暇咆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人丙曙。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓爸业,卻偏偏與公主長得像,于是被迫代替她去往敵國和親亏镰。 傳聞我的和親對象是個殘疾皇子扯旷,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容

  • vue-awesome-swiper是基于swiper的,安裝不同版本的vue-awesome-swiper對應(yīng)不...
    秋分落葉閱讀 8,481評論 0 0
  • js插件之iscroll,swiper 1.iscroll 1.1我們在電商網(wǎng)站移動端經(jīng)常會看見有側(cè)邊欄,如下圖 ...
    煤球快到碗里來閱讀 486評論 0 0
  • 圖片輪播是前端中經(jīng)常需要實現(xiàn)的一個功能索抓。最近學(xué)習(xí)Vue.js钧忽,就針對Swiper進行封裝毯炮,實現(xiàn)一個簡單的圖片輪播組...
    Ruheng閱讀 12,232評論 1 26
  • 前言 團隊合作臨摹餓了么移動端APP,選擇了現(xiàn)在比較熱門的React框架耸黑,雖然項目功能還不完善桃煎,但是在開發(fā)的過程中...
    視覺派Pie閱讀 17,064評論 7 90
  • 一、概念介紹 Vue.js和React.js分別是目前國內(nèi)和國外最火的前端框架大刊,框架跟類庫/插件不同为迈,框架是一套完...
    劉遠舟閱讀 1,061評論 0 0