插件组件封装系列:图片懒加载的插件封装

534 阅读4分钟

本文所有代码-github(src/demo/example)

图片延迟加载的意义:

项目中,如果一开始加载页面,就把所有的真实图片也去加载,不论是从网络消耗上,还是从页面渲染上都是非常的消耗性能的,导致加载过慢。

真实开发中,我们一般首次渲染,不去渲染真实的图片,把图片部分用一个默认的盒子占位(或者放一个默认的正在加载中背景图)然后当img标签完全出现在视口当中再进行图片加载。

实现demo

方法一: getBoundingClientRect

传统方案

原理:借用api getBoundingClientRect ,盒子底边距离视口上面的距离 bottom 小于等于视口的高度(屏幕的高度),就算完全出来了。

<style>
    htmlbody {
        height300%;
    }

    。lazyImageBox {
        position: absolute;
        left50%top1500pxtransform: translateX(-50%);
        width400pxheight300pxbackground: url("。/images/default。gif") no-repeat center center #EEE;
    }

    。lazyImageBox img {
        width100%height100%opacity0transitionopacity3s;
    }
</style>


<div class="lazyImageBox">
    <img src="" alt="" lazy-image="images/12.jpg">
</div>
function throttle(func, wait = 500) {
            let timer = null,
                previous = 0;
            return function anonymous(...params) {
                let now = new Date(),
                    remaining = wait - (now - previous);
                if (remaining <= 0) {
                    clearTimeout(timer);
                    timer = null;
                    previous = now;
                    func.call(this, ...params);
                } else if (!timer) {
                    timer = setTimeout(() => {
                        clearTimeout(timer);
                        timer = null;
                        previous = new Date();
                        func.call(this, ...params);
                    }, remaining);
                }
            };
        }

        let lazyImageBox = document.querySelector('.lazyImageBox'),
            lazyImage = lazyImageBox.querySelector('img');

        const singleLazy = function singleLazy() {
            let trueImg = lazyImage.getAttribute('lazy-image');
            lazyImage.src = trueImg;
            lazyImage.onload = () => {
                // 真实图片加载成功
                lazyImage.style.opacity = 1;
            };
            lazyImageBox.isLoad = true;
        };

        const lazyFunc = function lazyFunc() {
            console.log('OK');
            // 防止重复处理
            if (lazyImageBox.isLoad) return;
            let A = lazyImageBox.getBoundingClientRect().bottom,
                B = document.documentElement.clientHeight;
            if (A <= B) {
                singleLazy();
            }
        };

        setTimeout(lazyFunc, 1000);
        // window.onscroll = lazyFunc; 
        //默认浏览器会在最快的反应时间内,监听到scroll事件的触发,从而执行lazyFunc这个方法,这样导致触发频率太高了
        //节流处理
        window.onscroll = throttle(lazyFunc);

方法二: IntersectionObserver DOM监听器

原理:监听器 IntersectionObserver 会监听一个或者多个DOM元素和可视窗口的交叉信息。传入一个函数,当信息变化,执行回调函数,传入参数 changeschanges 一个数组,包含所有监听的DOM元素和视口的交叉信息。例如第一个监听的元素, changes[0].isIntersecting 表示是否出现在视口中。默认第一次加载完成会触发一次,当出现在视口中又会触发一次,当消失在视口中,又会触发一次。

第二个参数是配置,即出现在视口多少的时候触发回调函数。例如 {threshold: [0,0.5]} 会在刚出现时触发,出现到一半时触发。

不需要节流,这个监听器内部自动进行了优化操作。

根据这个原理,代码如下

let lazyImageBox = document.querySelector('.lazyImageBox'),
            lazyImage = lazyImageBox.querySelector('img');
        const singleLazy = function singleLazy() {
            let trueImg = lazyImage.getAttribute('lazy-image');
            lazyImage.src = trueImg;
            lazyImage.onload = () => {
                lazyImage.style.opacity = 1;
            };
        };
        // 使用DOM监听器 IntersectionObserver:监听一个或者多个DOM元素和可视窗口的交叉信息
        let ob = new IntersectionObserver(changes => {
            // changes是一个数组,包含所有监听的DOM元素和视口的交叉信息
            let item = changes[0],
                {
                    isIntersecting,
                    target//目标DOM
                } = item;
            if (isIntersecting) {
                // 完全出现在视口中了
                singleLazy();
                ob.unobserve(lazyImageBox); //加载真实图片后,移除对盒子的监听
            }
        }, {
            threshold: [1]
        });
        ob.observe(lazyImageBox);
        // ob.observe(lazyImageBox); //默认监听的时候是去重的,不需要重复坚挺

这个api在ie不兼容,在移动端是主要的应用方式。我们封装插件的时候,采用这种方式实现

封装成插件

封装的一些原则:

  • 易用性
    • 调用简单
    • 不需要太多的依赖(最好是零依赖)
    • 各种容错处理和完善的错误提示
    • 详细的说明文档和各种情况的参考DEMO
  • 强大
    • 功能强大,项目中常现的效果,基本都可以支持
    • 适配更多的需求
    • 更多的用户自定义扩展(样式/功能)
  • 升级及向后兼容(学习成本低)
    • 高性能(性能优化、轻量级(代码少、体积小))
    • 可维护性(各种设计模式的应用)

首先使用工厂模式,让导出的函数即可以当成一个类执行,也可以当做普通函数执行。当作普通函数执行的时候,也可以创造了它本身这个类的一个实例

image。png

const lz = LazyImage()
const lz = new LazyImage()

二者作用一样

完整封装代码,具体的逻辑都在注释中:

