日常开发中我们经常遇到一种场景,就是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的使用,有兴趣的小伙伴可以自行想一下为什么要用到这两个属性或者方法。
当然上述只是我的一点思考或者说一种解决方案,供各位参考。相信各位小伙伴经过自己的努力肯定会有更优的解决方案。