likes
comments
collection
share

懒加载+图片遮罩层——React项目结构+Js实现方法

作者站长头像
站长
· 阅读数 42

目标效果

是一个以该网页为参考的懒加载实现。

懒加载描述

懒加载的应用场景主要集中在图片资源较多的网页,典型的就是工程建设项目展示、图片搜索网站等。浏览器默认情况下是并行请求的,也就是说,即便当前窗口里只放得下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的图我放在如下了,挺好用的

loading.svg

转载自:https://juejin.cn/post/7244007111424098362
评论
请登录