跳至主要内容

仿一個掘金APP會員等級輪播

· 閱讀時間約 5 分鐘
Imagine Chiu
Front End Engineer @ Bearests

使用的是我開源庫 bear-react-carousel

主要試試看能做到什麼程度,之前公司也有類似的需求

廢話不多說,直接開始

需求

首先我們先看掘金在APP端的預覽結果

Juejin member level carousel

  • 可滑動的部分只有 卡片的部分
  • 並且在滑動時等級卡片會往上移動
  • 等級卡片顯示為置中為 選取項目
  • 等級卡下方需要被遮住(圓弧),不顯示完全
  • 等級名稱項目需同步移動
  • 等級名稱項目移動時圓弧移動
  • 等級名稱線需同步移動
  • API 撈回時,預設需要移動到選取的等級(無動畫移動)

开发輪播項目

輪播項目可切分為:

  • 等級卡片,顯示1.2個並且置
<BearCarousel
slidesPerView={1.2}
spaceBetween={20}
isCenteredSlides={true}
// ...ignore
/>
  • 等級名稱
  • 水平線 兩者都應顯示 3 並居中。
<BearCarousel
slidesPerView={3}
isCenteredSlides={true}
// ...ignore
/>

所以開發目標是在移動等級卡片時同步等級名稱等級線的移動。

import React, {useCallback, useEffect, useRef, useState} from 'react';
import BearCarousel, {TMoveEffectFn, TBearSlideItemDataList, BearSlideCard, elClassName, Controller, TOnSlideChange} from 'bear-react-carousel';

const MemberLevelWrapper = () => {
const carouselMainRef = useRef<BearCarousel>(null);
const carouselMetaRef = useRef<BearCarousel>(null);
const carouselLineRef = useRef<BearCarousel>(null);

return <>
{/* Level Card */}
<BearCarousel
ref={carouselMainRef}
syncCarouselRefs={[carouselMetaRef, carouselLineRef]}
slidesPerView={1.2}
spaceBetween={20}
isCenteredSlides={true}
// ...ignore
/>

{/* Level Name*/}
<BearCarousel
ref={carouselMetaRef}
// ...ignore
/>

{/* Level Line */}
<BearCarousel
ref={carouselLineRef}
// ...ignore
/>
</>;
};

接下來,滾動時,等級卡片會向上移動,我們需要實現一個新的動畫效果功能

import BearCarousel, {TMoveEffectFn} from 'bear-react-carousel';

const mainMoveEffectFn: TMoveEffectFn = useCallback((percentageInfo) => {
const transformY = 40;
return {
transform: `translate(0px, ${-transformY * (percentageInfo.calcPercentage - 1)}px)`,
};
}, []);

<BearCarousel
ref={carouselMainRef}
syncCarouselRefs={[carouselMetaRef, carouselLineRef]}
moveEffect={{
moveFn: mainMoveEffectFn,
}}
// ...ignore
/>

所以開發目標是 移動等級卡時,同步移動 等級名稱與等級線

const levelNameMoveEffectFn: TMoveEffectFn = useCallback((percentageInfo) => {
const transformY = -19;
return {
transform: `translate(0px, ${-transformY * (percentageInfo.calcPercentage - 1)}px)`,
};
}, []);

<BearCarousel
ref={carouselMainRef}
moveEffect={{
moveFn: levelNameMoveEffectFn,
}}
// ...ignore
/>

至於圓弧的部分,我們最後使用遮罩的方式來顯示,並且關閉 NavButton、Pagination、MouseMove,畫SVG不熟的話,可以使用這個工具

svg-path-editor

SVG Mask for level line

<LevelLine>
<LineBearCarousel
ref={carouselLineRef}
data={lineData}
slidesPerView={3}
isCenteredSlides={true}
isEnableNavButton={false}
isEnablePagination={false}
isEnableMouseMove={false}
/>

<svg height="100%" width="100%">
<clipPath id="wave12">
{/*跟隨線*/}
<path d="M 0 4 C 175 30 175 30 356 4 L 356 2 C 175 28 175 28 0 2" stroke="black" fill="transparent"/>
</clipPath>
</svg>
</LevelLine>

SVG Mask Result fro level line

而 Level Card 遮罩的部分一樣,但我用了3塊

SVG Mask 1 for Level Card SVG Mask 2 for Level Card SVG Mask 3 for Level Card

<svg height="100%" width="100%">
<clipPath id="wave10">
<path d="M 0,0 356,0 356,130 0,130" stroke="black" fill="transparent"/>
{/* 圓弧 */}
<path d="M 0 130 C 175 155 175 155 356 130" stroke="black" fill="transparent"/>
{/* 下箭頭 */}
<path d="M 152 143 L 176 153 L 178 153 L 202 143" stroke="black" fill="transparent"/>
</clipPath>
</svg>

SVG Mask Result for Level Card

最後就是 預設選取等級的部分(無動畫方式移動)

const [carouselMainController, setMainController] = useState<Controller>();
const [currLevel, setCurrLevel] = useState<{lv: number,count: number}|undefined>();


useEffect(() => {
carouselMainController?.slideToPage(5, false);
}, [carouselMainController]);


<BearCarousel
ref={carouselMainRef}
syncCarouselRefs={[carouselMetaRef]}
onSlideChange={handleSlideChange}
setController={setMainController}
slidesPerView={1.2}
spaceBetween={20}
isCenteredSlides={true}
// ...ignore
/>

完成結果

Finished Demo

