今天,为我的AI简历项目(https://github.com/weidong-repo/AIResume)的图片加载做一个小的优化。
当图片进入用户视图的时候再进行加载,减少用户访问的时候发送请求数量,优化访问体验。
下面开始记录一下整个流程
IntersectionObserver
是一个浏览器 API,主要用于 监听 DOM 元素是否进入视口(或某个容器),适用于 懒加载、无限滚动、曝光统计 等场景。
const observer = new IntersectionObserver(callback, options);
observer.observe(element); // 观察某个元素
observer.unobserve(element); // 停止观察某个元素
observer.disconnect(); // 断开观察器,释放资源
其中:
options
:配置观察器的参数,例如 触发条件 和 观察区域。
const options = {
root: document.querySelector('.container'), // 观察区域 (默认是视口)
rootMargin: '0px 0px -50px 0px', // 观察区域的外边距(类似 CSS margin)
threshold: [0, 0.5, 1] // 触发回调的可见比例(0=完全不可见,1=完全可见)
};
callback(entries, observer)
:当被观察的元素状态发生变化时,会触发 callback
回调函数。
回调函数callback(entries, observer)
接收两个参数:
entries
:表示当前所有被观察到的元素的信息(比如,在一次滑动页面的时候,有多个图片同时进入视口,这个时候,entries中就包含这些触发的元素)。entries
是一个数组,数组中每个entry
的一些关键属性:
{
time: 12345678, // 触发回调的时间戳
target: element, // 被观察的 DOM 元素
intersectionRatio: 0.5, // 目标元素的可见比例 (0 ~ 1)
isIntersecting: true, // 是否进入观察区域 (true=进入, false=离开)
boundingClientRect: {}, // 目标元素的尺寸 & 位置
intersectionRect: {}, // 目标元素的可见部分信息
rootBounds: {}, // 根容器的尺寸 & 位置
}
observer
:当前 IntersectionObserver
实例。
在 Vue 3 中,自定义指令允许你直接操作 DOM 元素,类似于原生的
v-if
或v-for
,但你可以为它们创建自定义行为。Vue 3 中的指令系统进行了优化,支持全局和局部注册,可以与 TypeScript 很好地配合使用。
在main.ts中全局注册自定义指令
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.directive('focus', {
mounted(el) {
el.focus(); // 在元素挂载到 DOM 后自动获取焦点
}
});
app.mount('#app');
自定义指令的生命周期函数钩子:
beforeMount
:指令在绑定元素的父组件挂载之前调用。
mounted
:指令在元素被挂载到 DOM 后调用。
beforeUpdate
:指令在所在组件的 VNode 更新之前调用。
updated
:指令在所在组件的 VNode 更新之后调用。
beforeUnmount
:指令在元素从 DOM 中移除之前调用。
unmounted
:指令在元素从 DOM 中移除后调用。
传参:
每个钩子函数都会传入不同的参数:
el
:指令所绑定的元素,通常是一个 DOM 元素。binding
:一个对象,包含指令的信息,如参数、值等。vnode
:虚拟节点,包含了 Vue 内部的 VNode 数据。prevVnode
:前一个虚拟节点,只有在更新过程中才可用。一个示例:
app.directive('tooltip', {
mounted(el, binding) {
const tooltipText = binding.value || '默认提示'; // 使用传入的值
const tooltipPosition = binding.arg || 'top'; // 使用动态参数,默认值为 'top'
// 创建 tooltip 元素
const tooltip = document.createElement('span');
tooltip.innerText = tooltipText;
tooltip.style.position = 'absolute';
tooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
tooltip.style.color = 'white';
tooltip.style.padding = '5px';
tooltip.style.borderRadius = '4px';
tooltip.style.visibility = 'hidden';
// 根据修饰符调整显示位置
if (binding.modifiers.bottom) {
tooltip.style.top = '100%';
} else {
tooltip.style.bottom = '100%';
}
// 将 tooltip 插入到目标元素
el.style.position = 'relative';
el.appendChild(tooltip);
// 显示 tooltip
el.addEventListener('mouseenter', () => {
tooltip.style.visibility = 'visible';
});
// 隐藏 tooltip
el.addEventListener('mouseleave', () => {
tooltip.style.visibility = 'hidden';
});
},
});
首先是lazyLoad.ts
,在这里定义了mounted
生命周期,对元素指令绑定的元素进行钩子监听。
并且定义了一个回调函数loadImage
,并且在IntersectionObserver
进行监控触发回调,当图片即将进入视口的时候,触发回调,把图片url替换回图片原本的地址(一开始默认是loading图)
import myImage from '@/assets/imgs/loading.gif';
export default {
mounted(el: HTMLImageElement, binding: any) {
el.src = myImage
const loadImage = (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {
const entry = entries[0];
if (entry.isIntersecting) {
el.src = binding.value;
observer.unobserve(el);
}
};
const observer = new IntersectionObserver(loadImage, { root: null, threshold: 0.1 });
observer.observe(el);
(el as any).__lazyObserver__ = observer; // 绑定 observer 到元素
},
unmounted(el: HTMLImageElement) {
if ((el as any).__lazyObserver__) {
(el as any).__lazyObserver__.disconnect();
}
}
};
在main.ts
中使用,加入下面两行
import lazyLoad from './directives/lazyLoad';
app.directive('lazy', lazyLoad);
组件中直接使用:
<img v-lazy="getTemplateImage(template)" :alt="template.name" class="template-image" />