图片懒加载和瀑布流实现和原理

前言:

图片懒加载通常是修改src而不是控制渲染的首尾索引,这是因为这样控制更方便,不需要更多的dom操作和资源管理,也可以通过占位符的设置提升用户体验。

一.图片懒加载

1.原生html实现

(1) img标签的loading属性

使用 loading="lazy" 的方式来实现一个图片懒加载的功能,并且不依赖我们的 javaScript, 仅仅依靠原生支持以及浏览器的优化,性能上自然不必多说。

然而,非常遗憾的是,在 chrome 上,也在 75 版本之后才有支持,兼容性简直令人望而却步。因此,如果我们想要首先使用原生支持的 lazy-loading, 依然需要配合 javaScript 做一个可用性判断,以决定当前程序要采用何种懒加载方式。

猜想对于海量的图片可能才会出现效果,这里由于图片很少,正常设置图片高度都是一次性加载完了,因此这里把图片的高度设置很高才能看到动态加载的效果。特别注意和css设置有关。

另外就是:浏览器刷新的时候会记住滚动条的位置,因此,这里添加了懒加载也是会从上一次滚动的位置开始加载。

<html>
<body>
    <h1>img loading 属性</h1>
    <style>
        .image-container {
            display: grid;
            grid-template-columns: repeat(1, 1fr);
            gap: 10px;
            margin: 20px;
            width: 300px;
        }
        .image-container img {
            width: 100px;
            height: 800px;
        }
    </style>
    <div class="image-container" id="imageContainer">
        <img src="./112.jpg" alt="北京"  loading="lazy">
        <img src="./113.jpeg" alt="上海" loading="lazy">

        <!-- 屏幕外的图像 -->
        <img src="./114.jpg" alt="武汉"  loading="lazy">
        <img src="./115.jpg" alt="上海"  loading="lazy">
        <img src="./116.jpeg" alt="上海"  loading="lazy">
        <img src="./117.jpeg" alt="上海"  loading="lazy">
        <img src="./118.jpeg" alt="体育公园"  loading="lazy">
    </div>
</body>
</html>

总结:这种原生标签的属性提供的懒加载行为不好控制,具体通过实践表明,加载数量与屏幕的高度,网速,窗口大小的变化有空,但是都不是线性关系,总之不同浏览器对懒加载的具体实现是不同的

(2) 基于 javaScript 来进行元素位置判断,动态替换 src来实现图片懒加载的效果

思路:把图片真正的src放到data-src里面,然后当图片进入可视区域时,将data-src属性的值赋给src属性,触发图片真正的加载,初始渲染需要调一次。

步骤:

(a)CSS Grid 布局:使用 display: grid 和 grid-template-columns: repeat(2, 1fr) 来创建两列的布局。1fr 表示每列占据相等的空间。

(b)图片宽度:设置每个图片的宽度为 100%,确保其在列中自适应宽度。

(c)懒加载功能:懒加载的实现与之前相同,依然通过监听滚动事件和判断图片是否进入视口来加载图片。

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图片懒加载示例</title>
    <style>
        .image-container {
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 10px;
            margin: 20px;
            width: 300px;
        }
        .image-container img {
            width: 100%;
            /* 设置图片宽度 */
            height: auto;
            /* 自适应高度 */
            opacity: 0;
            /* 初始透明度 */
            transition: opacity 3s;
            /* 过渡效果 */
        }
        .image-container img.loaded {
            opacity: 1;
            /* 加载完成后设置为不透明 */
        }
    </style>
</head>
<body>
    <div class="image-container" id="imageContainer"></div>
    <script>
        // 图片数组
        const images = [
            "https://image.watsons.com.cn//upload/415f984f.jpeg?w=828&h=1104&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/5c3e51e4.jpg?w=720&h=960&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/92761043.JPG?w=1000&h=999&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/bef41e67.JPG?w=712&h=534&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/25ab20fe.JPG?w=1000&h=1200&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/31e42833.jpg?w=750&h=750&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/4a3c1788.jpg?w=823&h=1000&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/0a89e6b7.jpg?w=1080&h=1920&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/99253111.jpg?w=1080&h=1920&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/13afe9a7.jpg?x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/98c7c4c3.jpg?w=1210&h=1210&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/54c5d7b4.jpg?w=828&h=991&x-oss-process=image/resize,w_1080",
        ];
        // 获取容器
        const imageContainer = document.getElementById('imageContainer');
        // 创建图片元素并添加到容器
        images.forEach(src => {
            const img = document.createElement('img');
            img.dataset.src = src; // 使用 data-src 属性存储图片 URL
            img.alt = 'Image';
            imageContainer.appendChild(img);
        });
        // 懒加载函数
        const lazyLoadImages = () => {
            const imgElements = document.querySelectorAll('img[data-src]');
            const windowHeight = window.innerHeight;
            imgElements.forEach(img => {
                const rect = img.getBoundingClientRect();
                if (rect.top < windowHeight && rect.bottom > 0) {
                    img.src = img.dataset.src; // 设置图片 src
                    img.classList.add('loaded'); // 添加加载完成的类
                    img.removeAttribute('data-src'); // 移除 data-src 属性
                }
            });
        };
        // 监听滚动事件
        window.addEventListener('scroll', lazyLoadImages);
        // 初始加载
        lazyLoadImages();
    </script>
</body>
</html>

 注意:

这里的加载效果是通过css的透明度和动画来实现的,未到视口时候是透明状态。过渡效果通过css类名来设置,在加载之后,给img添加一个加载完成的类。

此外,这里判断元素是不是到达视口的方式采用的是一般判断方法,对于可视区域不是浏览器整个页面的,可以采用IntersectionObserve来结合判断

