简单用React Hooks封装一个图片组件,包含失败、加载、懒加载、成功状态

562 阅读6分钟

大家好,我是在校准大三的暑假实习生

参数校验

包含默认的图片URL地址、高度、宽度、是否显示加载动画特效(默认是)、是否加载错误效果(默认是)、是否懒加载(默认是)、懒加载图片地址(默认有)

// Img.jsx
import proptypes from 'prop-types';
import { LazyImgUrl } from './data';
Img.propTypes = {
    src:  proptypes.string.isRequired,  // 图片地址
    alt: proptypes.string,      // 图片描述
    height:  proptypes.number,  // 图片高度
    width: proptypes.number,   // 图片宽度
    isLoading: proptypes.bool, // 是否加载动画特效,默认是
    isError: proptypes.bool,  // 是否加载错误效果, 默认是
    isLayout: proptypes.bool,  // 是否懒加载, 默认是
    lazyImg: proptypes.string  // 懒加载图片地址 
}

Img.defaultProps = {
    isLoading: true,
    isError: true,
    isLayout: true,
    lazyImg: LazyImgUrl
}

// data.js
// 懒加载默认图片
export const LazyImgUrl = 'https://cdn.uviewui.com/uview/album/1.jpg';

加载错误状态显示

1、大家应该都晓得在React里面img标签包含2个比较重要的事件onError、onLoad事件,分别对应图片加载成功、加载失败。

2、所以我们可以定义一个imgUrl状态,这个默认是props传递过来的,然后赋值给img标签的src属性,然后我们只要在onError事件里面动态的修改这个imgUrl状态就可以

    import { useState } from 'react';
    
    const [ imgUrl, setImgUrl ] = useState(props.src)
    
    const handleError = () => {
        setImgUrl(错误的图片地址)
    }
    
    <img ref={imgRef}  onError={handleError} onLoad={handleLoad} className="tsq_image_box_img" src={isLayout ? lazyImg : src} data-src={src} alt={alt}/> 
    

3、但好像不对了,为什么会出现闪动的效果,仔细思考就知道,setState是异步的,我们虽然动态修改了图片地址,但是还是会出现闪动

4、所以我想到了一个好办法,我们可以定义一个容器,通过ref绑定给那个img标签,然后在img标签里面修改src就可以了,没想到,还真行。

    import { useRef } from 'react';
    
     const handleError = () => {
        imgRef.current.src = '失败的图片地址';
    }
    
    <img ref={imgRef}  onError={handleError} onLoad={handleLoad} className="tsq_image_box_img" src={src} data-src={src} alt={alt}/> 
    

加载中状态显示

1、比较简单好吧,我们首先通过css3做个简单loading就可以了

// index.jsx
 <div className="tsq_image_box_img tsq_image_box_img_loading">
         <svg t="1660030065094" className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5305"><path d={'svg图片地址'} fill="#262626" p-id="5306"></path></svg>
  </div> 

// index.css
@keyframes circle {
    0% {
        transform: rotate(0);
    }
    25% {
        transform: rotate(90deg);
    }
    50% {
        transform: rotate(180deg);
    }
    75% {
        transform: rotate(270deg);
    }
    100% {
        transform: rotate(360deg);
    }
}

.tsq_image_box {
    position: relative;
    height: 80px;
    width: 80px;
}

.tsq_image_box_img {
    height: 100%;
    width: 100%;
}
.tsq_image_box_img_loading {
    position: absolute;
    left: 0;
    top: 0;
    height: 100%;
    width: 100%;
    background-color: #F4F6F9;
    z-index: 999;
    display: flex;
    align-items: center;
    justify-content: center;
}
.tsq_image_box_img_loading svg {
    height: 50%;
    width: 50%;
    animation: circle .5s infinite;
}

2、做好以后,有个问题,我们应该什么时候加载它和关闭了?

