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

日常开发中我们经常遇到一种场景,就是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的使用,有兴趣的小伙伴可以自行想一下为什么要用到这两个属性或者方法。

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

相关推荐
zengyuhan50310 小时前
Windows BLE 开发指南(Rust windows-rs)
前端·rust
醉方休10 小时前
Webpack loader 的执行机制
前端·webpack·rust
前端老宋Running10 小时前
一次从“卡顿地狱”到“丝般顺滑”的 React 搜索优化实战
前端·react.js·掘金日报
隔壁的大叔10 小时前
如何自己构建一个Markdown增量渲染器
前端·javascript
用户44455436542610 小时前
Android的自定义View
前端
WILLF10 小时前
HTML iframe 标签
前端·javascript
枫,为落叶10 小时前
Axios使用教程(一)
前端
小章鱼学前端10 小时前
2025 年最新 Fabric.js 实战:一个完整可上线的图片选区标注组件(含全部源码).
前端·vue.js
ohyeah10 小时前
JavaScript 词法作用域、作用域链与闭包:从代码看机制
前端·javascript
流星稍逝10 小时前
手搓一个简简单单进度条
前端