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

 更新时间:2022年11月16日 16:47:39   作者:Fantasy955  
这篇文章主要为大家介绍了React 悬浮框内容懒加载实例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

界面隐藏

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

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组件化了,因此实现思路有一点改变:

  • 每个Item都有一个对应的悬浮框DIV,默认情况hidden
  • 为了节省流量,悬浮框内的内容需要懒加载;
  • 显示悬浮框的时机是一致的——鼠标移入时,为了优化体验,节省流量,可以设定为移入一段时机后才显示;

原始代码

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>
    )
}

以上就是React 悬浮框内容懒加载实例详解的详细内容,更多关于React 悬浮框内容懒加载的资料请关注脚本之家其它相关文章!

相关文章

  • React开发进阶redux saga使用原理详解

    React开发进阶redux saga使用原理详解

    这篇文章主要为大家介绍了React开发进阶redux saga使用原理详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11
  • react native 仿微信聊天室实例代码

    react native 仿微信聊天室实例代码

    这篇文章主要介绍了react native 仿微信聊天室实例代码,代码简单易懂,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-09-09
  • React虚拟渲染实现50个或者一百个图表渲染

    React虚拟渲染实现50个或者一百个图表渲染

    这篇文章主要为大家介绍了React虚拟渲染实现50个或者100个图表渲染的实现,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-06-06
  • 一看就懂的ReactJs基础入门教程-精华版

    一看就懂的ReactJs基础入门教程-精华版

    现在最热门的前端框架有AngularJS、React、Bootstrap等。自从接触了ReactJS,ReactJs的虚拟DOM(Virtual DOM)和组件化的开发深深的吸引了我,下面来跟我一起领略ReactJs的风采吧~~ 文章有点长,耐心读完,你会有很大收获哦
    2021-04-04
  • 关于useEffect的第二个参数解读

    关于useEffect的第二个参数解读

    这篇文章主要介绍了关于useEffect的第二个参数,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-09-09
  • useEffect中不能使用async原理详解

    useEffect中不能使用async原理详解

    这篇文章主要为大家介绍了useEffect中为什么不能使用async的原理详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • 详解React如何实现代码分割Code Splitting

    详解React如何实现代码分割Code Splitting

    这篇文章主要为大家介绍了React如何实现代码分割Code Splitting示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08
  • React国际化react-i18next详解

    React国际化react-i18next详解

    react-i18next 是基于 i18next 的一款强大的国际化框架,可以用于 react 和 react-native 应用,是目前非常主流的国际化解决方案。这篇文章主要介绍了React国际化react-i18next的相关知识,需要的朋友可以参考下
    2021-10-10
  • React 函数式组件和类式组件详情

    React 函数式组件和类式组件详情

    这篇文章主要介绍了React函数式组件和类式组件详情,React是组件化的的JS库,组件化也是React的核心思想,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-08-08
  • 详解React中错误边界的原理实现与应用

    详解React中错误边界的原理实现与应用

    在React中,错误边界是一种特殊的组件,用于捕获其子组件树中发生的JavaScript错误,并防止这些错误冒泡至更高层,导致整个应用崩溃,下面我们就来看看它的具体应用吧
    2024-03-03

最新评论