3、React给我们提供了,我们可以在渲染DOM加载这个特效,然后我们通过new 一个Image对象,没错,就是这个,然后监听它的onload事件,就是加载成功的回调函数,然后关闭这个loading即可;另外,在加载失败前,关闭这个loading进行。

import { useState,useEffect } from 'react';

const [ loading, setLoading ] = useState(isLoading);

// 图片加载动画
    useEffect(() => {
        handleLoading();
    }, []);
    
// 图片加载失败
    const handleError = (e) => {
        if (isError) {
            setLoading(false);
            imgRef.current.src = FAIL_IMG;
        }
    }    
 

 // 图片加载动画
 const handleLoading = () => {
      const newImg = new Image();
      newImg.src = src;
      // 加载完成
      newImg.onload = () => {
          if (loading) setLoading(false);
      }
}

          <div className="tsq_image_box" style={{height: height + 'px', width: width + 'px'}}>
                <img ref={imgRef}  onError={handleError} onLoad={handleLoad} className="tsq_image_box_img" src={isLayout ? lazyImg : src} data-src={src} alt={alt}/> 
                { loading 
                ? 
                <div className="tsq_image_box_img tsq_image_box_img_loading">
                    <svg t="1660030065094" className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5305"><path d={PathD} fill="#262626" p-id="5306"></path></svg>
                </div> 
                : ''}
           </div>

懒加载

1、首先在img标签里面我们定义一个自定义属性data-src="真实的图片地址" src="懒加载的图片地址";然后在通过isLayoutprops来判断是否开启加载效果。

  <img ref={imgRef}  onError={handleError} onLoad={handleLoad} className="tsq_image_box_img" src={isLayout ? lazyImg : src} data-src={src} alt={alt}/> 

2、然后我们定义一个懒加载处理函数,在这个函数里面,我们获取页面的宽、高,在通过getBoundingClientRect()方法获取当前图片的top、right、bottom、left值,然后进行简单的判断大小,如果在可视区,就把那个data-src属性赋值给src属性即可

    // 懒加载
    const handleLazy = () => {
        const h = window.innerHeight;
        const w = window.innerWidth;
        const { top, right, bottom, left } =  imgRef.current.getBoundingClientRect();
        if (bottom < 0 || top > h) return;
        if (right < 0 || left > w) return;
        if (imgRef.current.src === imgRef.current.attributes['data-src'].textContent) return;
        else imgRef.current.src = imgRef.current.attributes['data-src'].textContent;
    }

3、然后我们在useState里面监听即可

    // 懒加载
    useEffect(() => {
        if (!isLayout) return;
        document.addEventListener('scroll', handleLazy)
    }, []);

    // 初次加载
    useEffect(() => {
        handleLazy();
    }, [])

全部代码

index.jsx

/**
 * @Author tsq
 * @Description 图片组件
 * @Date 2022-8-9 13:56
 */
import { memo, useState, useRef, useEffect } from 'react';
import style from './index.css';
import proptypes from 'prop-types';
import { FAIL_IMG, PathD, LazyImgUrl } from './data';


