普通视图

发现新文章,点击刷新页面。
昨天 — 2025年7月1日首页

🧠 Vue 懒加载实践:为什么使用 IntersectionObserver + Promise?它解决了哪些性能问题?

作者 梦语花
2025年7月1日 17:46

📌 背景介绍

在现代 Web 开发中,页面性能优化是一个非常重要的课题。尤其是在数据量大、内容多的场景下(如报表系统、商品列表页、图片墙等),如果所有内容都在页面初始化时一次性加载完成,会导致:

  • 页面加载速度慢;
  • 用户体验差;
  • 流量浪费(尤其是移动端);
  • 服务器压力大;

为了解决这些问题,我们引入了 懒加载技术(Lazy Load)


🧩 示例解析:懒加载 + 动态渲染

以下是我们要分析的核心组件结构:

vue
深色版本
<template>
  <div class="container w200px h400px mb20px bg-red">
    <!-- 使用 v-has-views 指令实现懒加载 -->
    <div v-has-views="{ threshold: 0.5, Lazy: true, callback: handleImageLoad }">
      {{ dataList }}
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const dataList = ref('');

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = true;
      if (success) {
        resolve({ data: '这是从服务器获取的数据', status: 200 });
      } else {
        reject(new Error('无法获取数据'));
      }
    }, 500);
  });
}

const handleImageLoad = async (flag) => {
  if (flag) {
    const res = await fetchData();
    dataList.value = res.data;
  }
}
</script>

✅ 核心功能点:

技术 功能
v-has-views 指令 判断元素是否进入视口
IntersectionObserver API 实现懒加载逻辑
Promise 异步请求 模拟真实数据获取过程
ref 响应式绑定 数据变化自动更新视图

🔍 为什么要用 IntersectionObserver?

传统的判断元素是否可视的方法是通过 window.scroll + getBoundingClientRect() 来实现的。但这种方式存在两个严重问题:

❌ 缺陷:

  1. 频繁触发 scroll 事件,性能开销大
  2. 需要手动管理监听器和节流机制

IntersectionObserver 是浏览器原生提供的 API,专门用于监听一个元素与视口或父容器的交叉状态,具备以下优势:

优势 描述
高性能 不依赖 scroll 事件,由浏览器内部优化
简洁易用 只需注册一次观察器即可监听多个元素
支持阈值控制 可设置元素多少比例可见后才触发回调

💡 为什么要封装成自定义指令?

Vue 的核心理念之一就是“关注分离”。把懒加载逻辑封装成自定义指令 v-has-views,有以下好处:

✅ 优点:

好处 描述
复用性强 在任意组件中都可以直接调用 v-has-views
逻辑解耦 模板层不关心实现细节,只负责绑定行为
易于维护 如果未来要换方案,只需修改指令一处
生命周期可控 可以在 mounted 和 unmounted 中处理资源回收

🚀 为什么要用 Promise + async/await?

在这个示例中,我们模拟了一个异步请求 fetchData,并通过 async/await 实现异步流程控制。

✅ 为什么这样设计?

  • 模拟真实网络请求:延迟加载数据,模拟真实业务场景。
  • 避免阻塞主线程:异步操作不会影响页面正常渲染。
  • 可扩展性好:后续可以轻松替换为真实的 API 请求。
  • 清晰的错误处理:可以通过 .catch() 或 try/catch 控制异常流程。

🎯 解决了什么问题?

问题 解决方式 效果
页面加载慢 懒加载未展示的内容 提升首屏加载速度
内容冗余加载 只加载用户能看到的部分 减少不必要的请求
代码重复复杂 封装成指令复用 提高开发效率
用户体验差 按需加载内容 更流畅的交互体验

🧩 应用场景举例

这个懒加载方案非常适合以下几种场景:

场景 说明
图片墙、商品列表 只有图片进入视口才加载真实地址
表格/报表页 滚动到底部再加载下一页数据
视频/音频播放器 用户滚动到视频区域后再加载资源
动态表单 子表或复杂字段按需加载

📦 拓展:结合 DmoBox 渲染报表列表

在完整页面中,你可能看到类似这样的结构:

html
深色版本
<ul>
  <DmoBox></DmoBox>
  <DmoBox></DmoBox>
  <DmoBox></DmoBox>
  <!-- 循环生成多个卡片 -->
</ul>

每个 <DmoBox> 组件都可以使用 v-has-views 指令,确保只有当用户滚动到该卡片位置时,才会发起网络请求并显示具体内容,从而大大提升整体性能。


✅ 总结:这种写法的价值在哪?

价值维度 说明
性能优化 按需加载减少初始请求,加快首屏渲染
代码结构 指令封装让逻辑清晰,易于复用
用户体验 内容随滚动逐步展现,交互更自然
可维护性 所有懒加载逻辑统一管理,便于后期升级

🚀 后续建议优化方向

如果你希望进一步完善这套懒加载系统,还可以考虑:

  • 添加 loading 状态提示;
  • 加入错误重试机制;
  • 对加载失败的元素做兜底处理;
  • 结合骨架屏(Skeleton Screen)提升体验;
  • 支持动态配置加载阈值(threshold);
  • 自动取消未完成的异步请求(AbortController);

如果你正在开发一个低代码平台、报表系统、或是企业级管理系统,这种懒加载方案是非常实用且高效的。希望这篇文章能帮助你理解背后的设计思路,并应用到实际项目中!

如需我帮你封装成完整的懒加载组件库、或提供 Vue3 + Vite + UnoCSS 的模板工程结构,也可以继续提问 😊

