一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第8天,点击查看活动详情。
提到项目优化一定会有懒加载,尤其是在spa单页面项目中;今天我们用原生的API封装一个图片懒加载组件,很多js的工具库里面的懒加载底层原理都是使用这个IntersectionObserver API,它是浏览器提供的。所以今天我们学会封装图片懒加载组件,日后封装其他懒加载组件都是差不多的!
先看IntersectionObserver的使用方法
var dom = dom元素 //获取需要被监听的dom元素
// 实例化一个观察者
// 它的参数1是一个回调:当被观察的目标进入视口/离开视口就会调用
var observer = new IntersectionObserver((entries)=>{
console.log(entries[0].isIntersecting) //没出现在页面就是false,出现就是true
console.log(entries[0].intersectionRatio)//没出现在页面是0
if(entries[0].isIntersecting) {
}
}, {rootMargin: "100px"}) //这里的意思图片距离页面显示还有100px的时候,就开始加载,
等于一个提前亮操作
// 观察者观察dom
observer.observe(dom)
observer.disconnect() // 停止观察者
observer.unobserve(dom) // 观察者停止对dom的观察
observer.observe(dom) 开启观察者并观察dom
observer.disconnect() // 停止观察者
这两个开启和停止观察者的操作要写在observer实例对象的外面,写在里面实例对象不能自身调自身
我们现在html上写一个例子,先看看我们的布局
<body>
<p style="height:80px">这是一段文字</p>
<p style="height:80px">这是一段文字</p>
<p style="height:80px">这是一段文字</p>
<img height="200" data-src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F1114%2F050421112027%2F210504112027-2-1200.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1652020451&t=ba76aca2e9107ae57e95116314b8309f" />
<!-- https://image.baidu.com/search/detail?ct=503316480&z=0&tn=baiduimagedetail&ipn=d&word=%E5%9B%BE%E7%89%87%E5%8A%A0%E8%BD%BD%E5%A4%B1%E8%B4%A5%E7%9A%84%E5%9B%BE%E7%89%87&step_word=&ie=utf-8&in=&cl=2&lm=-1&st=-1&hd=&latest=©right=&cs=2129842042,3019439823&os=4208003612,901355711&simid=4111145578,827000330&pn=7&rn=1&di=7060663421280190465&ln=1555&fr=&fmq=1649428493065_R&ic=&s=undefined&se=&sme=&tab=0&width=&height=&face=undefined&is=0,0&istype=2&ist=&jit=&bdtype=0&spn=0&pi=0&gsm=0&objurl=https%3A%2F%2Fgimg2.baidu.com%2Fimage_search%2Fsrc%3Dhttp%253A%252F%252Fpic.soutu123.cn%252Felement_origin_min_pic%252F01%252F37%252F85%252F80573c6529bb88f.jpg%2521%252Ffw%252F700%252Fquality%252F90%252Funsharp%252Ftrue%252Fcompress%252Ftrue%26refer%3Dhttp%253A%252F%252Fpic.soutu123.cn%26app%3D2002%26size%3Df9999%2C10000%26q%3Da80%26n%3D0%26g%3D0n%26fmt%3Dauto%3Fsec%3D1652020491%26t%3De435ed17ff4f901c5a8ac711a36b513d&rpstart=0&rpnum=0&adpicid=0&nojc=undefined&dyTabStr=MCwzLDYsMiw0LDEsNSw3LDgsOQ%3D%3D -->
<p style="height:80px">这是一段文字</p>
<p style="height:80px">这是一段文字</p>
<p style="height:80px">这是一段文字</p>
<p style="height:80px">这是一段文字</p>
<p style="height:80px">这是一段文字</p>
<p style="height:80px">这是一段文字</p>
<script>
</script>
</body>
给p标签高度撑开页面,出现滚动条效果,并把图片挤到页面显示下,这样我们可以实现下滑出现图片
这里给src改成自定义属性,是因为默认src属性的话,会默认发请求,为了实现我们懒加载的效果,我们需要当图片出现在页面在通过dom赋值给src再发请求
<img height="200" data-src="https://gimg2.baidu.com/image_search/
接下来通过IntersectionObserver这个API实现懒加载
<script>
let img = document.querySelector('img')
// 第一个参数函数的形参里要传一个数组
let ob = new IntersectionObserver((arr)=>{
if(arr[0].isIntersecting){//等于true时,说明已经观察到图片离展示页面100px
img.src=img.getAttribute('data-src') //在把自定义属性里面存的图片地址赋值给图片src属性
}
// 给图片注册点击事件,当图片加载失败时显示失败图片
img.onerror=function(){
img.src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fhbimg.b0.upaiyun.co m%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' })
ob.observe(img) //开启观察者观察dom
</script>
打开浏览器的Network,我们发现一开始进入页面是,并没有发送请求获取图片
此时我们往下滑动滚轮,当图片离展示页面100px的时候,就已经发送请求获取图片了
这时候图片已经加载完毕,继续往下滑动滚轮就可以看到图片了!
这里我还给图片注册了onerror事件,当图片加载失败时就会显示这张图片,同样也是懒加载效果,还可以定义图片再加载的时候显示等待效果图片,我们下个例子做
这里我们把图片的路径改错,就会触发onerror事件,展示错误的图片
这里先发请求找到错误图片,是因为我代码走过一次,浏览器中有缓存, 所以直接从缓存中读取的,要更快一些
然后重点我们封装一个图片懒加载组件,直接上代码
import classnames from 'classnames'
import { useEffect, useRef, 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 }: Props) => {
// 对图片元素的引用
const imgRef = useRef<HTMLImageElement>(null)
const [isError, setIsError] = useState(false)
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
const ob = new IntersectionObserver((entry) => {
// 图片到了可视区域
if (entry[0].isIntersecting) {
// 给图片赋值地址
imgRef.current!.src = src
// 图片正在加载时显示加载图标
setIsLoading(true)
// 观察者停止对dom的观察
ob.unobserve(imgRef.current!)
}
// 在组建销毁时,停止观察者
return () => {
ob.disconnect()
}
// 提前亮,等于距离还有300px的时候就提前获取图片
}, { rootMargin: '300px' }) //
// 创建实例对象后,观察者观察dom
ob.observe(imgRef.current!)
}, [])
return (
<div className={classnames(styles.root, className)}>
{/* 正在加载时显示的内容 */}
{
// isLoading=true加载图标显示
isLoading && <div className="image-icon">
<Icon type="iconphoto" />
</div>
}
{/* 加载出错时显示的内容 */}
{
// isLoading=true报错图标显示显示
isError && <div className="image-icon">
<Icon type="iconphoto-fail" />
</div>
}
{/* 加载成功时显示的内容 */}
{/* onLoad图片加载成功后的回调 */}
{<img alt="" onError={() => setIsError(true)} onLoad={() => setIsLoading(false)} data-src={src} ref={imgRef} />}
</div>
)
}
export default Image
代码的逻辑并不难,和我们上一个例子基本一样,我们加上了等待图片加载时显示的图片,和加载错误显示的图片,这样用户的提样会更好!
然后需要懒加载的的图片直接导入组件,传递地址过去就行了,对于移动端下拉加载或者长列表中的图片优化很方便
import Image from '@/components/Image/index'
{item.cover.images.map((image, index) => (
<div key={index} className="article-img-wrapper">
<Image src={image}></Image>
{/* <img src={image} alt="" /> */}
</div>
))}