懒加载图片
懒加载图片是 web
中常见的性能优化手段,可以有效的减少非首屏的图片请求,带宽消耗
实现方案
img
元素定义一个 data-src
属性存放图片地址或者普通元素定义一个 data-background-src
属性
- 获取屏幕可视区域的尺寸
- 获取元素到窗口边缘的距离
- 判断元素是否在可视边缘内,如果是则把
data-src
赋值给 src
或者 data-background-src
赋值给 background-image: url()
使用的 API
IntersectionObserver
:提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法,具体使用参考 mdn
scroll
:监听滚动事件,判断目标元素是否在视窗内
编码
目录结构
- 先写一个页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>lazy</title> <style> img{ width: 300px; min-height: 300px; border-radius: 5px; background-color: #ddd; margin: 10px; } .bg{ width: 300px; height: 300px; border-radius: 5px; background-color: #eee; margin: 10px; } .data{ width: 100%; overflow: scroll; } #data{ display: flex; width: 10000px; } </style> </head> <body> <div id="app"></div> <div class="data"> <div id="data"></div> </div> <script> window.onload = function() { let app = document.getElementById("app"); let data = document.getElementById("data"); let src = 'https://p9-passport.byteacctimg.com/img/user-avatar/fbf0dad2f64546b5718cb7e502e92c7b~1000x1000.image' let lazyHtml = new Array(20).fill(0).reduce((prev, cur, index) => { return prev + `<img class='img' index=${index} data-src='${src}?index=${+new Date() + index}'>` }, '') let lazyBackgroundHtml = new Array(20).fill(0).reduce((prev, cur, index) => { return prev + `<div class='bg' index=${index} style='background-position: center' data-background-image='${src}?index=${+new Date() + index}'></div>` }, '') app.innerHTML = lazyHtml + lazyBackgroundHtml data.innerHTML = lazyHtml + lazyBackgroundHtml } </script> <script src="./index.js"></script> </body> </html>
|
- 先写如何调用
1 2 3 4 5 6 7 8 9
| setTimeout(() => { window.lazyImage = LazyImage({ el: '.data' }) window.lazyImage = LazyImage({ el: '#app', }) })
|
- 开始来写
LazyImage
的实现,先把代码骨架写好
1 2 3 4 5 6 7 8 9
| class Lazy { }
function LazyImage(...arg) { return new Lazy(...arg) }
|
- 构造函数中需要去对参数进行提取合并,初始化操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| constructor(options) { options = options || {} const el = options.el || '' this.el = util.querySelector(el) this.wait = options.wait || 500 this.diffTop = 0 this.diffLeft = 0 this.isScrollContainer = options.isScrollContainer || !(this.el.scrollWidth <= this.el.clientWidth) || !(this.el.scrollHeight <= this.el.clientHeight) this.container = this.isScrollContainer ? this.el : window this.observerOption = options.observerOption || { thresholds: [1], root: this.isScrollContainer ? this.el : null, } this.images = [] this.initEvents = getEventFunc().bind(this) this.destroyEvent = () => { } this.update() }
|
getEventFunc
的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| let func = null function getEventFunc() { if (func) return func if (window.IntersectionObserver) { func = function () { let activeImages = [] let timeout let observer = new IntersectionObserver((images) => { activeImages.push(...images) if (timeout) clearTimeout(timeout) timeout = setTimeout(() => { activeImages .filter((image) => image.isIntersecting) .map((image) => image.target) .forEach((image) => { if (this.initImages(image)) { observer.unobserve(image) } }) activeImages = [] }, this.wait) }, this.observerOption) this.images.forEach((image) => observer.observe(image)) return () => { this.images.forEach((image) => observer.unobserve(image)) } } } else { func = function () { let timeout = null let load = () => { if (timeout) clearTimeout(timeout) timeout = setTimeout(() => { this.images.forEach((image) => this.initImages(image)) }, this.wait) } if (this.container !== window) { window.addEventListener('scroll', load) } this.container.addEventListener('scroll', load) return () => { this.container.removeEventListener('scroll', load) window.removeEventListener('scroll', load) } } } return func }
|
update
和 _init
方法实现
1 2 3 4 5 6 7 8 9 10 11
| _init() { this.destroyEvent() this.queryImage() this.destroyEvent = this.initEvents() } update() { return this._init() }
|
- 编写工具方法
util
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| const util = { getVwVh(container) { return { vw: container.innerWidth || window.innerWidth || document.documentElement.clientWidth, vh: container.innerHeight || window.innerHeight || document.documentElement.clientHeight, } }, querySelector(el = '') { return el ? document.querySelector(el) : document.body }, getAttribute(el, name) { return el.getAttribute(name) }, querySelectorAll(el, search) { return el && search ? el.querySelectorAll(search) : [] }, }
;[ ['src', 'getSrc', util.getAttribute], ['data-src', 'getDataSrc', util.getAttribute], ['data-background-image', 'getBackgroundSrc', util.getAttribute], ['style', 'getStyle', util.getAttribute], ['[data-src]', 'queryAllSrc', util.querySelectorAll], ['[data-background-image]', 'queryAllImage', util.querySelectorAll], ].forEach(([key, val, fn]) => { util[val] = (el) => fn(el, key) })
|
queryImage
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| queryImage() { if (!this.el) return this.images = [ ...util.queryAllSrc(this.el), ...util.queryAllImage(this.el), ].filter((el) => { return !!( !util.getSrc(el) && (util.getDataSrc(el) || util.getBackgroundSrc(el)) ) }) }
|
inViewport
判断是否处于视窗内
1 2 3 4 5 6 7 8 9 10 11 12 13
| inViewport(el) { const { vw, vh } = util.getVwVh(this.container) const { top, right, bottom, left } = el.getBoundingClientRect() return ( top - vh < this.diffTop && bottom > this.diffTop && left - vw < this.diffLeft && right > this.diffLeft ) }
|
initImages
加载图片方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| initImages(image) { if (!this.inViewport(image)) return null const src = util.getSrc(image) const dataSrc = util.getDataSrc(image) const dataBackground = util.getBackgroundSrc(image) if (src || (!dataSrc && !dataBackground)) return image if (dataSrc) { image.setAttribute('src', dataSrc) let load = () => { this.images = this.images.filter((img) => img !== image) image.removeAttribute('data-src') image.removeEventListener('load', load) } image.addEventListener('load', load) } if (dataBackground) { image.style = `${util.getStyle( image )}; background-image:url(${dataBackground});` this.images = this.images.filter((img) => img !== image) image.removeAttribute('data-background-image') } return image }
|
- 看看效果
打包
- 使用
rollup
打包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import babel from "rollup-plugin-babel"; import resolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; import { terser } from 'rollup-plugin-terser';
export default { input: 'src/index.js', output: { name: 'LazyImage', file: 'xy-lazyimage.min.js', format: 'umd' }, plugins: [ resolve(), commonjs(), babel({ exclude: 'node_modules/**', runtimeHelpers: true, }), terser() ] }
|
package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| { "name": "xy-lazyimage", "version": "1.0.6", "description": "懒加载图片插件", "main": "src/index.js", "module": "src/index.js", "browser": "xy-lazyimage.min.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "rollup -c" }, "homepage": "https://github.com/Luoyuda/xy-lazyimage", "author": "xiayuchen", "license": "ISC", "devDependencies": { "rollup-plugin-babel": "^4.4.0", "@babel/core": "^7.17.8", "@babel/plugin-transform-runtime": "^7.17.0", "@babel/polyfill": "^7.12.1", "@babel/preset-env": "^7.16.11", "core-js": "^3.21.1", "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-terser": "^7.0.2" }, "keywords": ["lazy", "lazy image", "lazyload"], "dependencies": { }, "files": [ "xy-lazyimage.min.js" ] }
|
源码地址
npm包地址