const Img = ({ src, alt, height, width, isLoading, isError, isLayout, lazyImg }) => {
    
    const [ loading, setLoading ] = useState(isLoading);
    const imgRef = useRef();

    // 图片加载动画
    useEffect(() => {
        handleLoading();
    }, []);

    // 懒加载
    useEffect(() => {
        if (!isLayout) return;
        document.addEventListener('scroll', handleLazy)
    }, []);

    // 初次加载
    useEffect(() => {
        handleLazy();
    }, [])

    // 图片加载成功
    const handleLoad = () => {
        console.log('图片加载成功')
    }

    // 图片加载失败
    const handleError = (e) => {
        if (isError) {
            setLoading(false);
            imgRef.current.src = FAIL_IMG;
        }
    }

    // 图片加载动画
    const handleLoading = () => {
        const newImg = new Image();
        newImg.src = src;
        // 加载完成
        newImg.onload = () => {
            if (loading) setLoading(false);
        }
    }

    // 懒加载
    const handleLazy = () => {
        const h = window.innerHeight;
        const w = window.innerWidth;
        const { top, right, bottom, left } =  imgRef.current.getBoundingClientRect();
        if (bottom < 0 || top > h) return;
        if (right < 0 || left > w) return;
        if (imgRef.current.src === imgRef.current.attributes['data-src'].textContent) return;
        else imgRef.current.src = imgRef.current.attributes['data-src'].textContent;
    }

    return (
        <>
            <div className="tsq_image_box" style={{height: height + 'px', width: width + 'px'}}>
                <img ref={imgRef}  onError={handleError} onLoad={handleLoad} className="tsq_image_box_img" src={isLayout ? lazyImg : src} data-src={src} alt={alt}/> 
                { loading 
                ? 
                <div className="tsq_image_box_img tsq_image_box_img_loading">
                    <svg t="1660030065094" className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5305"><path d={PathD} fill="#262626" p-id="5306"></path></svg>
                </div> 
                : ''}
            </div>
        </>
    )
}

Img.propTypes = {
    src:  proptypes.string.isRequired,  // 图片地址
    alt: proptypes.string,      // 图片描述
    height:  proptypes.number,  // 图片高度
    width: proptypes.number,   // 图片宽度
    isLoading: proptypes.bool, // 是否加载动画特效,默认是
    isError: proptypes.bool,  // 是否加载错误效果, 默认是
    isLayout: proptypes.bool,  // 是否懒加载, 默认是
    lazyImg: proptypes.string  // 懒加载图片地址 
}

Img.defaultProps = {
    isLoading: true,
    isError: true,
    isLayout: true,
    lazyImg: LazyImgUrl
}

export default memo((props) => <Img {...props} />)

index.css

@keyframes circle {
    0% {
        transform: rotate(0);
    }
    25% {
        transform: rotate(90deg);
    }
    50% {
        transform: rotate(180deg);
    }
    75% {
        transform: rotate(270deg);
    }
    100% {
        transform: rotate(360deg);
    }
}

.tsq_image_box {
    position: relative;
    height: 80px;
    width: 80px;
}

.tsq_image_box_img {
    height: 100%;
    width: 100%;
}
.tsq_image_box_img_loading {
    position: absolute;
    left: 0;
    top: 0;
    height: 100%;
    width: 100%;
    background-color: #F4F6F9;
    z-index: 999;
    display: flex;
    align-items: center;
    justify-content: center;
}
.tsq_image_box_img_loading svg {
    height: 50%;
    width: 50%;
    animation: circle .5s infinite;
}

data.js

/**
 * @Author tsq
 * @Description 图片组件资源
 * @Date 2022-8-9 14:30
 */

// 失败图片
export const FAIL_IMG = 'data:image/png;base64,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'

