亲宝软件园·资讯

展开

React 悬浮框内容懒加载实例详解

Fantasy955 人气:0

界面隐藏

一个容器放置视频,默认情况下

display: none;
z-index: 0;
transform: transform3d(10000px, true_y, true_z);

y轴和z轴左边都是真实的(腾讯视频使用绝对定位,因此是计算得到的),只是将其移到右边很远的距离。

懒加载

React监听鼠标移入(获取坐标)

onMouseEnter={(e) => { handleMouseEnter(e) }}
const handleMouseEnter = (e: React.MouseEvent) => {
  console.log(e.target)
}

注意事件类型React.MouseEvent

合成事件 – React (reactjs.org)

获取Element的绝对位置

typescript中HTMLElement 和 Element的区别

ts中:

let res =document.getElementById('test');  //HTMLElement
let el = document.querySelector('#test');  // Element

mdn中: querySelectorgetElementById两者均返回Element。

Element 是一个通用性非常强的基类,所有 Document 对象下的对象都继承自它。这个接口描述了所有相同种类的元素所普遍具有的方法和属性。一些接口继承自 Element 并且增加了一些额外功能的接口描述了具体的行为。

例如, HTMLElement 接口是所有 HTML 元素的基本接口,而 SVGElement 接口是所有 SVG 元素的基础。大多数功能是在这个类的更深层级(hierarchy)的接口中被进一步制定的。

实现:

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实现

在腾讯视频中,悬浮框是处于顶层div下的,因此使用绝对定位(绝对定位是相当与父节点的,并不是document)。

在React中,由于我们将展示视频信息的这个Item组件化了,因此实现思路有一点改变:

原始代码

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响应点击事件,跳转到视频详情页,以上代码还不含与本文相关内容。

放入新的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>

状态设置

加入状态表示是否隐藏悬浮框:

默认隐藏

const [hiddenDetail, setHiddenDetail] = useState(true)

样式设置

style={{
  backgroundColor: 'pink',
  display: hiddenDetail ? 'none' : 'inline-block',
  position: 'absolute',
  transform: `translate3d(0px, -100%, 0px)`,
}}

两个Card组件的宽度和高度已经设为一致,为了方便调试,将悬浮框的背景设为粉色;

使用绝对定位,让其能够覆盖原始信息;

通过transform改变悬浮框的位置,不设置的话,悬浮框默认被挤到下方,-100%表示在y轴上向上移动悬浮框高度对应的像素,由于两个Card组件高度相同,因此可以覆盖原始信息。

事件设置

第一个Card,即默认显示的元素,添加鼠标移入事件:

onMouseEnter={(e) => {
  setHiddenDetail(!hiddenDetail)
}}

第二个Card,即悬浮框,添加鼠标移出事件:

onMouseLeave={(e) => {
  // bug 向下移出不会触发 
  // 因为移入了底层Card,执行了setHiddenDetail(false)
  // 将移入事件改为 setHiddenDetail(!hiddenDetail)
  setHiddenDetail(true)
}}

这里我们使用!hiddenDetail,而不是直接设为true

因为如果底层DIV大于悬浮框的框的话,在悬浮框显示的情况下,如果移出过程进入了底层DIV,会导致悬浮框不会消失(虽然移出过程触发了onMouseLeave,将状态设为false,但移入底层DIV后,再次触发onMouseEnter,将状态设为true),这主要是应对悬浮框没有完全覆盖底层元素的情况。

事件优化

延迟显示悬浮框

在底层元素的事件响应中:

onMouseEnter={(e) => {  setHiddenDetail(!hiddenDetail)}}

将状态改变任务用Timeout包裹,设定延时t,如果在移出该元素时,定时器还没有结束,则结束该定时器:

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>

悬浮框内容懒加载

在腾讯视频中,悬浮框显示一小段视频,但是一个页面中包含多个悬浮框,如果一次全部加载这些资源,会造成比较大的流量浪费,因此,最后是要显示悬浮框时,才加载详细内容。

在本示例中,我们悬浮框显示的图片设为懒加载模式,我们需要增加一个状态firstLoad记录是否是第一次显示悬浮框,如果是第一次,则设一个定时器模拟发送请求,获取详细内容的链接。另一种情况是,在知道链接地址的情况下,不发送请求,将元素的src指向更高为正确的就行。

为了方便操作DOM元素,我们创建一个悬浮框的ref对象:detailRef

const [firstLoad, setFirstLoad] = useState(true)
const detailRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
    // 第一次加载悬浮框,并且悬浮框状态为显示
    if (firstLoad && !hiddenDetail) {
    // 在知道路径的情况下,可以直接修改路径,Promise用于模拟向服务器发送请求的等待过程
        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 向下移出不会触发 
                    // 因为移入了底层Card,执行了setHiddenDetail(false)
                    // 将移入事件改为 setHiddenDetail(!hiddenDetail)
                    setHiddenDetail(true)
                }}
            >
                <div className={styles.title}>
                    {video.title}
                </div>
            </Card>
        </NavLink>
    )
}

加载全部内容

相关教程
猜你喜欢
用户评论