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

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

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

相关推荐
拾光拾趣录11 分钟前
算法 | 下一个更大的排列
前端·算法
小屁孩大帅-杨一凡37 分钟前
如何使用Python将HTML格式的文本转换为Markdown格式?
开发语言·前端·python·html
于慨37 分钟前
uniapp云打包安卓
前端·uni-app
米粒宝的爸爸38 分钟前
【uniapp】使用uviewplus来实现图片上传和图片预览功能
java·前端·uni-app
LaoZhangAI38 分钟前
2025年虚拟信用卡订阅ChatGPT Plus完整教程(含WildCard停运后最新方案)
前端·后端
雪碧聊技术39 分钟前
Uniapp 纯前端台球计分器开发指南:能否上架微信小程序 & 打包成APP?
前端·微信小程序·uni-app·台球计分器
清风细雨_林木木40 分钟前
Vuex 的语法“...mapActions([‘login‘]) ”是用于在组件中映射 Vuex 的 actions 方法
前端·javascript·vue.js
会功夫的李白43 分钟前
Uniapp之自定义图片预览
前端·javascript·uni-app·图片预览
拾光拾趣录1 小时前
script 标签上有那些属性,分别作用是啥?
前端·javascript
码农胖大海1 小时前
前端搞基建之低代码平台再调研
前端·低代码