React?懸浮框內(nèi)容懶加載實(shí)例詳解
界面隱藏
一個(gè)容器放置視頻,默認(rèn)情況下
display: none; z-index: 0; transform: transform3d(10000px, true_y, true_z);
y
軸和z
軸左邊都是真實(shí)的(騰訊視頻使用絕對(duì)定位,因此是計(jì)算得到的),只是將其移到右邊很遠(yuǎn)的距離。
懶加載
React監(jiān)聽(tīng)鼠標(biāo)移入(獲取坐標(biāo))
- 添加事件監(jiān)聽(tīng)
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 是一個(gè)通用性非常強(qiáng)的基類,所有 Document 對(duì)象下的對(duì)象都繼承自它。這個(gè)接口描述了所有相同種類的元素所普遍具有的方法和屬性。一些接口繼承自 Element 并且增加了一些額外功能的接口描述了具體的行為。
例如, HTMLElement 接口是所有 HTML 元素的基本接口,而 SVGElement 接口是所有 SVG 元素的基礎(chǔ)。大多數(shù)功能是在這個(gè)類的更深層級(jí)(hierarchy)的接口中被進(jìn)一步制定的。
實(shí)現(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實(shí)現(xiàn)
在騰訊視頻中,懸浮框是處于頂層div下的,因此使用絕對(duì)定位(絕對(duì)定位是相當(dāng)與父節(jié)點(diǎn)的,并不是document)。
在React中,由于我們將展示視頻信息的這個(gè)Item組件化了,因此實(shí)現(xiàn)思路有一點(diǎn)改變:
- 每個(gè)Item都有一個(gè)對(duì)應(yīng)的懸浮框DIV,默認(rèn)情況
hidden
; - 為了節(jié)省流量,懸浮框內(nèi)的內(nèi)容需要懶加載;
- 顯示懸浮框的時(shí)機(jī)是一致的——鼠標(biāo)移入時(shí),為了優(yōu)化體驗(yàn),節(jié)省流量,可以設(shè)定為移入一段時(shí)機(jī)后才顯示;
原始代碼
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)點(diǎn)擊事件,跳轉(zhuǎn)到視頻詳情頁(yè),以上代碼還不含與本文相關(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)`, }}
兩個(gè)Card
組件的寬度和高度已經(jīng)設(shè)為一致,為了方便調(diào)試,將懸浮框的背景設(shè)為粉色;
使用絕對(duì)定位,讓其能夠覆蓋原始信息;
通過(guò)transform
改變懸浮框的位置,不設(shè)置的話,懸浮框默認(rèn)被擠到下方,-100%
表示在y
軸上向上移動(dòng)懸浮框高度對(duì)應(yīng)的像素,由于兩個(gè)Card
組件高度相同,因此可以覆蓋原始信息。
事件設(shè)置
第一個(gè)Card
,即默認(rèn)顯示的元素,添加鼠標(biāo)移入事件:
onMouseEnter={(e) => { setHiddenDetail(!hiddenDetail) }}
第二個(gè)Card
,即懸浮框,添加鼠標(biāo)移出事件:
onMouseLeave={(e) => { // bug 向下移出不會(huì)觸發(fā) // 因?yàn)橐迫肓说讓覥ard,執(zhí)行了setHiddenDetail(false) // 將移入事件改為 setHiddenDetail(!hiddenDetail) setHiddenDetail(true) }}
這里我們使用!hiddenDetail
,而不是直接設(shè)為true
,
因?yàn)槿绻讓?code>DIV大于懸浮框的框的話,在懸浮框顯示的情況下,如果移出過(guò)程進(jìn)入了底層DIV
,會(huì)導(dǎo)致懸浮框不會(huì)消失(雖然移出過(guò)程觸發(fā)了onMouseLeave
,將狀態(tài)設(shè)為false,但移入底層DIV
后,再次觸發(fā)onMouseEnter
,將狀態(tài)設(shè)為true
),這主要是應(yīng)對(duì)懸浮框沒(méi)有完全覆蓋底層元素的情況。
事件優(yōu)化
延遲顯示懸浮框
在底層元素的事件響應(yīng)中:
onMouseEnter={(e) => { setHiddenDetail(!hiddenDetail)}}
將狀態(tài)改變?nèi)蝿?wù)用Timeout
包裹,設(shè)定延時(shí)t
,如果在移出該元素時(shí),定時(shí)器還沒(méi)有結(jié)束,則結(jié)束該定時(shí)器:
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)容懶加載
在騰訊視頻中,懸浮框顯示一小段視頻,但是一個(gè)頁(yè)面中包含多個(gè)懸浮框,如果一次全部加載這些資源,會(huì)造成比較大的流量浪費(fèi),因此,最后是要顯示懸浮框時(shí),才加載詳細(xì)內(nèi)容。
在本示例中,我們懸浮框顯示的圖片設(shè)為懶加載模式,我們需要增加一個(gè)狀態(tài)firstLoad
記錄是否是第一次顯示懸浮框,如果是第一次,則設(shè)一個(gè)定時(shí)器模擬發(fā)送請(qǐng)求,獲取詳細(xì)內(nèi)容的鏈接。另一種情況是,在知道鏈接地址的情況下,不發(fā)送請(qǐng)求,將元素的src
指向更高為正確的就行。
為了方便操作DOM
元素,我們創(chuàng)建一個(gè)懸浮框的ref
對(duì)象:detailRef
。
const [firstLoad, setFirstLoad] = useState(true) const detailRef = useRef<HTMLDivElement | null>(null) useEffect(() => { // 第一次加載懸浮框,并且懸浮框狀態(tài)為顯示 if (firstLoad && !hiddenDetail) { // 在知道路徑的情況下,可以直接修改路徑,Promise用于模擬向服務(wù)器發(fā)送請(qǐng)求的等待過(guò)程 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 向下移出不會(huì)觸發(fā) // 因?yàn)橐迫肓说讓覥ard,執(zhí)行了setHiddenDetail(false) // 將移入事件改為 setHiddenDetail(!hiddenDetail) setHiddenDetail(true) }} > <div className={styles.title}> {video.title} </div> </Card> </NavLink> ) }
以上就是React 懸浮框內(nèi)容懶加載實(shí)例詳解的詳細(xì)內(nèi)容,更多關(guān)于React 懸浮框內(nèi)容懶加載的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React開(kāi)發(fā)進(jìn)階redux saga使用原理詳解
這篇文章主要為大家介紹了React開(kāi)發(fā)進(jìn)階redux saga使用原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11React虛擬渲染實(shí)現(xiàn)50個(gè)或者一百個(gè)圖表渲染
這篇文章主要為大家介紹了React虛擬渲染實(shí)現(xiàn)50個(gè)或者100個(gè)圖表渲染的實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06關(guān)于useEffect的第二個(gè)參數(shù)解讀
這篇文章主要介紹了關(guān)于useEffect的第二個(gè)參數(shù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09詳解React如何實(shí)現(xiàn)代碼分割Code Splitting
這篇文章主要為大家介紹了React如何實(shí)現(xiàn)代碼分割Code Splitting示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08詳解React中錯(cuò)誤邊界的原理實(shí)現(xiàn)與應(yīng)用
在React中,錯(cuò)誤邊界是一種特殊的組件,用于捕獲其子組件樹(shù)中發(fā)生的JavaScript錯(cuò)誤,并防止這些錯(cuò)誤冒泡至更高層,導(dǎo)致整個(gè)應(yīng)用崩潰,下面我們就來(lái)看看它的具體應(yīng)用吧2024-03-03