React?懸浮框內(nèi)容懶加載實例詳解
界面隱藏
一個容器放置視頻,默認(rèn)情況下
display: none; z-index: 0; transform: transform3d(10000px, true_y, true_z);
y軸和z軸左邊都是真實的(騰訊視頻使用絕對定位,因此是計算得到的),只是將其移到右邊很遠(yuǎn)的距離。
懶加載
React監(jiān)聽鼠標(biāo)移入(獲取坐標(biāo))
- 添加事件監(jiān)聽
onMouseEnter={(e) => { handleMouseEnter(e) }}
const handleMouseEnter = (e: React.MouseEvent) => {
console.log(e.target)
}
注意事件類型是React.MouseEvent。

typescript中HTMLElement 和 Element的區(qū)別
ts中:
let res =document.getElementById('test'); //HTMLElement
let el = document.querySelector('#test'); // Element
mdn中: querySelector,getElementById兩者均返回Element。
Element 是一個通用性非常強的基類,所有 Document 對象下的對象都繼承自它。這個接口描述了所有相同種類的元素所普遍具有的方法和屬性。一些接口繼承自 Element 并且增加了一些額外功能的接口描述了具體的行為。
例如, HTMLElement 接口是所有 HTML 元素的基本接口,而 SVGElement 接口是所有 SVG 元素的基礎(chǔ)。大多數(shù)功能是在這個類的更深層級(hierarchy)的接口中被進一步制定的。
實現(xiàn):
function getElementAbsPos(e: HTMLElement) {
var t = e!.offsetTop;
var l = e!.offsetLeft;
while (e = e!.offsetParent as HTMLElement) {
t += e.offsetTop;
l += e.offsetLeft;
}
return { left: l, top: t };
}
React實現(xiàn)
在騰訊視頻中,懸浮框是處于頂層div下的,因此使用絕對定位(絕對定位是相當(dāng)與父節(jié)點的,并不是document)。
在React中,由于我們將展示視頻信息的這個Item組件化了,因此實現(xiàn)思路有一點改變:
- 每個Item都有一個對應(yīng)的懸浮框DIV,默認(rèn)情況
hidden; - 為了節(jié)省流量,懸浮框內(nèi)的內(nèi)容需要懶加載;
- 顯示懸浮框的時機是一致的——鼠標(biāo)移入時,為了優(yōu)化體驗,節(jié)省流量,可以設(shè)定為移入一段時機后才顯示;
原始代碼
import { Card } from 'antd';
import { Content } from 'antd/lib/layout/layout';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom'
import styles from './css/VideoItem.module.css'
interface VideoItemProps {
video: Video,
topCategory?: string,
subCategory?: string,
}
export default function VideoItem(props: VideoItemProps) {
const { video, topCategory, subCategory } = props
const navigate = useNavigate()
const [loading, setLoding] = useState(false)
const to = (() => {
let itemTop = Object.getOwnPropertyNames(video.category)[0]
let itemSub = video.category[itemTop].length ? video.category[itemTop][0] : ''
if (topCategory) {
itemTop = topCategory
itemSub = ''
if (subCategory) {
itemSub = subCategory
}
}
if (itemSub) {
return `/detail/${itemTop}/${itemSub}/${video.id}`
} else {
return `/detail/${itemTop}/${video.id}`
}
})()
return (
<NavLink to={to}>
<Card hoverable
bordered={false}
style={{ width: 180, height: 280, overflow: 'hidden' }}
bodyStyle={{ padding: 4 }}
className={styles.video}
cover={<img style={{ width: '180px', height: '230px' }} alt={video.title} src={video.poster} />}
onMouseOver={() => { }}
onClick={() => handleClick()}
>
<div style={{ height: 280 }}>
{video.title}
</div>
</Card>
</NavLink>
)
}
handleClick響應(yīng)點擊事件,跳轉(zhuǎn)到視頻詳情頁,以上代碼還不含與本文相關(guān)內(nèi)容。
放入新的DIV
<NavLink to={to}>
<Card
bordered={false}
bodyStyle={{ padding: 4 }}
className={styles.video}
cover={<img alt={video.title} src={video.poster} />}
>
<div className={styles.title}>
{video.title}
</div>
</Card>
<Card hoverable
bordered={false}
style={{
backgroundColor: 'pink',
display: hiddenDetail ? 'none' : 'inline-block',
position: 'absolute',
transform: `translate3d(0px, -100%, 0px)`,
}}
bodyStyle={{ padding: 4 }}
className={styles.video}
cover={<img alt={video.title} src={'占位圖鏈接'} />}
>
<div className={styles.title}>
{video.title}
</div>
</Card>
</NavLink>
狀態(tài)設(shè)置
加入狀態(tài)表示是否隱藏懸浮框:
默認(rèn)隱藏
const [hiddenDetail, setHiddenDetail] = useState(true)
樣式設(shè)置
style={{
backgroundColor: 'pink',
display: hiddenDetail ? 'none' : 'inline-block',
position: 'absolute',
transform: `translate3d(0px, -100%, 0px)`,
}}
兩個Card組件的寬度和高度已經(jīng)設(shè)為一致,為了方便調(diào)試,將懸浮框的背景設(shè)為粉色;
使用絕對定位,讓其能夠覆蓋原始信息;
通過transform改變懸浮框的位置,不設(shè)置的話,懸浮框默認(rèn)被擠到下方,-100%表示在y軸上向上移動懸浮框高度對應(yīng)的像素,由于兩個Card組件高度相同,因此可以覆蓋原始信息。
事件設(shè)置
第一個Card,即默認(rèn)顯示的元素,添加鼠標(biāo)移入事件:
onMouseEnter={(e) => {
setHiddenDetail(!hiddenDetail)
}}
第二個Card,即懸浮框,添加鼠標(biāo)移出事件:
onMouseLeave={(e) => {
// bug 向下移出不會觸發(fā)
// 因為移入了底層Card,執(zhí)行了setHiddenDetail(false)
// 將移入事件改為 setHiddenDetail(!hiddenDetail)
setHiddenDetail(true)
}}
這里我們使用!hiddenDetail,而不是直接設(shè)為true,
因為如果底層DIV大于懸浮框的框的話,在懸浮框顯示的情況下,如果移出過程進入了底層DIV,會導(dǎo)致懸浮框不會消失(雖然移出過程觸發(fā)了onMouseLeave,將狀態(tài)設(shè)為false,但移入底層DIV后,再次觸發(fā)onMouseEnter,將狀態(tài)設(shè)為true),這主要是應(yīng)對懸浮框沒有完全覆蓋底層元素的情況。