(function () {
    function LazyImage(options) {
        //这样即能使用函数直接生成实例,又能当作构造函数使用
        return new LazyImage.prototype.init(options);
    }

    LazyImage.prototype = {
        constructor: LazyImage,
        init: function init(options) {//使用jQuery的工厂模式,这个init里的逻辑,其实就相当于原来的构造函数中的逻辑,因为最后要new init

            // init params 合并config
            options = options || {};
            let defaults = {
                context: document,
                attr: 'lazy-image',
                threshold: 1,
                speed: 300,
                callback: Function.prototype
            };
            let config = Object.assign(defaults, options)
            // 把信息挂在到实例上:在其它方法中,基于实例即可获取这些信息
            this.config = config;

            this.imageBoxList = [];

            // 创建监听器
            const oboptions = {
                threshold: [config.threshold]
            };
            this.ob = new IntersectionObserver(changes => {
                changes.forEach(item => {
                    let {
                        isIntersecting,
                        target
                    } = item;
                    if (isIntersecting) {
                        this.singleHandle(target);
                        this.ob.unobserve(target);//已经加载过了,就取消监听
                    }
                });
            }, oboptions);
            this.observeAll();//监听所有
        },
        // 单张图片的延迟加载
        singleHandle: function singleHandle(imgBox) {
            let config = this.config,
                imgObj = imgBox.querySelector('img'),
                trueImage = imgObj.getAttribute(config.attr);
            imgObj.src = trueImage;
            imgObj.removeAttribute(config.attr);
            imgObj.onload = () => {
                imgObj.style.transition =  ` opacity ${config.speed}ms ` ;
                imgObj.style.opacity = 1;
                // 回调函数->插件的生命周期函数「回调函数 & 发布订阅」
                config.callback.call(this, imgObj);
            };
        },
        // 监听需要的DOM元素
        observeAll(refresh) {
            let config = this.config,
                allImages = config.context.querySelectorAll( ` img[${config.attr}] ` );
            [].forEach.call(allImages, item => {
                let imageBox = item.parentNode;
                //list里面已经监听了这个盒子,就不监听了
                if (refresh && this.imageBoxList.includes(imageBox)) return;
                //还没监听的,放到list里面,然后监听,以后用来和refresh对比
                this.imageBoxList.push(imageBox);
                this.ob.observe(imageBox);//监听盒子
            });
        },
        // 刷新:获取新增的需要延迟加载的图片,做延迟加载
        refresh: function refresh() {
            this.observeAll(true);
        }
    };
    //因为我们最后new的是init,所以需要把LazyImage.prototype指定到init上
    LazyImage.prototype.init.prototype = LazyImage.prototype;

    if (typeof window !== "undefined") {
        window.LazyImage = LazyImage;
    }
    if (typeof module === "object" && typeof module.exports === "object") {
        //支持commonjs和es6module规范
        module.exports = LazyImage;
    }
})();

写完之后,使用webpack进行打包,导出一个经过压缩后的min.js版本供使用

使用

引入:

<script src="../dist/LazyImage.min.js"></script>

也支持commonjs和es6module规范

在需要加载的 img 标签上添加 'lazy-image' (默认,可自行修改)属性。然后将 img 标签的 opacity 设置为0,可根据需要添加默认的占位符。执行这个方法 LazyImage() 就可以把页面中需要延迟加载的图片做延迟加载。

支持自定义配置:

  • contextdocument 指定上下文
  • attr'lazy-image' 具备哪个属性的 img 需要做延迟加载(属性值是真实图片地址)
  • threshold1 何时出现在视口中再出发加载 1 代表完全, 0 代表刚出现
  • speed300 出现真实图片动画的过渡时间
  • callbackFunction.prototype 图片加载成功后触发的回调函数

支持的方法:

  • refresh() :对所有新加入的图片进行重新的懒加载设置

例如:

const lz = LazyImage({
    threshold:0.5,
    context:box
});
//新加入图片dom后
lz.refresh()

例子

可以去仓库 example文件夹下查看例子

查看例子-gitPage

对所有图片进行懒加载设置,并且滚动到底部后重新加入图片,并对新的图片进行懒加载设置

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>图片懒加载使用例子</title>
    <style>
        .wrapper {
            width: 236px;
            height: 420px;
            margin: 0 auto;
            background: url(./images/default.gif) center center no-repeat;
            padding-bottom: 300px;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        .wrapper img {
            opacity: 0;
        }
    </style>
</head>
<body>
<div class="img-area">
    <div class="wrapper">
        <img src="" alt="" lazy-image="./images/1.jpg">
    </div>
    <div class="wrapper">
        <img src="" alt="" lazy-image="./images/1.jpg">
    </div>
    <div class="wrapper">
        <img src="" alt="" lazy-image="./images/1.jpg">
    </div>
</div>
<div id="bottom">
    bottom
</div>
<script src="../dist/LazyImage.min.js"></script>
<script>
    const imgArea = document.querySelector('.img-area')
    //懒加载使用:
    const lz = LazyImage({
        threshold: 0.5,
        speed: 1000,
        callback: function (target) {
            console.log(this, target)
        }
    })

    //滚动到底部加载更多,并且依然对新增的div进行懒加载处理
    const bottomOb = new IntersectionObserver((changes) => {
        const {isIntersecting, target} = changes[0]
        if(isIntersecting){
            console.log('滚动到底部了,加载更多')
            const div = document.createElement('div')
            div.classList.add('wrapper')
            div.innerHTML = `<img src="" alt="" lazy-image="./images/1.jpg">`
            imgArea.appendChild(div)
            lz.refresh()//对新加的DOM进行懒加载处理
        }
    }, {
        threshold: [0]
    })
    bottomOb.observe(document.querySelector('#bottom'))


</script>
</body>
</html>

本文所有代码-github(src/demo/example)