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

語言: 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] 引入 swiperimport { 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場景化展現居家生活,我們力求讓每件單品都不再孤立呈現,置身其中,感受家的優選方案。期待與您一起共築美好的理想家。


✿  拓展閱讀

作者| 胡少鵬(棣棠)
編輯| 橙子君

本文分享自微信公眾號 - 大淘寶技術(AlibabaMTT)。
如有侵權,請聯絡 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。