短影片無盡流前端開發指南

語言: CN / TW / HK

本文基於對家裝家居內容短影片無盡流的開發實踐,總結出了一套適應於該場景及衍生場景的前端開發指南,通過閱讀本文可以快速瞭解短影片無盡流的 前端開發。

前言

短影片無盡流是當下比較熱門的一種業務場景,在日常生活中隨處可見。本文基於對家裝家居內容短影片無盡流的開發實踐,總結出了一套適應於該場景及衍生場景的前端開發指南,通過閱讀本文可以快速瞭解短影片無盡流的前端開發。

短影片無盡流介紹

短影片有著“短、平、快”的特點,使用者可以通過短影片快速獲得一些輸入。在家裝家居領域,可以通過幾十秒到幾分鐘的短影片向用戶輸出裝修乾貨經驗、居家好物推薦等等。在短影片無盡流場景中,會基於引流內容以及相關演算法推薦出更多內容,使用者隨著手勢上滑可以持續瀏覽獲得內容輸入。

短影片無盡流結構拆解

短影片無盡流從結構上可以拆解為兩層:滑動輪播容器、單張內容卡片。單張內容卡片又可以拆解為自定義控制欄的影片播放器(下文簡稱影片播放器)和內容相關資訊兩部分。

內容相關資訊為業務呈現模組,不同的業務有各自的表達方式,本文不對該部分展開介紹。下面將基於react介紹滑動輪播容器和影片播放器的開發指南。

   影片播放器

家裝家居內容短影片無盡流使用的是淘寶App內建的同層渲染播放器(VideoX橋接),本文為了增強普適性,直接採用HTML5 video標籤作為播放器來介紹。

播放器自身的控制欄樣式比較單一,往往不能滿足業務訴求,需要實現自定義的控制欄 。本小節將介紹如何實現播放器狀態按鈕和播放器進度條,以及播放器的啟用和銷燬,為應用在滑動輪播容器做前置準備。

  • 影片播放器狀態按鈕

常規來講播放器需要展示出兩種狀態:暫停中、緩衝中,播放中有進度條在推進一般不需要做額外展示。狀態按鈕元件實現如下:

tips:將按鈕狀態內建在元件中,暴露修改狀態的方法給父元素,可以避免在改變按鈕狀態時觸發父元素的re-render。

// ...
const StatusButton = forwardRef<IStatusButtonRef>((_, ref) => {
const [status, setStatus] = useState<EStatus>(EStatus.PLAY);


useImperativeHandle(ref, () => ({
setStatus,
}));


return (
<div className={styles.statusButton}>
{(() => {
switch (status) {
case EStatus.PAUSE:
return <div>{/* 暫停Icon */}</div>;
case EStatus.WAITING:
return <div>{/* 緩衝Icon */}</div>;
default:
return null;
}
})()}
</div>
);
});


export default memo(StatusButton);


// ...
const VideoPlayer: FC<IVideoPlayer> = (props) => {
const { source } = props;


const videoPlayerRef = useRef<HTMLVideoElement | null>(null);
const statusButtonRef = useRef<IStatusButtonRef | null>(null);


const onPlay = () => {
statusButtonRef.current?.setStatus(EPlayerStatus.PLAY);
};


useEffect(() => {
videoPlayerRef.current?.addEventListener('play', onPlay);
// 暫停(pause)和緩衝(waiting)監聽方法類似
// ...


return () => {
videoPlayerRef.current?.removeEventListener('play', onPlay);
};
}, []);


return (
<div className={styles.videoPlayerContainer}>
{/* 播放器 */}
<video
ref={videoPlayerRef}
className={styles.item}
src={source}
playsInline
autoPlay
/>
{/* 播放器狀態按鈕 */}
<StatusButton ref={statusButtonRef} />
</div>
);
};


export default memo(VideoPlayer);
  • 影片播放器進度條

有兩種情況會引起進度條“走動”:

  1. 影片正常播放,進度更新。

  2. 使用者手動拖拽進度條。

對於1,進度條元件對父元素暴露更新進度的方法,父元素監聽到播放器 timeupdate 時去呼叫該方法即可。

對於2,可以使用 @use-gesture/vanilla 實現跟手的拖拽效果,使用者停止拖拽時去做播放器的跳幀操作:

// ...
useEffect(() => {
const gesture = new DragGesture(
// 拖拽“點”
thumbRef.current,
(state) => {
if (state.first) {
setIsDragging(true);
}


const x = state.xy[0];


let walked: number;
// 判斷是否超出邊界
if (x < 0) {
walked = 0;
} else if (x > OVERALL_WIDTH) {
walked = OVERALL_WIDTH;
} else {
walked = x;
}


setCurrentWalked(walked);


if (state.last) {
// 使用者停止拖拽後,跳幀至當前時間
const duration = Math.ceil((walked / OVERALL_WIDTH) * maxDuration);
onChangeCurrentTime(duration);
setIsDragging(false);
}
},
{
axis: 'x',
pointer: { touch: true },
},
);


return () => {
gesture.destroy();
};
}, []);


return (
<div ref={thumbRef} />
);


  • 影片播放器啟用及銷燬

雖然該場景下存在n個內容卡片,但是我們只需要螢幕當中的那一個內容卡片渲染影片播放器,其餘內容卡片僅保留封面圖佔位即可,減少記憶體佔用。