事件優(yōu)化
延遲顯示懸浮框
在底層元素的事件響應(yīng)中:
onMouseEnter={(e) => { setHiddenDetail(!hiddenDetail)}}
將狀態(tài)改變?nèi)蝿?wù)用Timeout包裹,設(shè)定延時t,如果在移出該元素時,定時器還沒有結(jié)束,則結(jié)束該定時器:
let loadDetailJob: NodeJS.Timeout | null = null
<Card
bordered={false}
bodyStyle={{ padding: 4 }}
className={styles.video}
cover={<img alt={video.title} src={video.poster} />}
onMouseEnter={(e) => {
loadDetailJob = setTimeout(() => {
setHiddenDetail(!hiddenDetail)
}, 500)
}}
onMouseLeave={(e) => {
if (loadDetailJob) {
clearTimeout(loadDetailJob)
}
}}
>
<div className={styles.title}>
{video.title}
</div>
</Card>
懸浮框內(nèi)容懶加載
在騰訊視頻中,懸浮框顯示一小段視頻,但是一個頁面中包含多個懸浮框,如果一次全部加載這些資源,會造成比較大的流量浪費,因此,最后是要顯示懸浮框時,才加載詳細(xì)內(nèi)容。
在本示例中,我們懸浮框顯示的圖片設(shè)為懶加載模式,我們需要增加一個狀態(tài)firstLoad記錄是否是第一次顯示懸浮框,如果是第一次,則設(shè)一個定時器模擬發(fā)送請求,獲取詳細(xì)內(nèi)容的鏈接。另一種情況是,在知道鏈接地址的情況下,不發(fā)送請求,將元素的src指向更高為正確的就行。
為了方便操作DOM元素,我們創(chuàng)建一個懸浮框的ref對象:detailRef。
const [firstLoad, setFirstLoad] = useState(true)
const detailRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
// 第一次加載懸浮框,并且懸浮框狀態(tài)為顯示
if (firstLoad && !hiddenDetail) {
// 在知道路徑的情況下,可以直接修改路徑,Promise用于模擬向服務(wù)器發(fā)送請求的等待過程
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('load success')
}, 1000)
}).then(() => {
setFirstLoad(false)
detailRef.current!.querySelector('img')!.src = 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
})
}
}, [hiddenDetail])
完整代碼
import { Card } from 'antd';
import { Content } from 'antd/lib/layout/layout';
import { useEffect, useRef, useState } from 'react';
import { Image } from 'antd'
import { NavLink, useNavigate } from 'react-router-dom'
import styles from './css/VideoItem.module.css'
interface VideoItemProps {
video: Video,
topCategory?: string,
subCategory?: string,
}
function getElementAbsPos(e: HTMLElement) {
var t = e!.offsetTop;
var l = e!.offsetLeft;
while (e = e!.offsetParent as HTMLElement) {
t += e.offsetTop;
l += e.offsetLeft;
}
return { left: l, top: t };
}
export default function VideoItem(props: VideoItemProps) {
const { video, topCategory, subCategory } = props
const navigate = useNavigate()
const [hiddenDetail, setHiddenDetail] = useState(true)
const [firstLoad, setFirstLoad] = useState(true)
const detailRef = useRef<HTMLDivElement | null>(null)
let loadDetailJob: NodeJS.Timeout | null = null
const to = (() => {
let itemTop = Object.getOwnPropertyNames(video.category)[0]
let itemSub = video.category[itemTop].length ? video.category[itemTop][0] : ''
if (topCategory) {
itemTop = topCategory
itemSub = ''
if (subCategory) {
itemSub = subCategory
}
}
if (itemSub) {
return `/detail/${itemTop}/${itemSub}/${video.id}`
} else {
return `/detail/${itemTop}/${video.id}`
}
})()
useEffect(() => {
if (firstLoad && !hiddenDetail) {
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('load success')
}, 1000)
}).then(() => {
setFirstLoad(false)
detailRef.current!.querySelector('img')!.src = 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
})
}
}, [hiddenDetail])
return (
<NavLink to={to}>
<Card
bordered={false}
bodyStyle={{ padding: 4 }}
className={styles.video}
cover={<img alt={video.title} src={video.poster} />}
onMouseEnter={(e) => {
loadDetailJob = setTimeout(() => {
setHiddenDetail(!hiddenDetail)
}, 500)
}}
onMouseLeave={(e) => {
if (loadDetailJob) {
clearTimeout(loadDetailJob)
}
}}
>
<div className={styles.title}>
{video.title}
</div>
</Card>
<Card hoverable
bordered={false}
loading={firstLoad}
ref={(c) => { detailRef.current = c }}
style={{
backgroundColor: 'pink',
display: hiddenDetail ? 'none' : 'inline-block',
position: 'absolute',
transform: `translate3d(0px, -100%, 0px)`,
}}
bodyStyle={{ padding: 4 }}
className={styles.video}
cover={<img alt={video.title} src={'占位圖片鏈接'} />}
onMouseLeave={(e) => {
// bug 向下移出不會觸發(fā)
// 因為移入了底層Card,執(zhí)行了setHiddenDetail(false)
// 將移入事件改為 setHiddenDetail(!hiddenDetail)
setHiddenDetail(true)
}}
>
<div className={styles.title}>
{video.title}
</div>
</Card>
</NavLink>
)
}以上就是React 懸浮框內(nèi)容懶加載實例詳解的詳細(xì)內(nèi)容,更多關(guān)于React 懸浮框內(nèi)容懶加載的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
關(guān)于useEffect的第二個參數(shù)解讀
這篇文章主要介紹了關(guān)于useEffect的第二個參數(shù),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09
詳解React如何實現(xiàn)代碼分割Code Splitting
這篇文章主要為大家介紹了React如何實現(xiàn)代碼分割Code Splitting示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08
詳解React中錯誤邊界的原理實現(xiàn)與應(yīng)用
在React中,錯誤邊界是一種特殊的組件,用于捕獲其子組件樹中發(fā)生的JavaScript錯誤,并防止這些錯誤冒泡至更高層,導(dǎo)致整個應(yīng)用崩潰,下面我們就來看看它的具體應(yīng)用吧2024-03-03

