持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
1.目標
封装一个拥有懒加载功能的图片组件,实现其他組件的图片懒加载
2.懒加载的基本思路
当dom元素进入可视区域时,才去加载它。
如何判断一个dom元素是否进入了可见区域?
- 传统: 获取dom的位置open in new window,手动判断。(距离页面顶部的距离 比较 滚动条卷起的高度,还要考虑元素自己的高度 )
- 现在:直接判断元素进入可视区域的比例
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即可實現,但是學習嘛,瞭解一下底層原理也挺好😘