// ...
const VideoPlayer = forwardRef<IVideoPlayerRef, IVideoPlayerProps>((props, ref) => {
// 播放器狀態 預設為非啟用狀態
const [activeStatus, setActiveStatus] = useState<boolean>(false);
// ...

/**
* 隱藏封面佔位
*/
const hidePoster = () => {
posterRef.current?.hide();
};


/**
* 啟用播放器
*/
const activate = () => {
setActiveStatus(true);
};
/**
* 銷燬播放器
*/
const inActivate = () => {
setActiveStatus(false);
// 銷燬播放器時展示封面佔位
posterRef.current?.show();
};


useImperativeHandle(ref, () => ({
activate,
inActivate,
}));


useEffect(() => {
// 監聽影片首幀載入完成時再去隱藏封面佔位,防止螢幕閃動
videoPlayerRef.current?.addEventListener('loadeddata', hidePoster);
// ...


return () => {
videoPlayerRef.current?.removeEventListener('loadeddata', hidePoster);
};
}, []);

// ...
});


export default memo(VideoPlayer);


   滑動輪播容器

對於滑動輪播容器,採用swiper實現。swiper是強大的輪播元件,有豐富的內建能力,封裝了react元件可以方便地使用。

  • 虛擬輪播

由於短影片無盡流有”無盡“的特性,使用者單次可能會瀏覽幾十篇內容,因此可以使用swiper的virtual能力減少記憶體佔用,會隨著手動輪播切換增刪節點,僅保留視角內上下有限個swiper slide節點。如下圖所示,當前需要用到500個slide,但是通過動態增刪節點保證實際渲染出的節點數最多隻有5個(個數可配置)。

swiper入參配置可參考:

// swiper 6.x版本 和 8.x版本 使用上會有一定區別,註釋中會將不同點標註出來
import * as React from 'react';


// [swiper 8.x] 引入 swiper
import { Virtual } from 'swiper';


// [swiper 6.x] 引入 swiper
// import SwiperCore, { Virtual } from 'swiper';


import { Swiper, SwiperSlide } from 'swiper/react';


import type { FC } from 'react';
import type { IVideoCardItem } from '../../types';


import styles from './index.module.less';


// [swiper 6.x] 載入 Virtual 模組
// SwiperCore.use([Virtual]);


const VideoSwiper: FC = () => {
return (
<Swiper
className={styles.videoSwiperContainer}
// 切換方向
direction="vertical"
// 初始索引
initialSlide={0}
// 切換角度,防止誤切
touchAngle={30}
// [swiper 8.x] 載入 Virtual 模組
modules={[Virtual]}
// virtual配置,如下配置會保證至多有5 slide
virtual={{
// 在activeslide前多渲染1slide
addSlidesBefore: 1,
// 在activeslide後多預渲染1slide
addSlidesAfter: 1,
}}
>
{/* ... */}
</Swiper>
);
};

tips:移動端swiper切換時可能存在閃屏/抖動的異常情況,可以使用如下程式碼開啟硬體加速,可以解決大部分異常情況。

.videoSwiperContainer :global {
.swiper-wrapper {
transform: translate3d(0, 0, 0);

.swiper-slide {
transform: translate3d(0, 0, 0);
}
}
}
  • 影片播放器例項管理

上述中提到只需要螢幕當中的那一個內容卡片渲染播放器,其餘展示封面圖佔位即可。在輪播容器完成一次切換即 onTransitionEnd  時銷燬上一個內容卡片的播放器,同時啟用當前內容卡片的播放器,保證始終只有一個播放器處於啟用狀態。

tips:當前內容卡片的播放器啟用後,由於還需要載入影片資源,因此切換後會有短暫的等待時間。為了提升使用者體驗,可以配合影片資源的預載入,優先使用端側提供的預載入能力,若沒有該支援,可以嘗試使用blob等預載入方案。

底部懸浮loading條

無盡流場景不可避免的是載入loading,對於全螢幕的輪播容器,loading的出現/消失儘量避免產生布局偏移,如果loading過程中使用者想去做一些點選操作,但剛好操作的瞬間loading結束了,若此時發生了佈局偏移,會造成使用者點到非預期的行動點,有損使用者體驗。可以採用類似此輕量級的懸浮式loading:

.loadingBar {
&Container {
position: fixed;
left: 0;
bottom: 0;
z-index: 99;
display: flex;
align-items: center;
justify-content: flex-end;
width: 100vw;
height: 5rpx;
}


&Item {
height: 5rpx;
background-color: #fd0;
animation: loading 0.6s linear infinite;
}
}


@keyframes loading {
0% {
width: 0;
opacity: 0;
}
30% {
width: 30vw;
opacity: 1;
}
70% {
width: 70vw;
opacity: 1;
}
100% {
width: 100vw;
opacity: 0;
}
}

總結

本文通過對短影片無盡流結構的拆解,從各個功能點的角度介紹瞭如何實現並落地該場景。除了短影片無盡流外,還適用於其衍生場景,如圖文卡片無盡流、直播無盡流、3D場景無盡流等等。針對於不同場景,不變的是輪播容器的構建,在此基礎上根據不同場景構建單個場景卡片的邏輯即可。

團隊介紹

我們是大淘寶-家裝家居技術-前端團隊,團隊支撐大淘寶家居家裝業務。旗下包括:每平每屋App、淘寶【極有家】頻道。我們連通電商平臺和商家店鋪,覆蓋居家生活、裝修設計、線下市場,3D場景化展現居家生活,我們力求讓每件單品都不再孤立呈現,置身其中,感受家的優選方案。期待與您一起共築美好的理想家。

✿    拓展閱讀

作者 | 胡少鵬(棣棠)

編輯| 橙子君