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

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

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

相关推荐
lijun_xiao200912 分钟前
前端最新Vue2+Vue3基础入门到实战项目全套教程
前端
90后的晨仔25 分钟前
Pinia 状态管理原理与实战全解析
前端·vue.js
杰克尼31 分钟前
JavaWeb_p165部门管理
java·开发语言·前端
90后的晨仔33 分钟前
Vue3 状态管理完全指南:从响应式 API 到 Pinia
前端·vue.js
90后的晨仔1 小时前
Vue 内置组件全解析:提升开发效率的五大神器
前端·vue.js
我胡为喜呀1 小时前
Vue3 中的 watch 和 watchEffect:如何优雅地监听数据变化
前端·javascript·vue.js
我登哥MVP1 小时前
Ajax 详解
java·前端·ajax·javaweb
非凡ghost2 小时前
Typora(跨平台MarkDown编辑器) v1.12.2 中文绿色版
前端·windows·智能手机·编辑器·软件需求
馨谙2 小时前
/dev/null 是什么,有什么用途?
前端·chrome
JamSlade3 小时前
流式响应 sse 系统全流程 react + fastapi为例子
前端·react.js·fastapi