短影片無盡流前端開發指南
本文基於對家裝家居內容短影片無盡流的開發實踐,總結出了一套適應於該場景及衍生場景的前端開發指南,通過閱讀本文可以快速瞭解短影片無盡流的 前端開發。
前言
短影片無盡流是當下比較熱門的一種業務場景,在日常生活中隨處可見。本文基於對家裝家居內容短影片無盡流的開發實踐,總結出了一套適應於該場景及衍生場景的前端開發指南,通過閱讀本文可以快速瞭解短影片無盡流的前端開發。
短影片無盡流介紹
短影片有著“短、平、快”的特點,使用者可以通過短影片快速獲得一些輸入。在家裝家居領域,可以通過幾十秒到幾分鐘的短影片向用戶輸出裝修乾貨經驗、居家好物推薦等等。在短影片無盡流場景中,會基於引流內容以及相關演算法推薦出更多內容,使用者隨著手勢上滑可以持續瀏覽獲得內容輸入。
短影片無盡流結構拆解
短影片無盡流從結構上可以拆解為兩層:滑動輪播容器、單張內容卡片。單張內容卡片又可以拆解為自定義控制欄的影片播放器(下文簡稱影片播放器)和內容相關資訊兩部分。
內容相關資訊為業務呈現模組,不同的業務有各自的表達方式,本文不對該部分展開介紹。下面將基於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,進度條元件對父元素暴露更新進度的方法,父元素監聽到播放器 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={{
// 在active的slide前多渲染1個slide
addSlidesBefore: 1,
// 在active的slide後多預渲染1個slide
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場景化展現居家生活,我們力求讓每件單品都不再孤立呈現,置身其中,感受家的優選方案。期待與您一起共築美好的理想家。
✿ 拓展閱讀
作者 | 胡少鵬(棣棠)
編輯| 橙子君
- 第14個天貓雙11,技術創新帶來消費新體
- 如何避免寫重複程式碼:善用抽象和組合
- Lath(純前端容器)打造頁面間的無縫平滑連線體驗
- stream的實用方法和注意事項
- 淺析設計模式1 —— 工廠模式
- 跟蹤元素可視?試試Intersection Observer
- 談一談 build-scripts 架構設計
- mysql懸掛事務問題
- 一次單元測試優化的過程總結
- 前端腳手架開發入門
- 進入 WebXR 的世界
- 淘寶逛逛ODL模型優化總結
- ODPS SQL優化總結
- 一次日常需求處理帶給我的思考
- 告別BeanUtils,Mapstruct從入門到精通
- 2022淘寶造物節3D直播虛擬營地技術亮點揭祕
- 連續遷移學習跨域推薦排序模型在淘寶推薦系統的應用
- 大淘寶技術行業FaaS化實戰經驗分享
- 如何根治 Script Error?
- 響應式程式設計的複雜度和簡化