// 加载图片
export const PathD = 'M876.864 782.592c3.264 0 6.272-3.2 6.272-6.656 0-3.456-3.008-6.592-6.272-6.592-3.264 0-6.272 3.2-6.272 6.592 0 3.456 3.008 6.656 6.272 6.656z m-140.544 153.344c2.304 2.432 5.568 3.84 8.768 3.84a12.16 12.16 0 0 0 8.832-3.84 13.76 13.76 0 0 0 0-18.56 12.224 12.224 0 0 0-8.832-3.84 12.16 12.16 0 0 0-8.768 3.84 13.696 13.696 0 0 0 0 18.56zM552.32 1018.24c3.456 3.648 8.32 5.76 13.184 5.76a18.368 18.368 0 0 0 13.184-5.76 20.608 20.608 0 0 0 0-27.968 18.368 18.368 0 0 0-13.184-5.824 18.368 18.368 0 0 0-13.184 5.76 20.608 20.608 0 0 0 0 28.032z m-198.336-5.76c4.608 4.8 11.072 7.68 17.6 7.68a24.448 24.448 0 0 0 17.536-7.68 27.456 27.456 0 0 0 0-37.248 24.448 24.448 0 0 0-17.536-7.68 24.448 24.448 0 0 0-17.6 7.68 27.52 27.52 0 0 0 0 37.184z m-175.68-91.84c5.76 6.08 13.824 9.6 21.952 9.6a30.592 30.592 0 0 0 22.016-9.6 34.368 34.368 0 0 0 0-46.592 30.592 30.592 0 0 0-22.016-9.6 30.592 30.592 0 0 0-21.952 9.6 34.368 34.368 0 0 0 0 46.592z m-121.152-159.36c6.912 7.36 16.64 11.648 26.368 11.648a36.736 36.736 0 0 0 26.432-11.584 41.28 41.28 0 0 0 0-55.936 36.736 36.736 0 0 0-26.432-11.584 36.8 36.8 0 0 0-26.368 11.52 41.28 41.28 0 0 0 0 56zM12.736 564.672a42.88 42.88 0 0 0 30.784 13.44 42.88 42.88 0 0 0 30.784-13.44 48.128 48.128 0 0 0 0-65.216 42.88 42.88 0 0 0-30.72-13.44 42.88 42.88 0 0 0-30.848 13.44 48.128 48.128 0 0 0 0 65.216z m39.808-195.392a48.96 48.96 0 0 0 35.2 15.36 48.96 48.96 0 0 0 35.2-15.36 54.976 54.976 0 0 0 0-74.56 48.96 48.96 0 0 0-35.2-15.424 48.96 48.96 0 0 0-35.2 15.424 54.976 54.976 0 0 0 0 74.56zM168.32 212.48c10.368 11.008 24.96 17.408 39.68 17.408 14.592 0 29.184-6.4 39.552-17.408a61.888 61.888 0 0 0 0-83.84 55.104 55.104 0 0 0-39.616-17.408c-14.656 0-29.248 6.4-39.616 17.408a61.888 61.888 0 0 0 0 83.84zM337.344 124.8c11.52 12.16 27.712 19.264 43.968 19.264 16.256 0 32.448-7.04 43.968-19.264a68.672 68.672 0 0 0 0-93.184 61.248 61.248 0 0 0-43.968-19.264 61.248 61.248 0 0 0-43.968 19.264 68.736 68.736 0 0 0 0 93.184z m189.632-1.088c12.672 13.44 30.528 21.248 48.448 21.248s35.712-7.808 48.384-21.248a75.584 75.584 0 0 0 0-102.464A67.392 67.392 0 0 0 575.36 0c-17.92 0-35.776 7.808-48.448 21.248a75.584 75.584 0 0 0 0 102.464z m173.824 86.592c13.824 14.592 33.28 23.104 52.736 23.104 19.584 0 39.04-8.512 52.8-23.104a82.432 82.432 0 0 0 0-111.744 73.472 73.472 0 0 0-52.8-23.168c-19.52 0-38.912 8.512-52.736 23.168a82.432 82.432 0 0 0 0 111.744z m124.032 158.528c14.976 15.872 36.032 25.088 57.216 25.088 21.12 0 42.24-9.216 57.152-25.088a89.344 89.344 0 0 0 0-121.088 79.616 79.616 0 0 0-57.152-25.088c-21.184 0-42.24 9.216-57.216 25.088a89.344 89.344 0 0 0 0 121.088z m50.432 204.032c16.128 17.088 38.784 27.008 61.632 27.008 22.784 0 45.44-9.92 61.568-27.008a96.256 96.256 0 0 0 0-130.432 85.76 85.76 0 0 0-61.568-27.072c-22.848 0-45.44 9.984-61.632 27.072a96.192 96.192 0 0 0 0 130.432z'


// 懒加载默认图片
export const LazyImgUrl = 'https://cdn.uviewui.com/uview/album/1.jpg';