
今天,为我的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" />