这个列数是可以任意设置的,通过css的网格布局属性来设置。

这些设置了datasrc的图片初始是不渲染的,但是有个占位符的宽高,通过设置占位符区域可以自定义来提升用户体验。

注意代码中,每次修改了图片的src之后,需要把之前设置的data-src属性移除,防止下一次重复找到这个dom元素。

拓展:

dataset 属性是一个 JavaScript 的 DOM 属性,用于访问 HTML 元素上所有以 data- 开头的自定义数据属性,属性名中的连字符 - 会被转换为驼峰命名法,常用于存储额外信息。

 二.图片瀑布流

图片的瀑布流实现思路就是维护图片的列数,哪个列的高度小就把图片放到对应的位置。以下是两种方式。

1.已知图片的宽高比

知道图片的宽高比,那么只需要根据屏幕或者布局的宽度算出来图片渲染之后的实际宽度,利用宽高比算出实际高度,然后利用高度进行判断,哪一列高度小就把数据放在那一列,然后完成两个数组的遍历渲染即可。

<template>
    <div class="card-view">
        <div>
            <div class="grid-box" v-for="item in leftItems">
                <img :src="item.cover_image.url"/>
                <p>{{item.title}}</p>
            </div>
        </div>
        <div>
            <div class="grid-box" v-for="item in rightItems">
                <img :src="item.cover_image.url"/>
                <p>{{item.title}}</p>
            </div>
        </div>
    </div>
</template>
<script setup>
import { ref,watch, } from 'vue'
const props = defineProps({
    userTravels: {type:Array,required:true}
})
let leftItems = ref([]);
let rightItems = ref([]);
let leftHeight = ref(0);
let rightHeight = ref(0);

watch(props,(newValue,oldValue)=>{
    // 将初始图片列表按照高度均衡地分配到左右两侧
    const { userTravels } = newValue;
    userTravels.forEach(item => {
        if (leftHeight.value <= rightHeight.value) {
            leftItems.value.push(item);
            leftHeight.value += ((window.innerWidth / 2) * (item.cover_image.height / item.cover_image.width));
        } else {
            rightItems.value.push(item);
            rightHeight.value += ((window.innerWidth / 2) * (item.cover_image.height / item.cover_image.width));
        }
    })
    props.userTravels = userTravels;
})
</script>

<style scoped>
.card-view {
    padding: 0 5px;
    display: grid; 
    grid-template-columns: repeat(2, 1fr);
}

.grid-box {
    img {
        width: 100%;
    }
}
</style>

注意:利用了网格布局,如何分开渲染其中一列,用一个div包起来即可。

实际项目中要考虑间距和边框之类的,会更精确。

(2)不知道图片的宽高(原生html实现)

不知道图片的宽高也没关系,我们可以获取到每一列的dom的,当他渲染了图片之后列自动就会有自己的高度,我们只需要用lie的offesetHeight去和其他列比较就可知道哪个列的高度最低

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>瀑布流布局 - 原生实现</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: Arial, sans-serif;
            background-color: #f0f0f0;
        }
        .container {
            display: flex;
            justify-content: space-between;
            width: 90%;
            margin: 20px auto;
        }
        .column {
            flex: 1;
            margin: 0 10px;
            height: 100%;
        }
        .item {
            margin-bottom: 20px;
            background-color: #fff;
            border-radius: 8px;
            overflow: hidden;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
        }
        .item img {
            width: 100%;
            display: block;
        }
    </style>
</head>
<body>
    <div class="container" id="container">
        <div class="column"></div>
        <div class="column"></div>
        <div class="column"></div>
        <div class="column"></div>
    </div>

    <script>
        images = [
            "https://image.watsons.com.cn//upload/415f984f.jpeg?w=828&h=1104&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/5c3e51e4.jpg?w=720&h=960&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/92761043.JPG?w=1000&h=999&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/bef41e67.JPG?w=712&h=534&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/25ab20fe.JPG?w=1000&h=1200&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/31e42833.jpg?w=750&h=750&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/4a3c1788.jpg?w=823&h=1000&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/0a89e6b7.jpg?w=1080&h=1920&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/99253111.jpg?w=1080&h=1920&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/13afe9a7.jpg?x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/98c7c4c3.jpg?w=1210&h=1210&x-oss-process=image/resize,w_1080",
            "https://image.watsons.com.cn//upload/54c5d7b4.jpg?w=828&h=991&x-oss-process=image/resize,w_1080",
        ];

        // 获取所有列
        const columns = document.querySelectorAll('.column');

        // 加载图片并插入到最短的列中
        function loadImages() {
            images.forEach((imageUrl) => {
                const img = new Image(); // 创建图片对象
                img.src = imageUrl;

                // 图片加载完成后
                img.onload = () => {
                    const item = document.createElement('div');
                    item.className = 'item';
                    item.appendChild(img);

                    // 找到最短的列
                    const shortestColumn = getShortestColumn();
                    shortestColumn.appendChild(item);
                };
            });
        }

        // 获取当前最短的列
        function getShortestColumn() {
            let shortestColumn = columns[0];
            columns.forEach((column) => {
                if (column.offsetHeight < shortestColumn.offsetHeight) {
                    shortestColumn = column;
                }
            });
            return shortestColumn;
        }
// 初始化加载图片 loadImages(); </script> </body> </html>

其中的img.onload不加好像也可以实现,这个有点意思。

核心点就是列的高度:column.offsetHeight

实际项目中可能还要结合第一步骤的图片懒加载来使用,并且列数可能是要动态调整的,还有滚动到底部加载更多等等。

posted @ 2025-02-10 13:27  122www  阅读(105)  评论(0)    收藏  举报