懒加载+图片遮罩层——React项目结构+Js实现方法
目标效果
是一个以该网页为参考的懒加载实现。
懒加载描述
懒加载的应用场景主要集中在图片资源较多的网页,典型的就是工程建设项目展示、图片搜索网站等。浏览器默认情况下是并行请求的,也就是说,即便当前窗口里只放得下6张图,但你的页面本身有100个<img>
标签,那么浏览器会把网络带宽分给整整100个图。试想一下,即便每张图只有200KB大小,并行加载这么多也会显得很慢。
不过,有一个现象值得注意,那就是上面提到的当前窗口里只放得下6张图。如果对于不出现在初始视窗的图(或任意资源),我不去加载呢?而是等到这些图顶部快要进入视窗时再加载?显然,把带宽集中分配给当前视窗内为数不多的图片资源,加载速度的体验会好很多,这就是懒加载。
React项目结构
├── build
├── public
├── src
│ ├── components
│ └── App.js
└── static
实现思路
如标题所示,是用传统js实现(因为有一个挺新的方法叫IntersectionObserver
,这里不多赘述)
首先,每个元素默认情况下属于一个类名,随便起什么都好,我这里定义className="lazy-load"
当存在lazy-load
这个类名时,元素标签的background-image
就设为none
。
随后,当页面 首次加载、滚动、缩放、移动端缩放 时,就要为页面的元素们执行一个轮询,检查每个元素顶部(或左侧)是否达成一定条件,例如顶部距离视窗100px
时开始加载,那么这段距离就是检测目标。
距离检测完成,对于满足资源加载条件的标签,移除其lazy-load
类名,为了自定义一些别的特效,我们还可以额外添加新的类名,这里我添加了一个loaded
类名。当处于loaded
状态时,就执行资源加载逻辑,把标签对应的图片资源路径加上。
代码实现
import React, { useEffect, useState } from "react";
import {useNavigate} from "react-router-dom";
import cookie from "react-cookies";
import { Menu } from "antd";
import "./index.css"
// 以这个Device组件为例,菜单栏部分不用管,用到了antd组件,主要看图片列表
const Device = (props) => {
const navigator = useNavigate();
// 菜单栏的collapse以及按钮选择
const [collapse, setCollapse] = useState(false);
const [selectedKeys, setSelectedKeys] = useState("device-manage");
const menu_items = [
{
label: "设备管理",
key: "device-manage"
},
{
label: "用户管理",
key: "user-manage"
}
];
// 图片列表
const [imgList, setImgList] = useState([
"/img/logo192.png",
"/img/logo192.png",
"/img/logo192.png",
"/img/logo192.png",
"/img/logo192.png",
"/img/logo192.png",
"/img/logo192.png",
"/img/3.jpg",
"/img/logo192.png",
"/img/logo192.png",
"/img/logo192.png",
"/img/1.jpg",
"/img/logo192.png",
"/img/logo192.png",
"/img/logo192.png",
"/img/logo192.png",
"/img/2.jpg",
"/img/logo192.png",
"/img/logo192.png",
"/img/logo192.png",
])
useEffect(()=>{
watchLazyLoad();
}, []);
useEffect(()=>{
window.removeEventListener("resize", watchWindowSize)
window.addEventListener("resize", watchWindowSize)
return ()=>{
window.removeEventListener("resize", watchWindowSize)
}
}, [collapse])
// 移动端适配,监控窗口变化
function watchWindowSize() {
var w = window.innerWidth;
if(w>520)
setCollapse(false);
else if(w<=520 && !collapse)
setCollapse(true);
}
// 检查图片位置,并调整资源加载
function watchLazyLoad() {
var lazyloadImages = document.querySelectorAll(".device-img.lazy-load");
var lazyloadThrottleTimeout;
loadImg();
function lazyload() {
if (lazyloadThrottleTimeout) {
clearTimeout(lazyloadThrottleTimeout);
}
lazyloadThrottleTimeout = setTimeout(loadImg, 200);
}
function loadImg() {
lazyloadImages.forEach(function(img) {
if (isInViewPort(img)) {
img.classList.remove('lazy-load');
img.classList.add('loaded');
img.style.backgroundImage = "url("+img.getAttribute('data-src')+")";
}
});
if (lazyloadImages.length == 0) {
document.removeEventListener("scroll", lazyload);
window.removeEventListener("resize", lazyload);
window.removeEventListener("orientationChange", lazyload);
}
}
// 检查元素是否处于视窗内
function isInViewPort(element) {
const viewWidth = window.innerWidth || document.documentElement.clientWidth;
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
const {
top,
right,
bottom,
left,
} = element.getBoundingClientRect();
// 这个是判断元素完整的在视口内
/* return (
top >= 0 &&
left >= 0 &&
right <= viewWidth &&
bottom <= viewHeight
); */
// 这个是判断元素刚进入视窗,我设置的是当元素顶部距离视窗底部还有100px时就要开始加载
return (
top >= 0 &&
top <= viewHeight+100 &&
left >= 0
);
}
document.addEventListener("scroll", lazyload);
window.addEventListener("resize", lazyload);
window.addEventListener("orientationChange", lazyload);
}
return (<>
<div className="device-display">
<div className="device-menu-bar">
<div style={{width: "100%", backgroundColor: "#fff"}}>
<div className="collapse-menu" onClick={()=>setCollapse(!collapse)}>
<div className="triangle"
style={{
position: "absolute",
left: "calc(50% - 10px)",
top: collapse?10:0,
transform: collapse?"rotate(0deg)":"rotate(180deg)"
}}/>
</div>
</div>
<div style={{height: collapse?0:"auto", overflow: "hidden"}}>
<Menu items={menu_items} selectedKeys={selectedKeys}
onSelect={v=>setSelectedKeys(v.key)}/>
</div>
</div>
<div className="device-detail-wrapper">
<ul className="device-detail-list">
{
imgList.map(img=>{
return (
<li>
<a href="#">
<div className="device-img lazy-load"
data-src={img}
style={{
position: "relative",
paddingTop: "69.3%",
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
backgroundPosition: "center"
}}></div>
</a>
</li>
)
})
}
</ul>
</div>
</div>
</>)
};
export default Device;
需要注意的是,图片我是以background-image方式放在了一个div里。外层的父元素a标签会一直运行一个遮罩层,当图片未加载时展示加载中的特效,使整个过程更丝滑。遮罩层思路也是目前比较主流的实现方案。
/* 展区 */
.device-display{
padding-top: 60px;
width: 100%;
height: 100vh;
}
/* 菜单栏区域 */
.device-menu-bar{
min-width: 160px;
height: 100%;
float: left;
overflow: hidden;
transition: height 0.3s;
}
/* 菜单栏collapse */
.collapse-menu{
display: none;
position: relative;
height: 30px;
margin: 0 5px;
border-radius: 5px;
text-align: center;
cursor: pointer;
transition: background-color 0.3s;
}
.collapse-menu:hover{
background-color: #e9f1f7;
}
/* 详情区域 */
.device-detail-wrapper{
width: calc(100% - 160px);
height: 200%;
padding: 24px 48px;
float: left;
background-color: cadetblue;
}
/* 图片展示列表 */
.device-detail-list{
width: 100%;
height: 100%;
background-color: aliceblue;
}
.device-detail-list li{
float: left;
width: 33%;
height: auto;
padding: 0 5px;
}
.device-detail-list li a{
display: block;
width: 100%;
position: relative;
}
.device-detail-list li a::before{
content: '';
animation: loaderAnimation .8s infinite linear;
background: url(../../../public/img/loading.svg) no-repeat 50%;
position: absolute;
left: 50%;
top: 50%;
width: 40px;
height: 40px;
margin: -20px 0 0 -20px;
}
.device-img{
transition: opacity 0.3s;
}
.lazy-load{
background-image: none;
}
.device-img.lazy-load{
opacity: 0;
}
.device-img.loaded{
opacity: 1;
}
@media screen and (max-width: 520px) {
.device-display{
padding-top: 48px;
}
.device-menu-bar{
position: fixed;
width: 100%;
height: auto;
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.1);
}
.collapse-menu{
display: block;
}
.device-detail-wrapper{
width: 100%;
padding: 12px 12px;
margin: 48px 0 0;
}
}
@media screen and (max-width: 780px) {
.device-detail-list li{
width: 50%;
}
}
// 定义遮罩层关键帧
@keyframes loaderAnimation {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg)
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg)
}
}
这个loading.svg的图我放在如下了,挺好用的
转载自:https://juejin.cn/post/7244007111424098362