以上就完成了,基本上如果兩邊都需要移動的話,互相同步在(bear-react-carousel)[https://bear-react-carousel.pages.dev] 因為是進行獨立的同步控制,所以不會循環互控,但也因為這樣所以目前是無法A控制B , B 自動再控制C,所以在這邊可以發現是A控制B和C。

About SSR

· 閱讀時間約 2 分鐘
Imagine Chiu
Front End Engineer @ Bearests

這幾天將一些範例更新到首頁上,並透過 SSG 放到 Cloudflare,發現了一些奇怪的問題,在 開發模式中就好好的,但是 page build 上到雲端上,就不如預期

關於SSR在Carousel中的影響

  • 使用到 window 會報錯,因為在 Nodejs 中沒有這個變數
  • 避免 SSR在CSR進行水合時,UI 渲染錯誤
  • 原本使用 ulid 綁定在 中,在插入 標籤指定id bear-carousel_id 的樣式,但若元件重新渲染則會導致 產生新的id,變成樣式設定不到
Error: Hydration failed because the initial UI does not match what was rendered on the server.

做了哪些改動

  • 將 window 的部分改為 GlobalThis.window,無法使用在建構子進行跟 window 有關的項目,所以會有一些邏輯拆分的情況異動,使用 useState 在 useEffect 進行更新狀態來判斷 是CSR (如果是 class component 則是 this.setState 和 componentDidMount),並且將會使用到 window 的部分加上 isClientOnly ce0537c4
  • 不使用 id 的方式設定樣式,改為 css 變數的方式讓 style css file 使用,並刪除對 ulid 的依賴 9b438c3

懶加載圖片

· 閱讀時間約 1 分鐘
Imagine Chiu
Front End Engineer @ Bearests

一般輪播的情況,當頁面載入圖片就會直接被載入,一但如果圖片過多的時候,不但會佔用 瀏覽器下載的線程,還會讓後面更需要優先被使用的資源更慢被加載

一般情況加載圖片

不正常的情況

img 標籤沒有設定渲染樣式 (width, border 之類的),導致觀察者無法判別進入到畫面中

所以在 這個 e4e0abf 中進行了修改

使用 Lazy Load

這個時候我們就會需要使用 Lazy load,也就是懶加載,也可以說是 延遲加載

將 isLazy 功能打開即可,參考文件

Lazy laod image

我們可以適當的使用 Lazy load 來優化你的網站

關於 Swiper 設計方式和循環模式

· 閱讀時間約 4 分鐘
Imagine Chiu
Front End Engineer @ Bearests

研究輪播機制系列

swiper banner

因為前陣子開發文字動態效果,在Clone項目上需要重置時, 會導致動畫無法連貫,所以就想說來看看目前的 Swiper 有沒有這個問題。

於事,發現了他已經不使用 Clone 的方式了。

一般情況

Normal State

可以看到,一般情況下,該方法類似於實現輪播的一般方法。

遇到需要 Clone 的情況

Need Loop State

我們從 aria-label 中的信息可以看出,active item 被移動到第一個位置,然後序列繼續

對於每一個動作,我們只需要激活 transition-duration 動畫

運動完成後,動畫停用,運動控制應鎖定,直到運動完成

但實際上是

// active 5 (is last)
1 / 5 (-1496 * 0)
2 / 5 (-1496 * 1)
3 / 5 (-1496 * 2)
4 / 5 (-1496 * 3)
5 / 5 (-1496 * 4)

// click next, active 1
// then duration 0ms, transform: -4488px,
// then duration: 900ms, transform: -5984px,
// move
2 / 5 (0)
3 / 5 (-1496px)
4 / 5 (-2992px)
5 / 5 (-4488px)
1 / 5 (-5984px)
// then duration 0ms


// click next, active 2
// then duration 0ms, transform: -4488px,
// then duration: 900ms, transform: -5984px,
3 / 5 (0)
4 / 5 (-1496px)
5 / 5 (-2992px)
1 / 5 (-4488px)
2 / 5 (-5984px)
// then duration 0ms

因為速度太快看不出破綻,下面是大概的重現方式

containerEl.style.transform = `translate3d(${-1904}px, 0px, 0px)`;
containerEl.style.transitionDuration = '0ms';
setTimeout(() => {
containerEl.style.transform = `translate3d(${-3808}px, 0px, 0px)`;
containerEl.style.transitionDuration = `${this._configurator.setting.moveTime}ms`;
}, 0);

於是補上將下一個目標序列移動到最後一個,然後跳轉到上一個位置,再移動到最後一個 排序變了,就變成了判斷是最後還是第一個,按順序判斷


可以解決什麼問題

我本來以為要循環,必須複製然後立即替換和重置,通過這樣做,我們可以避免與額外複製相關的問題以及輪播項目不同的情況

每次都需要創建新輪播項目(依照需要復制的數量),通常是輪播項目中有動態效果時比較容易受到影響

會產生什麼問題

Loop mode, total item 5, slidePreView 3

  • 因為改變數組順序需要重新渲染 (值得思考的是,如果真的改變了數組的順序,可以考慮是否使用css order來達到同樣的效果)

  • 當數量無法整除移動群數時,當數量無法整除移動群數時,順序切換補間會出現破綻

  • 手動滑動的時候,或者數量不夠的時候,很難判斷切換順序是依照 開頭還是結尾

  • 第一頁指定移動到最後一頁的時候,很難瞬間在 動畫移動中切換順序(除非使用 requestAnimationFrame)

Tip

到目前為止,使用克隆可能仍然是利大於弊。

Ref

測試不使用Clone的分支