🧱 优雅封装 Axios 请求:从错误处理到统一响应

作者 梦语花
2025年7月1日 17:10

在前端开发中,我们经常需要与后端 API 进行交互。为了提高代码的可维护性、减少重复逻辑,并提升开发效率,对 axios 的请求进行合理的封装是非常有必要的。

本文将带你一步步了解如何通过一个通用的 to 函数,优雅地封装 axios 请求,实现:

  • 统一处理业务异常;
  • 简化异步调用流程;
  • 避免层层嵌套的 if (res.code !== 0) 判断;
  • 提高代码可读性和复用性。

🧩 背景问题:传统写法的痛点

假设你有一个接口调用如下:

js
深色版本
let res = await getMerchantOrder(data);
if (res.code != 0) {
  showToast(res.msg);
  return false;
}

这种写法虽然功能正常,但存在几个明显的问题:

  1. 每个请求都需要手动判断 res.code
  2. 错误提示分散,不易统一管理
  3. 返回值结构不一致,不利于后续处理
  4. 难以集中处理网络层和业务层的错误

这会导致你的业务代码中充斥大量“防御性判断”,严重影响可读性。


✨ 解决方案:使用 to 函数统一处理请求结果

我们可以创建一个名为 to 的辅助函数,用于包装任何基于 Promise 的请求(如 axios),并返回一个标准格式的 [error, data] 结构。

🔨 实现代码如下:

ts
深色版本
/**
 * 将 Promise 包装成 [error, data] 形式,简化异步操作错误处理
 * @param {Promise} promise - 需要包装的 Promise 对象
 * @param {*} errorExt - 可选,附加的错误信息对象
 * @returns {Array} [error, data]
 */
export const to = async (promise, errorExt) => {
  try {
    const res = await promise;

    // 如果业务状态码不是成功状态(例如 code !== 0)
    if (res?.code !== 0) {
      const errorMessage = res?.msg ?? '获取数据失败';
      showToast(errorMessage);
      return [new Error(errorMessage), null];
    }

    // 成功时返回 null 错误 + 数据
    return [null, res.data];

  } catch (err) {
    // 捕获异常并返回 [error, null]
    let parsedError = err;

    if (errorExt) {
      parsedError = Object.assign({}, err, errorExt);
    }

    return [parsedError, null];
  }
};

💡 注意:这里的 showToast 是你项目中已有的 UI 提示方法,比如 uni.showToastElMessage 或自定义 Toast 工具。


📦 使用方式:简洁又直观

现在你可以这样使用:

ts
深色版本
const [err, data] = await to(getMerchantOrder(data));

if (err) {
  console.error('请求失败:', err.message);
  return;
}

// 正常处理 data
console.log('订单数据:', data);

✅ 优势总结:

特性 描述
✅ 统一错误处理 所有错误都在 err 中返回
✅ 清晰结构 返回 [err, data],无需 try/catch 嵌套
✅ 减少冗余 不再需要 if (res.code !== 0) 处理
✅ 可扩展性强 支持自定义错误信息注入

🧪 示例场景对比

❌ 原始写法:

js
深色版本
async function fetchOrder() {
  try {
    const res = await getMerchantOrder(data);
    if (res.code !== 0) {
      showToast(res.msg);
      return;
    }
    // do something with res.data
  } catch (err) {
    showToast('网络异常');
  }
}

✅ 使用 to 后:

js
深色版本
async function fetchOrder() {
  const [err, data] = await to(getMerchantOrder(data));

  if (err) {
    // 统一错误提示 + 日志记录
    console.error('请求出错', err.message);
    return;
  }

  // 直接处理 data
  console.log('订单数据:', data);
}

是不是更加清爽了?


🧰 高级用法:结合 TypeScript 更加安全

如果你使用的是 TypeScript,可以为 to 添加类型支持:

ts
深色版本
type ResponseData<T> = {
  code: number;
  msg?: string;
  data?: T;
};

export async function to<T>(promise: Promise<ResponseData<T>>, errorExt?: any): Promise<[Error | null, T | null]> {
  try {
    const res = await promise;

    if (res.code !== 0) {
      const msg = res.msg || '获取数据失败';
      showToast(msg);
      return [new Error(msg), null];
    }

    return [null, res.data as T];
  } catch (err) {
    const error = errorExt ? Object.assign({}, err, errorExt) : err;
    return [error, null];
  }
}

这样在 IDE 中就可以获得完整的类型提示和自动补全支持!


🧠 总结:让请求更优雅、更可控

通过对 Axios 请求的封装,我们实现了:

  • 统一的错误处理机制
  • 标准化的数据结构返回
  • 更清晰的业务代码逻辑
  • 更强的可维护性和可测试性

无论你是开发小程序、Web 应用,还是构建中后台系统,这样的封装都能极大提升开发体验和代码质量。


📚 扩展建议

如果你希望进一步增强这个封装工具,还可以考虑加入以下功能:

  • 自动重试机制(如网络失败时 retry);
  • 请求拦截器/响应拦截器;
  • 全局错误上报;
  • 接口 Mock 支持;
  • 请求缓存策略;
  • 支持取消请求(AbortController);

📌 最后提醒:
不要把所有请求都写得“千篇一律”,封装是手段,统一和规范才是目的


如需我帮你生成一个完整的 Axios 封装模块(包含拦截器、TypeScript 支持、Mock 等),欢迎继续提问 👍

你可以将这篇文章发布到:

  • 掘金 / CSDN / 博客园 / 知乎 / 微信公众号 / Notion / 内部 Wiki
  • 或者作为团队编码规范的一部分共享给同事
❌
❌