el-image 的基本功能有:
- 加载中占位
- 加载失败占位
- 加载成功显示图片
- 图片懒加载
先考虑加载占位的情况,我们不难发现加载有成功、失败、加载中这三种状态,由于加载成功这个状态可以视为其他两种状态的排除,所以实际上我们只需要定义两个状态:
// 默认为加载中状态
loading = ref(true);
// 默认加载没有出错
error = ref(false);
然后提供插槽让用户可以自定义加载中和出错的图片占位:
<template>
<slot v-if="loading">
<span>Loading</span>
</slot>
<slot v-else-if="error">
<span>Error</span>
</slot>
<img v-else :src="src"/>
</template>
页面结构搭好后,就该考虑如何监听图片加载事件了。el-image 的方法很巧妙,通过新建一个 image 对象并设置这个对象的 src 加载图片,然后监听这个对象的加载事件,通过不同的事件更改状态:
loadImage() {
const img = new Image();
// 图片加载成功(但不一定加载完成)
img.addEventLisntener('load', () => {
loading = false;
error = false;
})
// 图片加载失败
img.addEventLisntener('error', () => {
loading = false;
error = true;
})
// 这里省去了 attrs 绑定到 img 上的代码
// ...
img.src = src;
}
于是我们可以在 mounted
事件中用 loadImage
方法加载图片(如果不是懒加载的话)。
懒加载
el-image 的懒加载实现方式是判断图片是否进入滚动容器,如果进入就调用 loadImage
方法。这样的好处是无需指定一个具体的 container(和 IntersectionObserver 不同)。那么如何找到滚动容器和判断图片是否进入滚动容器呢?下面提供一个简化版的实现:
// 获取最近的滚动容器
function getScrollContainer(el) {
let parent = el;
while(parent) {
// 如果已经找到顶了还是没找到滚动容器的话,就直接返回 window
if([window, document, document.documentElement].includes(parent)) {
return window;
}
// isScroll 判断元素是否是滚动元素
// 原理很简单,就是判断元素的 overflow 是否为 auto、overlay 或者 scroll
if(isScroll(parent)) {
return parent;
}
// 继续往上走
parent = parent.parentElement;
}
// 通常情况到这里 parent 一定是 null
return parent;
}
// 判断一个元素是否在另一元素中
// 只要 el 在 container 中可见,就返回 true
// 也就是说这个函数不是判断一个元素是否完全被另一个元素包裹
function isInContainer(el, container) {
let elRect = el.getBoundingClientRect();
let rect;
if(container instanceof Element) {
rect = container.getBoundingClientRect();
} else {
// 如果 container 不是 element,直接设置 rect 为视口 rect
rect = {
top: 0,
left: 0,
right: window.innerWidth,
bottom: window.innerHeight
}
}
return (
elRect.top < rect.bottom &&
elRect.bottom > rect.top &&
elRect.rigth > rect.left &&
elRect.left < rect.right
)
}
有了这两个工具函数,我们还需一个注册懒加载的函数:
function lazyLoadHandler() {
if(isInContainer(container, scrollContainer)) {
loadImage();
// 移除事件监听
scrollContainer.removeEventLisntener('scroll', lazyLoadHandler)
}
}
function addLazyLoadEventListener() {
const scrollContainer = getScrollContainer(container);
// 此处可以有个节流函数
scrollContainer.addEventListener('scroll', lazyLoadHandler);
}
最终结果:
mounted(() => {
if(lazyLoad) {
addLazyLoadEventListener();
// 100ms 后自动调用一次懒加载判断
// 避免用户没有触发 scroll 事件导致图片加载不会触发
setTimeout(() => {
lazyLoadHandler()
}, 100);
} else {
loadImage()
}
})