怎么监听图片或者背景图片渲染完成

日常开发中我们经常遇到一种场景,就是pc端网站登录页面有一个背景图片,但是设计提供的背景图一般最少都是2倍图,就是图片比较大,但是图片在没有访问过的时候会导致无法一次性渲染出来。这时候产品可能会提出自己的要求,要求图片必须一次性加载出来。。。。这时候该怎么办呢?显然使用onLoad是肯定无法实现上述效果的。

所以我们需要解决的关键问题在于如何监听图片元素是否真正渲染完成,于是乎就想到了采用MutationObserver+requestAnimationFrame的方案来实现。

具体思路如下

tsx 复制代码
import React, { useState, useEffect, useRef } from 'react'

type ImageRenderMonitorProps = {
  children?: React.ReactNode;
};

// 判断是 img 元素还是 div, 主要用来区分是img标签还是背景图片
const isImageElement = (element: HTMLElement): boolean => {
  return (
    element.tagName === 'IMG' ||
    window.getComputedStyle(element).backgroundImage !== 'none' ||
    document.defaultView?.getComputedStyle(element, null).backgroundImage !== 'none'
  )
}

const ImageRenderMonitor: React.FC<ImageRenderMonitorProps> = ({ children }) => {
  const containerRef = useRef<HTMLDivElement>(null)
  const observerRef = useRef<HTMLDivElement>(null)
  const pendingElements = useRef<Set<Element>(new Set())
  const [isShow, setIsShow] = useState<boolean>(false)

  // 获取图片地址
  const getBgUrl = (element: HTMLElement): string | null => {
    const bgImage = window.getComputedStyle(element).backgroundImage
    const match = bgImage.match(/url\(["']?(.*?)["']?\)/)

    return match ? match[1] : null
  }

  // 渲染完成,让图片显示出来
  const confirmRenderComplete = (element: HTMLElement) => {
    // 强制布局计算
    void element.offsetHeight

    element.style.opacity = '1'
    element.style.transition = 'opacity 0.3s ease-in-out'
  }

  // 加载背景图
  const preloadBackgroundImage = (element: HTMLElement): HTMLImageElement | null => {
    const bgUrl = getBgUrl(element)
    if (!bgUrl) return null

    const img = new Image()
    img.src = bgUrl
    return img
  }

  // 处理追加元素
  const handleNewElement = (element: HTMLElement) => {
    element.style.opacity = '0'

    pendingElements.current.add(element)
    
    const isImgTag = element.tagName === 'IMG'
    const img = isImgTag ? (element as HTMLElement) : preloadBackgroundImage(element)

    if (!img) return

    // 加载完成
    const onLoad = () => {
      requestAnimationFrame(() => {
        requestAnimationFrame(() => {
          confirmRenderComplete(element)
        })
      })
    }

    // 加载失败
    const onError = () => {
      pendingElements.current.delete(element)

      // 这里可以处理错误逻辑,比如显示默认图片或者错误文案之类的
    }

    img.addEventListener('load', onLoad)
    img.addEventListener('error', onError)
  }

  useEffect(() => {
    // 触发元素渲染
    setIsShow(true)
    // 初始化 MutationObserver

    const initMutationObserver = () => {
      if (!containerRef.current) return
      observerRef.current = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
          mutation.addedNodes.forEach((node) => {
            if (node.nodeType === Node.ELEMENT_NODE) {
              const element = node as HTMLElement
              // 满足条件对元素进行处理
              if (isImageElement(element)) {
                handleNewElement(element)
              }
            }
          })
        })
      })
    }

    initMutationObserver()


    // 卸载监听
    return () => {
      observerRef?.current?.disconnect()
      pendingElements?.current?.forEach((element) => {
        if (element instanceof HTMLElement) {
          element.style.opacity = '1'
        }
      })
      pendingElements?.current?.clear()
    }
  }, [])

  return (
    <div ref={containerRef}
    style={{
      position: 'absolute',
      width: '100%',
      height: '100%'
    }}>
      {isShow && children}
    </div>
  )
}


export default ImageRender

具体使用如下

tsx 复制代码
import ImageRender from 'xxxx/ImageRender'
import bgUrl from 'xxx/bg.png'
// 方式1: 背景图片
<ImageRender>
  <div style={{backgroudImage: url(`${bgUrl}`)}}></div>
</ImageRender>

// 方式2: img元素
<ImageRender>
  <img src={bgUrl} width="100%" alt="" />
</ImageRender>

总结

上述代码中,涉及到了 MutationObserver 的使用和 opacity的使用,有兴趣的小伙伴可以自行想一下为什么要用到这两个属性或者方法。

当然上述只是我的一点思考或者说一种解决方案,供各位参考。相信各位小伙伴经过自己的努力肯定会有更优的解决方案。

相关推荐
GIS之路8 小时前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug8 小时前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu121388 小时前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中9 小时前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路9 小时前
GDAL 实现矢量合并
前端
hxjhnct9 小时前
React useContext的缺陷
前端·react.js·前端框架
前端 贾公子9 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端
菩提小狗9 小时前
Sqlmap双击运行脚本,双击直接打开。
前端·笔记·安全·web安全
前端工作日常9 小时前
我学习到的AG-UI的概念
前端
韩师傅9 小时前
前端开发消亡史:AI也无法掩盖没有设计创造力的真相
前端·人工智能·后端