手寫圖片嬾加載

193 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

1.目標

封装一个拥有懒加载功能的图片组件,实现其他組件的图片懒加载

2.懒加载的基本思路

当dom元素进入可视区域时,才去加载它。

如何判断一个dom元素是否进入了可见区域?

3.實現思路

利用浏览器提供的 IntersectionObserveropen in new window,监听图片元素是否进入可视区域,进入后才真正去设置图片元素的 src 属性进行图片加载。

4.格式

var dom = dom元素
// 实例化一个观察者
// 它的参数1是一个回调:当被观察的目标进入视口/离开视口就会调用
var observer = new IntersectionObserver((entries)=>{
//entries為觀察的元素組成的數組
  console.log(entries[0].isIntersecting)//isIntersecting為是否在可視區
  console.log(entries[0].intersectionRatio)
  if(entries[0].isIntersecting) {
    
  }
}, 其他配置)//{rootMargin: "100px"}下拉100px為true

// 观察者观察dom
observer.observe(dom)
observer.disconnect()   // 停止观察者
observer.unobserve(dom) // 观察者停止对dom的观察

5.舉例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>
<body>
    <div>
        <p style="padding: 30px;">1</p>
        <p style="padding: 30px;">2</p>
        <p style="padding: 30px;">3</p>
        <p style="padding: 30px;">4</p>
        <p style="padding: 30px;">5</p>
        <p style="padding: 30px;">6</p>
        <p style="padding: 30px;">7</p>
        <p style="padding: 30px;">8</p>
        <p style="padding: 30px;">9</p>
        <p style="padding: 30px;">10</p>
        <p style="padding: 30px;">11</p>
        <p style="padding: 30px;">12</p>
        <p style="padding: 30px;">13</p>
        <p style="padding: 30px;">14</p>
        <!-- <img height="200" src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Ffzn.cc%2Fwp-content%2Fuploads%2F2020%2F04%2F640-8.jpg&refer=http%3A%2F%2Ffzn.cc&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1641365183&t=b5d7bdae0fe3f2c4831b52e3985abdf1" /> -->
        <img height="200" data-src="https://gimg2.baidu.com/image_search1/src=http%3A%2F%2Ffzn.cc%2Fwp-content%2Fuploads%2F2020%2F04%2F640-8.jpg&refer=http%3A%2F%2Ffzn.cc&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1641365183&t=b5d7bdae0fe3f2c4831b52e3985abdf1" />
        <p style="padding: 30px;">1</p>
        <p style="padding: 30px;">2</p>
        <p style="padding: 30px;">3</p>
        <p style="padding: 30px;">4</p>
        <p style="padding: 30px;">5</p>
        <p style="padding: 30px;">6</p>
        <p style="padding: 30px;">7</p>
        <p style="padding: 30px;">8</p>
        <p style="padding: 30px;">9</p>
    </div>

    <script>
        var img = document.querySelector("img")
        var observer = new IntersectionObserver((arr)=>{
            console.log(arr[0].isIntersecting)
            console.log(arr[0].intersectionRatio)
            if(arr[0].isIntersecting) {
                img.src = img.getAttribute('data-src')
                img.onerror = function(){
                    img.src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2Fcdb58e6860bcf06028c4b40e47aa17fd7ffa2e6f67cc-UQZRFK_fw658&refer=http%3A%2F%2Fhbimg.b0.upaiyun.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1641366439&t=07c8d22d1c35def62dcaaf04d94e1255"
                }
            }
        }, {rootMargin: "100px"})

        observer.observe(img)
    </script>
</body>
</html>

6.創建圖片組件大體框架

import classnames from 'classnames'//用於單個標簽多個classname的使用
import { useRef } from 'react'//用來處理dom
import Icon from '../Icon' //封裝的公共圖標組件
import styles from './index.module.scss'

/**
 * 拥有懒加载特性的图片组件
 * @param {String} props.src 图片地址
 * @param {String} props.className 样式类
 */
