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

目录
  • 界面隐藏
  • 懒加载
  • React实现
    • 原始代码
    • 放入新的DIV
      • 状态设置
      • 样式设置
      • 事件设置
    • 事件优化
      • 延迟显示悬浮框
      • 悬浮框内容懒加载
    • 完整代码

界面隐藏

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

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 悬浮框内容懒加载的资料请关注我们其它相关文章!

(0)

相关推荐

  • React TypeScript 应用中便捷使用Redux Toolkit方法详解

    目录 前言 背景 Redux-Toolkit 常规使用 优化方案 优化 useDispatch 和 useSelector 优化修改 redux 状态的步骤 总结 前言 本文介绍的主要内容是 Redux-Toolkit 在 React + TypeScript 大型应用中的实践,主要解决的问题是使用 createSlice 的前提下消费 redux 状态仍旧有点繁琐的问题. 阅读本文需要的前置知识:了解 React .Redux-Toolkit .TypeScript 的使用. 关于 Redux

  • React 中的重新渲染类组件及函数组件

    目录 缘起 类组件 React 合成事件 定时器回调后触发 setState 异步函数后调触发 setState 原生事件触发 setState 修改不参与渲染的属性 只是调用 setState,页面会不会重新渲染 多次渲染的问题 测试代码 函数组件 React 合成事件 定时器回调 异步函数回调 原生事件 更新没使用的状态 小结 不同的情况 设置同样的 State React Hook 中避免多次渲染 将全部 state 合并成一个对象 使用 useReducer 状态直接用 Ref 声明,需

  • Vue/React子组件实例暴露方法(TypeScript)

    目录 Vue2 Vue3 React 最近几个月都在用TS开发各种项目,框架有涉及到Vue3,React18等:记录一下Vue/React组件暴露出变量/函数的方法的写法: Vue2 回顾一下Vue2 组件暴露出去方法,它并没有约束,写在methods里的方法都能被调用,data里的变量也能被接收: 现拉一个vue 2.6.10的模板下来子组件的数据 父组件获取子组件实例,调用子组件方法等: 控制台输出: 这个输出的子组件实例里包含所有的变量和方法: Vue3 组件通过vue3提供的define

  • React setState是异步还是同步原理解析

    目录 setState异步更新 那么为什么setState设计为异步呢? 如何获取异步的结果 setState一定是异步的吗? setState异步更新 开发中当组件中的状态发生了变化,页面并不会重新渲染.我们必须要通过setState来告知React数据已经发生了变化,重新渲染页面. 先来看下面的例子: constructor() { super(); this.state = { message: "Hello World", }; } changeText() { this.se

  • React 之最小堆min heap图文详解

    目录 二叉树 完全二叉树 二叉堆 最小堆 React 采用原因 React 函数实现 插入过程(push) >>> 1 删除过程(pop) halfLength peek 二叉树 二叉树(Binary tree),每个节点最多只有两个分支的树结构.通常分支被称作“左子树”或“右子树”.二叉树的分支具有左右次序,不能随意颠倒. 完全二叉树 在一颗二叉树中,若除最后一层外的其余层都是满的,并且最后一层要么是满的,要么在右边缺少连续若干节点,则此二叉树为完全二叉树(Complete Binar

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

    目录 界面隐藏 懒加载 React实现 原始代码 放入新的DIV 状态设置 样式设置 事件设置 事件优化 延迟显示悬浮框 悬浮框内容懒加载 完整代码 界面隐藏 一个容器放置视频,默认情况下 display: none; z-index: 0; transform: transform3d(10000px, true_y, true_z); y轴和z轴左边都是真实的(腾讯视频使用绝对定位,因此是计算得到的),只是将其移到右边很远的距离. 懒加载 React监听鼠标移入(获取坐标) 添加事件监听 o

  • Vue lazyload图片懒加载实例详解

    文档:https://github.com/hilongjw/vue-lazyload 1.安装 cnpm i vue-lazyload -S 或 npm i vue-lazyload -S 2.实例 导入配置等操作 src/main.js import Vue from 'vue' import App from './App' import router from './router' //[1]导入懒加载 import VueLazyload from 'vue-lazyload' Vue

  • androidx下的fragment的lazy懒加载问题详解

    网上关于androidx的fragment懒加载文章已经有很多,各有侧重.几乎都点到了sexMaxLifecycle和修改FragmentPagerAdapter.很少看到经过实践的文章,谨以此文,更加详尽的把实践后的结果记录下来,赠予有缘人. 一.前置准备工作 几个关于androidx的fragment懒加载方案,都离不开如下几个包: androidx.fragment:fragment:1.1.0-alpha07 以上,支持setMaxLifecycle方法即可 androidx.viewp

  • vue前端性能优化之预加载和懒加载示例详解

    目录 预加载 图片预加载 JS预加载 js的加载方式 preload prefetch Preload & Prefetch 的区别 不同资源加载的优先级规则 懒加载 图片懒加载 路由懒加载 组件懒加载 最后 预加载 预加载简单来说就是将所有所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源:我们使用该技术预先告知浏览器,等下某些资源可能要被使用,先把资源下载下来,不要等使用的时候再下载,可以看出这样的加载技术会增加服务器的压力,但是用户的体验会比较好,因为可以较快的看到后面的

  • vue按需加载实例详解

    vue-router配置路由,使用vue的异步组件技术,可以实现按需加载.这种方式下一个组件生成一个js文件 用例: { path: '/promisedemo', name: 'PromiseDemo', component: resolve => require(['../components/PromiseDemo'], resolve) } es提案的import() (推荐) webpack官方文档:webpack中使用import() vue官方文档:路由懒加载(使用import()

  • iscroll.js滚动加载实例详解

    滚动加载是个好东西,可以解决一次加载过多的尴尬,其实就是变相的分页,总结下这个轮子的用法,挺简单的. 首先是html结构: <div class=" saleRecord panelList clear" style="position:relative;height:400px;"> <div id="wrapper"> <div id="scroller"> <ul id=&quo

  • PyTorch模型保存与加载实例详解

    目录 一个简单的例子 保存/加载 state_dict(推荐) 保存/加载整个模型 保存加载用于推理的常规Checkpoint/或继续训练 保存多个模型到一个文件 使用其他模型来预热当前模型 跨设备保存与加载模型 总结 torch.save:保存序列化的对象到磁盘,使用了Python的pickle进行序列化,模型.张量.所有对象的字典. torch.load:使用了pickle的unpacking将pickled的对象反序列化到内存中. torch.nn.Module.load_state_di

  • ionic2懒加载配置详解

    文章标题为part 1,并解释目前可以如此配置,但后续使用上可能还有变动. 以ion-cli默认home组件为例.添加home.module.ts文件 import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { HomePage } from './home'; @NgModule({ declarations: [HomePage], imports: [

  • 详解webpack + react + react-router 如何实现懒加载

    在 Webpack 1 中主要是由bundle-loader进行懒加载,而 Webpack 2 中引入了类似于 SystemJS 的System.import语法,首先我们对于System.import的执行流程进行简单阐述: Webpack 会在编译过程中扫描代码库时将发现的System.import调用引入的文件及其相关依赖进行单独打包,注意,Webpack 会保证这些独立模块及其依赖不会与主应用的包体相冲突. 当我们访问到这些独立打包的组件模块时,Webpack 会发起 JSONP 请求来

  • javascript瀑布流式图片懒加载实例解析与优化

    之前写过一版图片"懒加载"的文章,刚好周末在整理文件的时候,大概又看了一遍之前写的代码发现有很多可以优化的地方. 这篇文章主要就是结合上篇<javascript瀑布流式图片懒加载实例>再来看看图片"懒加载"的一些知识. 图片"懒加载"的主旨: 按照需要加载图片,也就是说需要显示的时候再加载图片显示,减少一次性加载的网络带宽开销. 先来看一段代码: var conf = { 'loadfirst': true, 'loadimg': t

随机推荐