IdbI

第19章—手写react-lazyload

网页里可能会有很多图片,图片加载有一个过程,我们会在图片加载过程中展示占位图片。

并且我们不需要一开始就加载所有图片,而是希望在图片滚动到可视区域再加载。

这种效果我们会用 react-lazyload 来实现。

创建个项目:

npx create-vite

进入项目,安装 react-lazyload

npm install

npm install --save react-lazyload

npm install --save-dev @types/react-lazyload

npm install --save prop-types

prop-types 是 react-lazyload 用到的包。

去掉 index.css 和 StrictMode:

然后改下 App.tsx

import img1 from './img1.png';
import img2 from './img2.png';
import LazyLoad from 'react-lazyload';

export default function App() {
  return (
    <div>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <p>xxxxxx</p>
      <LazyLoad placeholder={<div>loading...</div>}>
        <img src={img1}/>
      </LazyLoad>
      <LazyLoad placeholder={<div>loading...</div>}>
        <img src={img2}/>
      </LazyLoad>
    </div>
  );
};

在超出一屏的位置加载两张图片,用 LazyLoad 包裹。

可以看到,最开始展示 placeholder 的内容。

当图片划入可视区域后,会替换成图片:

在网络里也可以看到,当图片进入可视区域才会下载:

这就是 react-lazyload 的作用。

当然,它能做的可不只是懒加载图片,组件也可以。

我们知道,用 lazy 包裹的组件可以异步加载。

我们写一个 Guang.tsx

export default function Guang() {
    return '神说要有光';
}

然后在 App.tsx 里异步引入:

const LazyGuang = React.lazy(() => import('./Guang'));

import() 包裹的模块会单独打包,然后 React.lazy 是用到这个组件的时候才去加载。

试下效果:

可以看到,确实是异步下载了这个组件并渲染出来。

那如果我们想组件进入可视区域再加载呢?

这样:

react-lazyload 是进入可视区域才会把内容替换为 LazyGuang,而这时候才会去下载组件对应的代码。

效果就是这样的:

可以看到,Guang.tsx 的组件代码,img2.png 的图片,都是进入可视区域才加载的。

你还可以设置 offset,也就是不用到可视区域,如果 offset 设置 200,那就是距离 200px 到可视区域就触发加载:

可以看到,现在 img2 还没到可视区域就加载了。

知道了 react-lazyload 怎么用,那它是怎么实现的呢?

用前两节讲过的 IntersectionObserver 就可以实现。

我们来写一下:

src/MyLazyLoad.tsx

import {
    CSSProperties,
    FC,
    ReactNode,
    useRef,
    useState
} from 'react';

interface MyLazyloadProps{
    className?: string,
    style?: CSSProperties,
    placeholder?: ReactNode,
    offset?: string | number,
    width?: number | string,
    height?: string | number,
    onContentVisible?: () => void,
    children: ReactNode,
}

const MyLazyload: FC<MyLazyloadProps> = (props) => {

    const {
        className = '',
        style,
        offset = 0,
        width,
        onContentVisible,
        placeholder,
        height,
        children
    } = props;

    const containerRef = useRef<HTMLDivElement>(null);
    const [visible, setVisible] = useState(false);

    const styles = { height, width, ...style };

    return <div ref={containerRef} className={className} style={styles}>
        {visible? children : placeholder}
    </div>
}

export default MyLazyload;

先看下 props:

className 和 style 是给外层 div 添加样式的。

placeholder 是占位的内容。

offset 是距离到可视区域多远就触发加载。

onContentVisible 是进入可视区域的回调。

然后用 useRef 保存外层 div 的引用。

用 useState 保存 visible 状态。

visible 的时候展示 children,否则展示 placeholder。

然后补充下 IntersectionObserver 监听 div 进入可视区域的情况:

const elementObserver = useRef<IntersectionObserver>();

useEffect(() => {
    const options = {
        rootMargin: typeof offset === 'number' ? `${offset}px` : offset || '0px',
        threshold: 0
    };

    elementObserver.current = new IntersectionObserver(lazyLoadHandler, options);

    const node = containerRef.current;

    if (node instanceof HTMLElement) {
        elementObserver.current.observe(node);
    }
    return () => {
        if (node && node instanceof HTMLElement) {
            elementObserver.current?.unobserve(node);
        }
    }
}, []);

这里的 rootMargin 就是距离多少进入可视区域就触发,和参数的 offset 一个含义。

threshold 是元素进入可视区域多少比例的时候触发,0 就是刚进入可视区域就触发。

然后用 IntersectionObserver 监听 div。

之后定义下 lazyloadHandler:

function lazyLoadHandler (entries: IntersectionObserverEntry[]) {
    const [entry] = entries;
    const { isIntersecting } = entry;

    if (isIntersecting) {
        setVisible(true);
        onContentVisible?.();

        const node = containerRef.current;
        if (node && node instanceof HTMLElement) {
            elementObserver.current?.unobserve(node);
        }
    }
};

当 isIntersecting 为 true 的时候,就是从不相交到相交,反之,是从相交到不相交。

这里设置 visible 为 true,回调 onContentVisible,然后去掉监听。

测试下:

可以看到,首先是图片加载,然后是组件加载,这说明 offset 生效了:

这样,我们就实现了 react-lazyload。

案例代码上传了小册仓库

总结

当图片进入可视区域才加载的时候,可以用 react-lazyload。

它支持设置 placeholder 占位内容,设置 offset 距离多少距离进入可视区域触发加载。

此外,它也可以用来实现组件进入可视区域时再加载,配合 React.lazy + import() 即可。

它的实现原理就是 IntersectionObserver,我们自己实现了一遍,设置 rootMargin 也就是 offset,设置 threshold 为 0 也就是一进入可视区域就触发。

图片、组件的懒加载(进入可视区域再触发加载)是非常常见的需求,不但要会用 react-lazyload 实现这种需求,也要能够自己实现。

返回上级