React實現一個簡易版Swiper

来源:https://www.cnblogs.com/marui01/archive/2022/10/27/16832313.html
-Advertisement-
Play Games

背景 最近在公司內部進行一個引導配置系統的開發中,需要實現一個多圖輪播的功能。到這時很多同學會說了,“那你直接用swiper不就好了嗎?”。但其實是,因為所有引導的展示都是作為npm依賴的形式來進行插入的,所以我們想要做的就是:儘量減少外部依賴以及包的體積。所以,我們開始了手擼簡易版swiper之路 ...


背景

最近在公司內部進行一個引導配置系統的開發中,需要實現一個多圖輪播的功能。到這時很多同學會說了,“那你直接用swiper不就好了嗎?”。但其實是,因為所有引導的展示都是作為npm依賴的形式來進行插入的,所以我們想要做的就是:儘量減少外部依賴以及包的體積。所以,我們開始了手擼簡易版swiper之路。

功能訴求

首先,由於我們所有的內容都是支持配置的,所以首先需要支持停留時間(delay)的可配置;由於不想讓用戶覺得可配置的內容太多,所以我們決定當停留時間(delay)大於0時,預設開啟autoplay

其次,在常規的自動輪播外,還需要滿足設計同學對於分頁器(Pagination)的要求,也就是當前的展示內容對應的氣泡(bullet)需要是一個進度條的樣式,有一個漸進式的動畫效果

最後,由於滑動效果實現起來太麻煩,所以就不做了,其他的基本都是swiper的常規功能了。

由此,整體我們要開發的功能就基本確定,後面就是開始逐步進行實現。

效果展示

2022-10-25 11.21.56.gif

整體思路

1、入參與變數定義

由於需要用戶自定義配置整體需要展示的圖片,並且支持自定義整體的寬高輪播時間(delay);同樣,我們也應該支持用戶自定義輪播的方向(direction)

綜上我們可以定義如下的入參:

{
  direction?: 'horizontal' | 'vertical';
  speed?: number;
  width: string;
  height: string;
  urls: string[];
}

而在整個swiper運行的過程中我們同樣是需要一些參數來幫助我們實現不同的基礎功能,比如

2、dom結構

從dom結構上來說,swiper的核心邏輯就是,擁有單一的可視區,然後讓所有的內容都在可視區移動、替換,以此來達到輪播的效果實現。

Web 1280 1.png

那麼如何來實現上的效果呢?這裡簡單梳理一下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;

總結

其實很多時候,我們都會覺得對於一個需求(功能)的開發無從下手。可是如果我們耐下心來,將我們要實現的目標進行抽絲剝繭樣的拆解,讓我們從最最簡單的部分開始進行實現和設計,然後逐步自我迭代,將功能細化、優化、深化。那麼最後的效果可能會給你自己一個驚喜哦。

妙言至徑,大道至簡。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • uni-app自帶uni.request用於網路請求,因為我們需要自定義攔截器等功能,也是為了和我們後臺管理保持統一,這裡我們使用比較流行且功能更強大的axios來實現網路請求。 Axios 是一個基於 promise 網路請求庫,作用於node.js 和瀏覽器中。 它是 isomorphic 的( ...
  • 隨著移動智能終端的普及,未成年人首次觸網的年齡不斷降低。根據中國互聯網路信息中心第48次《中國互聯網路發展狀況統計報告》顯示,截至2021年6月,中國網民規模為10.11億人,6—19歲網民占15.7%,共1.58億人。同時根據第十次中國未成年人互聯網運用調查顯示,中國未成年人互聯網普及率為99.2 ...
  • 好家伙,本篇用於測試"添加"介面,為後續"用戶註冊"功能做鋪墊 (完整代碼在最後) 我們要實現"添加"功能 老樣子我們先來理清一下思路, 現在,我是一個用戶,我來到了註冊頁面,我在一個①表單中要把我要添加的數據填好, 然後點擊添加按鈕,進行②數據校驗(看看用戶有沒有少填的數據), 隨後,③發送網路請 ...
  • 使用three.js(webgl)搭建智慧樓宇、3D園區、3D廠房、3D碼頭、3D海關、3D倉庫、3D定位、三維室內定位、設備檢測、數字孿生、物聯網3D、物業3D監控、物業基礎設施可視化運維、3d建築,3d消防,消防演習模擬,3d庫房,webGL,threejs,3d機房,bim管理系統 ...
  • 2022-10-27 一、Vue的列表渲染 (1)關鍵字:v-for (2)用法:v-for:"臨時變數名 in 列表名"。“列表”的說明是寫在script中的Vue中的data中。 (3)擴展:在列表渲染中,渲染就是在前端能看見的。需要用到索引的用法。v-for:"(臨時變數名,index) in ...
  • 01、HTTP 常⻅的狀態碼有哪些? 1xx 伺服器收到請求 2xx 請求成功 200 成功狀態碼 3xx 重定向 301永久重定向,瀏覽器下次⾃動取重定向的地址 302臨時重定向,瀏覽器下次還會請求原地址 304 資源未被修改 4xx 客戶端錯誤 403 沒有許可權 404 資源未找到 5xx 服務 ...
  • 今天npm run dev的時候,有個頁面報錯,提示[Vue warn]: Failed to mount component: template or render function not defined.昨天還好好的,今天就報錯了,也沒改啥。經過查資料,反覆查證回想改了什麼,發現是因為昨天在在 ...
  • 上文 《Vitepress搭建組件庫文檔(上)—— 基本配置》已經討論了 vitepress 搭建組件庫文檔的基本配置,包括站點 Logo、名稱、首頁 *home* 佈局、頂部導航、左側導航等。本文進入最重要的部分 —— 如何像 *Element Plus* 那樣一遍代碼就可以展示組件的效果和源代碼... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...