<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          React 輪播動(dòng)畫探索

          共 14204字,需瀏覽 29分鐘

           ·

          2022-01-08 12:30

          1. 前言

          1.1. 氛圍氣泡需求

          最近投入了一個(gè)需求,遇到一個(gè)需要用動(dòng)畫去實(shí)現(xiàn)的場(chǎng)景。

          我們的產(chǎn)品大大管它叫氛圍氣泡,在很多應(yīng)用(淘寶、抖音、bilibili)的直播間場(chǎng)景都會(huì)有類似這樣營造氛圍感的組件,能夠讓你感知到其他用戶在當(dāng)前直播間的行為。

          這個(gè)東西看起來轉(zhuǎn)瞬即逝的,但背后其實(shí)是基于一套和 push 通道相關(guān)的設(shè)計(jì):

          前人栽樹后人乘涼,所幸大佬們把?push 消息中心?和?后臺(tái)服務(wù)?都建設(shè)得很完善,所以這次開發(fā)我只需要做這么一件事情:

          設(shè)置監(jiān)聽 push 的回調(diào),拿到數(shù)據(jù)渲染對(duì)應(yīng)組件。

          1.2. 開發(fā)評(píng)估

          看上去好簡(jiǎn)單!讓我來簡(jiǎn)單的評(píng)估一下它的開發(fā)成本:

          1.2.1. 首先看看它像啥(是否有現(xiàn)有組件可以復(fù)用)

          這東西一進(jìn)一出的,還撲棱撲棱的閃,好似一個(gè)輪播圖。在公共組件庫搜羅一下,找到了一個(gè)?Marquee(跑馬燈)?組件,它是基于?swiper/react?去實(shí)現(xiàn)的。

          swiper?大家都熟,一個(gè)功能非常強(qiáng)大且開箱即用的組件,目前已經(jīng)迭代到了 v7 版本。它也支持在現(xiàn)代前端框架下的使用,例如說支持?React

          在 React 中,我們可以給它初始化一堆幻燈片,讓它可以滑動(dòng):

          1.2.2. swiper 實(shí)踐

          基礎(chǔ)示例

          import SwiperCore, { Autoplay } from"swiper";
          import { Swiper, SwiperSlide } from"swiper/react";

          // Import Swiper styles
          import"swiper/css";
          import"swiper/css/navigation";
          import"swiper/css/pagination";
          import"swiper/css/scrollbar";
          import"./styles.css";

          // install Swiper modules
          SwiperCore.use([Autoplay]);

          const renderBubble = (bubble) => {
          const { name, wording, btnText } = bubble;
          return (
          <SwiperSlide>
          <p className="live-bubble">
          <span className="live-bubble-username">{name}span>

          <span className="live-bubble-wording">{wording}span>
          <span className="live-bubble-action">{btnText}span>
          p>
          SwiperSlide>
          );
          };

          export default () => {
          const dataList = [
          { name: "李*", wording: "咨詢了課程", btnText: "去咨詢" },
          { name: "黃*", wording: "領(lǐng)取了優(yōu)惠券", btnText: "去領(lǐng)取" },
          { name: "高*", wording: "分享了直播間給好友", btnText: "去分享" },
          { name: "劉*", wording: "領(lǐng)取了直播間資料", btnText: "去領(lǐng)取" },
          { name: "朱*", wording: "購買了直播間課程《xxx》", btnText: "去領(lǐng)取" }
          ];

          return (
          <div style={{ background: "#000" }}>
          <Swiper
          slidesPerView={1}
          direction="horizontal"
          onSlideChange={() =>
          console.log("slide change")}
          autoplay
          >
          {dataList.map((item) => renderBubble(item))}
          Swiper>

          div>
          );
          };

          設(shè)置了 autoplay,可以自動(dòng)播放,效果如下:

          細(xì)節(jié)改造

          你可能發(fā)現(xiàn)了,上面的示例跟想要實(shí)現(xiàn)的效果還差很遠(yuǎn),我們需要的效果是:

          • 輪播方向:從左至右

          • 進(jìn)入效果:從左到右一邊移入,一邊漸現(xiàn)

          • 退出效果:原地不動(dòng),漸隱

          接下來讓我們逐個(gè)擊破,改造一下 swiper。

          輪播方向修改

          autoplay 除了支持自動(dòng)播放,還可以設(shè)置自動(dòng)播放的方向。比如說,當(dāng)?direction?為?horizontal?的時(shí)候,每個(gè)滑塊默認(rèn)是向左退出和進(jìn)入的,即從右至左輪播。如果想要變成相反方向,可以這樣設(shè)置:

          autoplay={{
          delay: 3000,
          reverseDirection: true
          }}

          進(jìn)入效果和退出效果

          關(guān)于 swiper 切換效果的定制,官方也提供了一些切換效果相關(guān)的 api:effect

          其中比較符合我們要求的應(yīng)該是?creativeEffect,創(chuàng)造性的切換效果。我們目前想要定制一套漸隱退出和滑動(dòng)漸現(xiàn)進(jìn)入的效果。

          我們?cè)僭O(shè)置一下 swiper 的參數(shù):

            slidesPerView={1}
          direction="horizontal"
          onSlideChange={() =>console.log("slide change")}
          autoplay={{
          delay: 3000,
          reverseDirection: true
          }}
          loop
          speed={3000}
          effect={"creative"}
          creativeEffect={{
          prev: {
          opacity: 0,
          translate: ['-300%', 0, 0]
          },
          next: {
          opacity: 0
          }
          }}
          >

          prev 和 next 的具體參數(shù)類型可以參考?swiper creativeEffect,比如說上面示例中?creativeEffect?的意思是:

          • 進(jìn)入動(dòng)畫的起始狀態(tài)(prev):

            • translate:[’-300%‘, 0, 0],表示一開始在 x 軸的 -300% swiper 寬度的位置上

            • opacity:0,表示一開始透明不可見

          • 退出動(dòng)畫的結(jié)束狀態(tài)(next):

            • opacity:0,表示結(jié)束時(shí)透明不可見

          經(jīng)過我們的改造,最終效果如下:

          局限性

          上面的效果其實(shí)并沒有完全滿足我們的要求,我們可以觀察到 swiper 的幻燈片進(jìn)入和退出有這樣的特點(diǎn):會(huì)有某一段時(shí)間,上一張幻燈片和下一張幻燈片會(huì)同時(shí)存在于可視區(qū)域

          這個(gè)會(huì)導(dǎo)致我們的滑動(dòng)漸現(xiàn)進(jìn)入效果不能完美實(shí)現(xiàn),只能通過調(diào)整起始位置到盡可能遠(yuǎn),來擬合我們想要的效果。像上面其實(shí)就設(shè)置了 -300%,才達(dá)到了比較理想的效果。與之相對(duì)的,也帶來了另一個(gè)問題:透明度變化太快了,進(jìn)入可視區(qū)域時(shí)幻燈片的 opacity 已經(jīng)接近 1 了

          這下可把我整不會(huì)了,沒想到 swiper 還有這樣的局限性。但幻燈片切換效果不佳并不是最主要的,更重要的還是氛圍氣泡業(yè)務(wù)邏輯的實(shí)現(xiàn),我們看看結(jié)合 push 命令,動(dòng)態(tài)更新幻燈片數(shù)量的情況下,swiper 在 react 中的狀態(tài)管理會(huì)變得多不堪。

          另一個(gè)問題 —— 基于 swiper 動(dòng)態(tài)更新氛圍氣泡

          在實(shí)際業(yè)務(wù)使用中,其實(shí)還遇到了優(yōu)先級(jí)展示的問題,氛圍氣泡的位置一共有三個(gè)組件在輪流展示:

          • 打開直播間,先展示一段 5s 的課程公告

          • 公告消失后,如果有氛圍氣泡數(shù)據(jù),就展示氛圍氣泡

          • 如果沒有氛圍氣泡,就展示兜底的引導(dǎo)進(jìn)群組件

          如果我們需要?jiǎng)討B(tài)插入氛圍氣泡的話,就會(huì)有兩種情況:

          • 氛圍氣泡組件未初始化時(shí):通過組件 state 去緩存氛圍氣泡數(shù)組

          • 氛圍氣泡組件初始化后:通過 swiper 實(shí)例,調(diào)用 swiper.appendSlide/prependSlide 方法去插入氛圍氣泡幻燈片

          比如說以下的業(yè)務(wù)代碼:

          // 氛圍氣泡推送監(jiān)聽
          onAtmosphereBubble((data) => {
          // 未展示前,緩存氣泡到狀態(tài)中
          if (this.state.chatBoxTopIndex === 0) {
          this.setState((prev) => ({
          bubbleList: [
          data,
          ...prev.bubbleList,
          ],
          }));
          } elseif (this.state.chatBoxTopIndex === 1) {
          // 展示中,通過 swiper 實(shí)例插入幻燈片
          this.addBubble(data);
          } else {
          // 即將展示,向狀態(tài)中插入幻燈片
          this.setState((prev) => ({
          bubbleList: [
          data,
          ...prev.bubbleList,
          ],
          }));
          this.nextChatBoxTop(1);
          }
          });

          const addBubble = (data) => {
          this.swiper?.prependSlide('
          new Slide
          '
          );
          };

          從這里就能看出來,在 react 中,如果需要?jiǎng)討B(tài)更新幻燈片的場(chǎng)景,使用 swiper 相當(dāng)不合適。原因是 swiper 是通過示例方法去更新 UI,而 react 是通過 數(shù)據(jù)(狀態(tài))去更新 UI 的,兩者不太兼容。

          除此之外,實(shí)踐中也發(fā)現(xiàn)了很多其他的問題,比如說:

          • 通過 swiper.appendSlide/prependSlide 方法去插入新的幻燈片,只能傳入 HTML 字符串,不能傳入 React 組件。也就是說,新的幻燈片需要手動(dòng)綁定事件,且不具備 React 的生命周期 hook。

          • swiper 的幻燈片數(shù)據(jù)由組件 state 和 swiper 實(shí)例共同控制,會(huì)出現(xiàn)兩者數(shù)據(jù)不同步的情況,不易理解和管理。

          1.3. 別的方案?

          總的來說,swiper 在這個(gè)需求里表現(xiàn)得不是很好,使用它反而會(huì)讓代碼變得復(fù)雜。既然沒有現(xiàn)有的組件可以復(fù)用,我們可以怎么另辟蹊徑呢?接下來就來到本文的正題了,我們來通過一個(gè)神奇的 React 動(dòng)畫庫來實(shí)現(xiàn)我們的需求。

          2. react-transition-group

          react-transition-group?是 React 官方實(shí)現(xiàn)的,用于操作過渡效果的組件庫。它可以在組件安裝和卸載時(shí),增加過渡效果。一共提供了 4 個(gè) api,上手成本極低。

          我們首先從單個(gè)組件切換的場(chǎng)景入手,比如說現(xiàn)在希望為一個(gè)組件增加進(jìn)入和退出的動(dòng)畫,就可以使用 Transition 或者 CSSTransition。

          2.1. Transition 組件

          Transition 組件是一個(gè)自由度比較高的組件,CSSTransition 也是基于它擴(kuò)展的。它通過 in 參數(shù)跟蹤組件的進(jìn)入和退出(或者說顯隱),并由開發(fā)者自定義動(dòng)畫樣式。

          話不多說,我們直接上代碼。下面設(shè)計(jì)了一個(gè)按鈕點(diǎn)擊來控制組件進(jìn)入退出的示例:

          • index.js

          import React, { useState } from"react";
          import { Transition } from"react-transition-group";

          import"./styles.css";

          const duration = 500;

          const defaultStyle = {
          transition: `opacity ${duration}ms ease-in-out`,
          opacity: 0
          };

          const transitionStyles = {
          entering: { opacity: 1 },
          entered: { opacity: 1 },
          exiting: { opacity: 0 },
          exited: { opacity: 0 }
          };

          exportdefaultfunction App() {
          const [inProp, setInProp] = useState(false);
          return (
          <div>
          <Transition in={inProp} timeout={duration}>
          {(state) => (
          <div
          style={{
          ...defaultStyle,
          ...transitionStyles[state]
          }}
          >

          I'm a fade Transition!
          div>

          )}
          Transition>
          <button onClick={() => setInProp((prev) => !prev)}>Click to Enterbutton>
          div>
          );
          }

          效果如下:

          Transition 包含了以下參數(shù):

          • in:控制組件顯示的布爾值,觸發(fā)進(jìn)入或退出狀態(tài)

          • timeout:動(dòng)畫的持續(xù)時(shí)間,單位為毫秒,可以一次設(shè)置所有狀態(tài)的動(dòng)畫時(shí)間,也可以單獨(dú)設(shè)置每個(gè)狀態(tài)的動(dòng)畫時(shí)間。這個(gè)時(shí)間主要是結(jié)合 SwitchTransition api 使用的,需要和 css 中的動(dòng)畫時(shí)間保持一致。

          timeout={500}

          // or

          timeout={{
          appear: 500,
          enter: 300,
          exit: 500,
          }}
          • children 函數(shù):提供了一個(gè) state 參數(shù)的 children 函數(shù),用于渲染我們的組件。其中 state 包括了以下狀態(tài):

            • 'entering'

            • 'entered'

            • 'exiting'

            • 'exited'

          • 開始動(dòng)畫的三個(gè)鉤子,均接收一個(gè)回調(diào)函數(shù)?Function(node: HtmlElement, isAppearing: bool) -> void?,回調(diào)函數(shù)接收 2 個(gè)參數(shù),第一個(gè)參數(shù)為當(dāng)前元素的 dom 節(jié)點(diǎn),第二個(gè)參數(shù)表示當(dāng)前動(dòng)畫是否為元素初次掛載時(shí)發(fā)生

            • onEnter:在動(dòng)畫狀態(tài)變?yōu)?entering 之前調(diào)用

            • onEntering:在動(dòng)畫狀態(tài)變?yōu)?entering 之后調(diào)用

            • onEntered:在動(dòng)畫狀態(tài)變?yōu)?entered 之后調(diào)用

          • 離開動(dòng)畫的三個(gè)鉤子,均接收一個(gè)回調(diào)函數(shù)?Function(node: HtmlElement) -> void?,回調(diào)函數(shù)僅接收當(dāng)前元素的 dom 節(jié)點(diǎn)

            • onExit:在動(dòng)畫狀態(tài)變?yōu)?exiting 之前調(diào)用

            • onExiting:在動(dòng)畫狀態(tài)變?yōu)?exiting 之后調(diào)用

            • onExited:在動(dòng)畫狀態(tài)變?yōu)?exited 之后調(diào)用

          • ......

          2.2. CSSTransition 組件

          通過上面 Transition 的例子,我們也看到了,組件當(dāng)前的 class 是由 children 函數(shù)中的 state 參數(shù)來決定的,寫法上不夠 auto。CSSTransition 組件解決了這一問題,它繼承了 Transition 組件,并簡(jiǎn)化了 className 的聲明。

          同理,我們上一段代碼看看區(qū)別,依然是剛才的例子:

          • index.js

          import React, { useState } from"react";
          import { CSSTransition } from"react-transition-group";

          import"./styles.scss";

          const duration = 300;

          exportdefaultfunction App() {
          const [inProp, setInProp] = useState(false);
          return (
          <div>
          <CSSTransition in={inProp} timeout={duration} classNames="my-fade">
          <p className="my-fade">I'm a fade Transition!p>

          CSSTransition>
          <button onClick={() => setInProp((prev) => !prev)}>Click to Enterbutton>
          div>
          );
          }
          • style.scss

          .my-fade {
          ?opacity: 0;
          ?&-enter { opacity: 0 }
          ?&-enter-active {
          ? ?opacity: 1;
          ? ?transition: opacity 300ms;
          ?}
          ?&-enter-done {
          ? ?opacity: 1;
          ?}
          ?&-exit { opacity: 1 }
          ?&-exit-active {
          ? ?opacity: 0;
          ? ?transition: opacity 300ms;
          ?}
          ?&-exit-done {
          ? ?opacity: 0;
          ?}
          };

          效果依然是:

          CSSTransition 會(huì)根據(jù) in 參數(shù)的變化,為組件添加不同的 class。例如,當(dāng) in 變?yōu)?true,會(huì)先后為組件添加 {classNames}-enter、{classNames}-enter-active、{classNames}-enter-done 的 class,形成入場(chǎng)的動(dòng)畫效果;當(dāng) in 變?yōu)?false,則會(huì)為組件添加 {classNames}-exit、{classNames}-exit-active、{classNames}-exit-done 的 class,形成退場(chǎng)的動(dòng)畫效果。

          CSSTransition 默認(rèn)有三個(gè)階段 —— 消失(appear)、進(jìn)入(enter)、離開(exit)。并且每個(gè)階段都先后添加三個(gè)類名,以 enter 為例,分別是:

          • enter 表示開始動(dòng)畫的初始階段

          • enter-active 表示開始動(dòng)畫的激活階段

          • enter-done 表示開始動(dòng)畫的結(jié)束階段,也是樣式的持久化展示階段

          CSSTransition 的參數(shù)跟 Transition 差別不大,需要注意的是?classNames?這個(gè)參數(shù)。為了與 React 中的 className 進(jìn)行區(qū)別,classNames?這個(gè)參數(shù)在?className?的基礎(chǔ)上在末尾加了個(gè)?s

          一般來說,這個(gè)參數(shù)將作為動(dòng)畫過渡的一系列類名(-enter、-enter-active、-enter-done、......)的前綴。

          classNames="my-fade"

          // my-fade-enter
          // my-fade-enter-active
          // my-fade-enter-done
          // ......

          但也支持對(duì)每個(gè)類名單獨(dú)定義:

          classNames={{
          appear: 'my-appear',
          appearActive: 'my-active-appear',
          appearDone: 'my-done-appear',
          enter: 'my-enter',
          enterActive: 'my-active-enter',
          enterDone: 'my-done-enter',
          exit: 'my-exit',
          exitActive: 'my-active-exit',
          exitDone: 'my-done-exit',
          }}

          2.3. SwitchTransition

          SwitchTransition 用于包裹 Transition 或 CSSTransition 組件,并修改它們內(nèi)部組件的過渡模式。它擁有一個(gè)?mode?參數(shù),可以實(shí)現(xiàn)兩種效果:

          • out-in?:當(dāng)前元素先轉(zhuǎn)出,然后當(dāng)完成時(shí),新元素轉(zhuǎn)入。

          • in-out?:新元素首先轉(zhuǎn)入,然后當(dāng)完成時(shí),當(dāng)前元素轉(zhuǎn)出。

          默認(rèn):?'out-in'

          同樣上代碼來看看效果:

          • index.js

          import React, { useState } from"react";
          import { CSSTransition, SwitchTransition } from"react-transition-group";
          import { Button, Form } from"react-bootstrap";

          import"./styles.scss";

          const modes = ["out-in", "in-out"];

          exportdefaultfunction App() {
          ?const [mode, setMode] = useState("out-in");
          ?const [state, setState] = useState(true);
          ?return (
          ? ?<div>
          ? ? ?<div className="label">Mode:div>

          ? ? ?<div className="modes">
          ? ? ? ?{modes.map((m) => (
          ? ? ? ? ?<Form.Check
          ? ? ? ? ? ?key={m}
          ? ? ? ? ? ?custom
          ? ? ? ? ? ?inline
          ? ? ? ? ? ?label={m}
          ? ? ? ? ? ?id={`mode=msContentScript${m}`}
          ? ? ? ? ? ?type="radio"
          ? ? ? ? ? ?name="mode"
          ? ? ? ? ? ?checked={mode === m}
          ? ? ? ? ? ?value={m}
          ? ? ? ? ? ?onChange={(event) =>
          {
          ? ? ? ? ? ? ?setMode(event.target.value);
          ? ? ? ? ? ?}}
          ? ? ? ? ?/>
          ? ? ? ?))}
          ? ? ?div>
          ? ? ?<div className="main">
          ? ? ? ?<SwitchTransition mode={mode}>
          ? ? ? ? ?<CSSTransition
          ? ? ? ? ? ?key={state}
          ? ? ? ? ? ?addEndListener={(node, done) =>
          {
          ? ? ? ? ? ? ?node.addEventListener("transitionend", done, false);
          ? ? ? ? ? ?}}
          ? ? ? ? ? ?classNames="fade"
          ? ? ? ? ?>
          ? ? ? ? ? ?<div className="button-container">
          ? ? ? ? ? ? ?<Button onClick={() => setState((state) => !state)}>
          ? ? ? ? ? ? ? ?{state ? "Hello, world!" : "Goodbye, world!"}
          ? ? ? ? ? ? ?Button>
          ? ? ? ? ? ?div>
          ? ? ? ? ?CSSTransition>
          ? ? ? ?SwitchTransition>
          ? ? ?div>
          ? ?div>
          ?);
          }
          • style.scss

          body {
          ?padding: 2rem;
          ?font-family: sans-serif;
          ?text-align: center;
          }

          .label {
          ?margin-bottom: 0.5rem;
          }

          .modes {
          ?margin-bottom: 1rem;
          }

          .button-container {
          ?margin-bottom: 0.5rem;
          }

          .fade {
          ?&-enter .btn {
          ? ?opacity: 0;
          ? ?transform: translateX(-100%);
          ?}
          ?&-enter-active .btn {
          ? ?opacity: 1;
          ? ?transform: translateX(0%);
          ?}
          ?&-exit .btn {
          ? ?opacity: 1;
          ? ?transform: translateX(0%);
          ?}
          ?&-exit-active .btn {
          ? ?opacity: 0;
          ? ?transform: translateX(100%);
          ?}
          ?&-enter-active .btn,
          ?&-exit-active .btn {
          ? ?transition: opacity 500ms, transform 500ms;
          ?}
          }

          效果如下:

          從這里可以看出和 swiper 的區(qū)別,swiper 類似于?in-out?的效果,而我們希望實(shí)現(xiàn)的氛圍氣泡是?out-in?的效果。

          2.4. TransitionGroup

          顧名思義,TransitionGroup?是用來管理多個(gè) Transition/CSSTransition 的。當(dāng)需要管理多個(gè) Transition,即需要實(shí)現(xiàn)不同的動(dòng)畫效果時(shí),適合使用它。


          ?{list.map(({ id }) => (
          ? ?<CSSTransition
          ? ? ?key={id}
          ? ? ?timeout={300}
          ? ? ?classNames="my"
          ? ?>

          ? ? ?<MyComponent key={id}/>
          ? ?CSSTransition>

          ?))}

          3. 用 react-transition-group 實(shí)現(xiàn)氛圍氣泡

          了解了?react-transition-group?之后,我們很容易聯(lián)想到,可以用 CSSTransition + SwitchTransition(out-in mode) 實(shí)現(xiàn)我們的需求。

          但在實(shí)現(xiàn)之前,還需要定義一下我們的數(shù)據(jù)結(jié)構(gòu)。

          3.1. 數(shù)據(jù)結(jié)構(gòu)

          氣泡是隨著用戶的交互,從客戶端觸發(fā)上報(bào)給到后臺(tái),再由后臺(tái)反饋到其他客戶端的。由于上文提到氛圍氣泡不是常駐的,會(huì)去展示其他的組件,所以當(dāng)后臺(tái)反饋了新的氣泡數(shù)據(jù),會(huì)存在三種情況:

          • 正在展示氛圍氣泡:將氣泡數(shù)據(jù)插入到展示順序的尾部。

          • 正在展示不可中斷的組件(課程公告):將氣泡數(shù)據(jù)緩存起來。

          • 正在展示可中斷的組件(兜底組件):將氣泡數(shù)據(jù)緩存起來,并立即展示氛圍氣泡。

          顯然是一個(gè)?隊(duì)列,我們可以維護(hù)一個(gè)氣泡的 JSX 元素?cái)?shù)組,用一個(gè) index 來決定當(dāng)前展示的氣泡來實(shí)現(xiàn)切換渲染,并將不斷到來的氣泡數(shù)據(jù)插入到數(shù)組的尾部。

          這樣的好處在于,相比 swiper/react 通過狀態(tài)和實(shí)例來維護(hù)氣泡的方式,我們統(tǒng)一使用狀態(tài)來維護(hù)氣泡數(shù)據(jù)更加符合數(shù)據(jù)驅(qū)動(dòng)視圖的思想。

          3.2. 隊(duì)列實(shí)現(xiàn)

          我們將氣泡列表的展示順序(index)放到 props 中維護(hù),使之變成受控的。并在隊(duì)列中維護(hù)一個(gè)定時(shí)器,定時(shí)觸發(fā) props 中的 nextBubble 方法去更新 index。如下:

          import React, { useEffect, useCallback } from"react";
          import { CSSTransition, SwitchTransition } from"react-transition-group";
          import"./index.css";

          const AtmosphereBubbleSequence = ({
          ?bubbleList,
          ?index,
          ?setCurIndex,
          ?nextBubble,
          ?next,
          ?resetList
          }
          ) =>
          {
          ?const bubbleListLength = bubbleList.length;
          ?let curIndex = 0;

          ?if (index > -1 && index < bubbleListLength) {
          ? ?curIndex = index;
          ?} else {
          ? ?curIndex = bubbleListLength - 1;
          ? ?setCurIndex(curIndex);
          ?}

          ?useEffect(() => {
          ? ?// 定時(shí)觸發(fā)
          ? ?const interval = setInterval(() => {
          ? ? ?nextBubble();
          ? ?}, 3000);
          ? ?return() => {
          ? ? ?clearInterval(interval);
          ? ?};
          ?}, []);

          ?const onExited = useCallback(
          ? ?(node) => {
          ? ? ?if (+node.dataset.bubbleKey === bubbleListLength - 1) {
          ? ? ? ?// 切換到兜底組件
          ? ? ? ?next();
          ? ? ? ?// 清空氣泡列表
          ? ? ? ?resetList();
          ? ? ?}
          ? ?},
          ? ?[bubbleListLength, next, resetList]
          ?);

          ?return (
          ? ?<SwitchTransition>
          ? ? ?<CSSTransition
          ? ? ? ?key={bubbleList[index]?.type?.name + index || Math.random()}
          ? ? ? ?timeout={{
          ? ? ? ? ?enter: 300,
          ? ? ? ? ?exit: 300
          ? ? ? ?}}
          ? ? ? ?classNames="atmosphere-bubble-cnt"
          ? ? ? ?unmountOnExit
          ? ? ? ?onExited={onExited}
          ? ? ?>

          ? ? ? ?<div data-bubble-key={index} className="atmosphere-bubble-cnt">
          ? ? ? ? ?{bubbleList[index]}
          ? ? ? ?div>

          ? ? ?CSSTransition>
          ? ?SwitchTransition>
          ?);
          };

          export default AtmosphereBubbleSequence;

          值得注意的是,需要在 onExited 回調(diào)中去判斷氣泡列表是否已經(jīng)展示完畢,調(diào)用銷毀氣泡序列組件的方法,并清空氣泡數(shù)據(jù)列表,去展示其他的組件。

          同時(shí),我們?cè)O(shè)置氣泡的樣式如下:

          .atmosphere-bubble-cnt {
          ?padding: 016px;
          }

          .atmosphere-bubble-cnt-enter {
          ?opacity: 0;
          ?transform: translate3d(-60px, 0, 0);
          }

          .atmosphere-bubble-cnt-enter-active {
          ?opacity: 1;
          ?transform: translate3d(0, 0, 0);
          ?transition: opacity 300ms, transform 300ms;
          }

          .atmosphere-bubble-cnt-exit {
          ?opacity: 1;
          }

          .atmosphere-bubble-cnt-exit-active {
          ?opacity: 0;
          ?transition: opacity 300ms;
          }

          3.3. 效果模擬

          為了試驗(yàn)效果,我們?cè)谕獠吭O(shè)置一段 mock 邏輯,來模擬不斷塞入氣泡數(shù)據(jù)的情況:

          import React, { useEffect, useState } from"react";
          import Bubble from"./Bubble";
          import AtmosphereBubbleSequence from"./Sequence";
          import"./styles.css";

          exportdefaultfunction App() {
          ?const [index, setIndex] = useState(0);

          ?const [bubbleDataList, setBubbleDataList] = useState([
          ? ?{ nick: "李*", content: "咨詢了課程", eventMsg: "去咨詢" },
          ? ?{ nick: "黃*", content: "領(lǐng)取了優(yōu)惠券", eventMsg: "去領(lǐng)取" },
          ? ?{ nick: "高*", content: "分享了直播間給好友", eventMsg: "去分享" },
          ? ?{ nick: "劉*", content: "領(lǐng)取了直播間資料", eventMsg: "去領(lǐng)取" },
          ? ?{ nick: "朱*", content: "購買了直播間課程《xxx》", eventMsg: "去領(lǐng)取" }
          ?]);

          ?useEffect(() => {
          ? ?const interval = setInterval(() => {
          ? ? ?setBubbleDataList((prev) => [
          ? ? ? ?...prev,
          ? ? ? ?{ nick: "朱*", content: "購買了直播間課程《xxx》", eventMsg: "去領(lǐng)取" }
          ? ? ?]);
          ? ?}, 3000);
          ? ?return() => {
          ? ? ?clearInterval(interval);
          ? ?};
          ?}, []);

          ?if (!bubbleDataList.length) {
          ? ?returnnull;
          ?}

          ?const nextBubble = (newIndex) => {
          ? ?setIndex((prevIndex) => {
          ? ? ?returntypeof newIndex === "undefined" ? prevIndex + 1 : newIndex;
          ? ?});
          ?};

          ?return (
          ? ?<div
          ? ? ?style={{
          ? ? ? ?background: "#000",
          ? ? ? ?height: "56px",
          ? ? ? ?display: "flex",
          ? ? ? ?alignItems: "center"
          ? ? ?}}
          ? ?>

          ? ? ?<AtmosphereBubbleSequence
          ? ? ? ?bubbleList={bubbleDataList.map((data) =>
          (
          ? ? ? ? ?<Bubble data={data} />
          ? ? ? ?))}
          ? ? ? ?index={index}
          ? ? ? ?setCurIndex={setIndex}
          ? ? ? ?nextBubble={nextBubble}
          ? ? ?/>
          ? ?div>

          ?);
          }

          接著來看看效果:

          很好,已經(jīng)實(shí)現(xiàn)我們想要的效果了!

          4. 總結(jié)

          在本文我們接觸到了 swiper 和 react-transition-group 的使用,并分別用它們實(shí)現(xiàn)了氛圍氣泡需求。

          4.1. 動(dòng)畫效果層面的對(duì)比

          • react-transition-group 更加靈活,針對(duì)組件過渡的動(dòng)畫效果有更廣泛的應(yīng)用場(chǎng)景。

          • swiper 作為輪播效果組件,它受限于前后幻燈片同時(shí)存在的這一問題,在氛圍氣泡需求中表現(xiàn)不是很好。

          4.2. 狀態(tài)管理層面的對(duì)比

          雖然 swiper 有這樣的局限性,但這一問題并不是不能解決的,還是有 hack 技巧的。比如說,可以先把 swiper-container 先漸隱,再觸發(fā)幻燈片切換,并在中途增加動(dòng)畫類實(shí)現(xiàn)漸現(xiàn)。只是說這段 js 邏輯不太優(yōu)雅而已:

          componentDidMount() {
          ?const { next, setBubbleList } = this.props;
          ?const swiperIns = this.swiper;
          ?const interval = setInterval(() => {
          ? ?swiperIns.$wrapperEl?.addClass('swiperFadeOut');
          ? ?delay(300)
          ? ? ?.then(() => {
          ? ? ? ?if (swiperIns.activeIndex === 0) {
          ? ? ? ? ?clearInterval(interval);
          ? ? ? ? ?next(2);
          ? ? ? ? ?setBubbleList();
          ? ? ? ? ?thrownewError('退出動(dòng)畫');
          ? ? ? ?}
          ? ? ? ?// 設(shè)置上一個(gè)滑塊為透明隱藏
          ? ? ? ?(swiperIns.slides[swiperIns.activeIndex] as HTMLElement).style.opacity = '0';
          ? ? ? ?swiperIns.slidePrev(500);
          ? ? ? ?return delay(300);
          ? ? ?})
          ? ? ?.then(() => {
          ? ? ? ?swiperIns.$wrapperEl.addClass('swiperFadeIn');
          ? ? ? ?return delay(200);
          ? ? ?})
          ? ? ?.then(() => {
          ? ? ? ?swiperIns.$wrapperEl.removeClass('swiperFadeOut');
          ? ? ? ?swiperIns.$wrapperEl.removeClass('swiperFadeIn');
          ? ? ?})
          ? ? ?.catch((error) => {
          ? ? ? ?if (error.message === '退出動(dòng)畫') {
          ? ? ? ? ?console.log('已退出');
          ? ? ? ?}
          ? ? ?});
          ?}, 2000);
          }

          由此也可以發(fā)現(xiàn),在狀態(tài)管理方面:

          • react-transition-group 可以讓我們大膽地設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu),來管理組件的過渡狀態(tài)。

          • swiper 相對(duì)不太適合 react 的狀態(tài)管理,在需要?jiǎng)討B(tài)增刪幻燈片的場(chǎng)景,它依賴于實(shí)例方法,不易做到數(shù)據(jù)同步。

          4.3. 方案選擇

          面對(duì)類似氛圍氣泡的需求,如何選擇 swiper 和 react-transition-group 這兩類實(shí)現(xiàn)方案?

          其實(shí)只要觀察,數(shù)據(jù)列表的長度是靜態(tài)的,還是會(huì)動(dòng)態(tài)改變的。

          • 靜態(tài):使用幻燈片組件,如 swiper

          • 動(dòng)態(tài):使用 react 生態(tài)的組件,如 react-transition-group

          其中原因,相信你已經(jīng)有所理解~

          5. CodeSandbox demo




          緊追技術(shù)前沿,深挖專業(yè)領(lǐng)域
          掃碼關(guān)注我們吧!
          瀏覽 39
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  青娱乐精 | 一起草成人视频 | 91麻豆精品国产91久久久 | 亚洲高清无码在线观看视频 | 一级骚逼 |