type Props = {//接收到的參數的類型
  src: string
  className?: string//非必傳
}//父子組件見傳值
const Image = ({ src, className }: Props) => {//接收參數
  // 对图片元素的引用
  const imgRef = useRef(null)

  return (
    <div className={classnames(styles.root, className)}>
      {/* 正在加载时显示的内容 */}
      {
        <div className="image-icon">
          <Icon type="iconphoto" />
        </div>
      }

      {/* 加载出错时显示的内容 */}
      {
        <div className="image-icon">
          <Icon type="iconphoto-fail" />
        </div>
      }

      {/* 加载成功时显示的内容 */}
      {<img alt="" data-src={src} ref={imgRef} />}
    </div>
  )
}

export default Image

7.樣式

.root //用於設置css作用域,自動生成,使用時如className="styles.root“

:global //内部樣式可直接調用,如className="image-icon"

.root {
  position: relative;
  display: inline-block;
  width: 100%;
  height: 100%;
  :global {
    img {
      display: block;
      width: 100%;
      height: 100%;
    }

    .image-icon {
      position: absolute;
      left: 0;
      top: 0;
      display: flex;
      justify-content: center;
      align-items: center;
      width: 100%;
      height: 100%;
      background-color: #f7f8fa;
    }

    .icon {
      color: #dcdee0;
      font-size: 32px;
    }
  }
}

8.效果實現

思路:使用 useEffect 在组件创建时和销毁时,监听图片元素一旦进入可视区域,就设置它的 src 属性进行加载

import classnames from 'classnames'
import React, { useRef, useEffect, useState } from 'react'
import Icon from '../Icon'
import styles from './index.module.scss'
/**
 * 拥有懒加载特性的图片组件
 * @param {String} props.src 图片地址
 * @param {String} props.className 样式类
 */
type Props = {
  src: string
  className?: string
}
const Image = ({ src, className, ...rest }: Props) => {
  // 对图片元素的引用
  const imgRef = useRef<HTMLImageElement>(null)
  // 是否正在加載
  const [loading, setLoading] = useState(true)
  // 錯誤顯示
  const [error, setError] = useState(false)
  useEffect(() => {
    const ob = new IntersectionObserver(
      // 当被观察的目标进入视口/离开视口就会调用的回調
      (entries) => {
        console.log('working....')
        // 可見
        if (entries[0].isIntersecting) {
        // 設置src
          imgRef.current!.src = src// 非空
          // 观察者停止对圖片的观察
          ob.unobserve(imgRef.current!)// 非空判斷
        }
      },
      { rootMargin: '300px' }//非必寫,超出300px觸發
    )

    ob.observe(imgRef.current!)
    return () => {
      // 停止观察者
      ob.disconnect()
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  return (
    <div className={classnames(styles.root, className)}>
      {/* 正在加载时显示的内容 */}
      {loading && (
        <div className="image-icon">
          <Icon type="iconphoto" />
        </div>
      )}

      {/* 加载出错时显示的内容 */}
      {error && (
        <div className="image-icon">
          <Icon type="iconphoto-fail" />
        </div>
      )}

      {/* 加载成功时显示的内容 */}
      {
        <img
          {...rest}//將Image標簽内配置遍歷到該img標簽中
          alt=""
          onError={() => setError(true)}
          onLoad={() => setLoading(false)}
          data-src={src}//自定義命名
          ref={imgRef}
        />
      }
    </div>
  )
}

export default Image

onError :設置true,配合“error &&”即可控制圖片顯示時顯示的圖片

onLoad : 設置false,配合“loading &&”即可在圖片正在加載時顯示加載的圖片,在加載完后loading為false即可隱藏該加載的圖片

9.調用嬾加載組件

<Image src="xxxx" className="xxxx" />

題外話:雖然我們直接使用ant的image的lazy即可實現,但是學習嘛,瞭解一下底層原理也挺好😘