如何设计一个 Vue 3 `Composition API` 的自定义 Hook,用于处理异步数据加载,并包含 loading 和 error 状态管理。

各位靓仔靓女,老少爷们,晚上好!今天咱们来唠唠 Vue 3 Composition API 里自定义 Hook 的那些事儿,重点是搞定异步数据加载,还要优雅地管理 loading 和 error 状态。保证你听完之后,腰不酸了,腿不疼了,写代码也更有劲儿了!

开场白:Hook 的魅力与必要性

在Vue 2 的 Options API 里,我们吭哧吭哧地把 data、methods、computed、watch 塞到一个对象里。代码量少的时候还行,一旦组件变得复杂,代码就像一团乱麻,维护起来那叫一个痛苦。这时候,Composition API 就闪亮登场了。它允许我们把相关的逻辑提取出来,放到一个个独立的函数里,也就是 Hook。

Hook 的好处嘛,那是杠杠的:

  • 代码复用性爆表: 同一个逻辑,在不同的组件里随便用。
  • 可读性大幅提升: 逻辑清晰,一眼就能看明白。
  • 维护性蹭蹭上涨: 修改一个地方,影响范围可控。

今天咱们要讲的自定义 Hook,就是利用 Composition API,把异步数据加载的逻辑封装起来,让你的组件专注于展示数据,而不是操心数据怎么来。

正文:打造你的专属异步数据加载 Hook

咱们的目标是创建一个 Hook,它能:

  1. 发起异步请求(比如用 fetch 或者 axios
  2. 管理 loading 状态(请求中/请求完成)
  3. 处理 error 状态(请求失败)
  4. 返回数据和状态给组件

先来个简单的框架:

// useAsyncData.js
import { ref, onMounted } from 'vue';

export function useAsyncData(url) {
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);

  onMounted(async () => {
    //  你的异步请求逻辑
  });

  return {
    data,
    loading,
    error,
  };
}

解释一下:

  • useAsyncData:这就是我们的 Hook 函数,接收一个 url 参数,表示要请求的地址。
  • dataloadingerror:这三个 ref 变量用来存储数据、加载状态和错误信息。ref 是 Vue 3 响应式系统的核心,当它们的值发生变化时,使用这个 Hook 的组件会自动更新。
  • onMounted:Vue 3 的生命周期钩子,类似于 Vue 2 的 mounted。在这里发起异步请求,确保组件挂载后才开始加载数据。

接下来,往 onMounted 里面填充异步请求的逻辑:

// useAsyncData.js (完整版)
import { ref, onMounted } from 'vue';

export function useAsyncData(url) {
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);

  const fetchData = async () => {
    loading.value = true;
    error.value = null; // 重置错误信息

    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      data.value = await response.json();
    } catch (e) {
      error.value = e;
    } finally {
      loading.value = false;
    }
  };

  onMounted(fetchData);

  return {
    data,
    loading,
    error,
    refetch: fetchData, // 添加一个手动重新加载数据的函数
  };
}

代码解读:

  • fetchData:一个异步函数,封装了 fetch 请求的逻辑。
  • loading.value = true:请求开始前,把 loading 设置为 true,告诉组件正在加载数据。
  • error.value = null:请求前重置错误信息,避免显示上次的错误。
  • try...catch...finally:标准的 try...catch 结构,用于捕获请求过程中可能出现的错误。
  • response.ok:检查 HTTP 状态码,如果不是 200-299 范围内的,就抛出一个错误。
  • response.json():把响应体解析成 JSON 格式。
  • error.value = e:如果请求失败,把错误信息保存到 error 变量里。
  • loading.value = false:请求结束后,无论成功还是失败,都要把 loading 设置为 false
  • refetch: fetchData:导出一个 refetch 函数,允许组件手动重新加载数据。这个在需要手动刷新数据时非常有用。

如何使用这个 Hook?

在你的 Vue 组件里,像这样使用它:

<template>
  <div>
    <p v-if="loading">Loading...</p>
    <p v-if="error">Error: {{ error.message }}</p>
    <div v-if="data">
      <h1>{{ data.title }}</h1>
      <p>{{ data.body }}</p>
    </div>
    <button @click="refetch" :disabled="loading">Refresh</button>
  </div>
</template>

<script>
import { useAsyncData } from './useAsyncData';
import { ref, onMounted } from 'vue';

export default {
  setup() {
    const apiUrl = 'https://jsonplaceholder.typicode.com/posts/1'; // 随便找个 API
    const { data, loading, error, refetch } = useAsyncData(apiUrl);

    return {
      data,
      loading,
      error,
      refetch,
    };
  },
};
</script>

解释:

  • import { useAsyncData } from './useAsyncData':引入我们自定义的 Hook。
  • const { data, loading, error, refetch } = useAsyncData(apiUrl):调用 Hook,并解构返回的值。
  • v-if="loading":当 loadingtrue 时,显示 "Loading…"。
  • v-if="error":当 error 不为 null 时,显示错误信息。
  • v-if="data":当 data 不为 null 时,显示数据。
  • @click="refetch":点击按钮时,调用 refetch 函数重新加载数据。
  • :disabled="loading":当 loadingtrue 时,禁用按钮,防止用户重复点击。

进阶:更强大的 Hook

上面的 Hook 已经能满足基本的需求了,但是还可以更强大!

  1. 自定义 initialValue

有时候,我们希望在数据加载之前,先显示一个默认值。可以给 Hook 添加一个 initialValue 参数:

// useAsyncData.js (添加 initialValue)
import { ref, onMounted } from 'vue';

export function useAsyncData(url, initialValue = null) {
  const data = ref(initialValue); // 使用 initialValue 初始化 data
  const loading = ref(false);
  const error = ref(null);

  const fetchData = async () => {
    // ... 省略请求逻辑,和之前一样
  };

  onMounted(fetchData);

  return {
    data,
    loading,
    error,
    refetch: fetchData,
  };
}

使用方式:

<script>
import { useAsyncData } from './useAsyncData';

export default {
  setup() {
    const apiUrl = 'https://jsonplaceholder.typicode.com/posts/1';
    const { data, loading, error, refetch } = useAsyncData(apiUrl, { title: 'Loading...', body: '' }); // 提供 initialValue

    return {
      data,
      loading,
      error,
      refetch,
    };
  },
};
</script>

现在,在数据加载完成之前,会显示 title: 'Loading...', body: ''

  1. 延迟加载 (Lazy Loading)

有时候,我们不希望组件一挂载就立即加载数据,而是希望在特定条件下才加载。可以添加一个 immediate 参数:

// useAsyncData.js (添加 immediate)
import { ref, onMounted, watch } from 'vue';

export function useAsyncData(url, initialValue = null, immediate = true) {
  const data = ref(initialValue);
  const loading = ref(false);
  const error = ref(null);

  const fetchData = async () => {
    // ... 省略请求逻辑,和之前一样
  };

  if (immediate) {
    onMounted(fetchData);
  }

  return {
    data,
    loading,
    error,
    refetch: fetchData,
  };
}

使用方式:

<template>
  <div>
    <button @click="loadData" v-if="!data && !loading && !error">Load Data</button>
    <p v-if="loading">Loading...</p>
    <p v-if="error">Error: {{ error.message }}</p>
    <div v-if="data">
      <h1>{{ data.title }}</h1>
      <p>{{ data.body }}</p>
    </div>
  </div>
</template>

<script>
import { useAsyncData } from './useAsyncData';
import { ref } from 'vue';

export default {
  setup() {
    const apiUrl = 'https://jsonplaceholder.typicode.com/posts/1';
    const shouldLoad = ref(false);
    const { data, loading, error, refetch } = useAsyncData(apiUrl, null, false); // immediate 设置为 false

    const loadData = () => {
      refetch();
    };

    return {
      data,
      loading,
      error,
      refetch,
      loadData,
    };
  },
};
</script>

现在,组件挂载后不会立即加载数据,而是需要点击 "Load Data" 按钮才会加载。

  1. 响应式地改变 URL

有时候,我们需要根据用户的操作动态地改变 URL。可以用 watch 监听 URL 的变化,并在 URL 变化时重新加载数据:

// useAsyncData.js (响应式 URL)
import { ref, onMounted, watch } from 'vue';

export function useAsyncData(url, initialValue = null, immediate = true) {
  const data = ref(initialValue);
  const loading = ref(false);
  const error = ref(null);

  const fetchData = async () => {
    loading.value = true;
    error.value = null;

    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      data.value = await response.json();
    } catch (e) {
      error.value = e;
    } finally {
      loading.value = false;
    }
  };

  if (immediate) {
    onMounted(fetchData);
  }

  watch(
    () => url,
    (newUrl) => {
      if (newUrl) {
        fetchData();
      }
    }
  );

  return {
    data,
    loading,
    error,
    refetch: fetchData,
  };
}

使用方式:

<template>
  <div>
    <input type="text" v-model="userId" placeholder="Enter user ID">
    <p v-if="loading">Loading...</p>
    <p v-if="error">Error: {{ error.message }}</p>
    <div v-if="data">
      <h1>User ID: {{ userId }}</h1>
      <p>Name: {{ data.name }}</p>
      <p>Email: {{ data.email }}</p>
    </div>
  </div>
</template>

<script>
import { useAsyncData } from './useAsyncData';
import { ref } from 'vue';

export default {
  setup() {
    const userId = ref(1);
    const apiUrl = ref(`https://jsonplaceholder.typicode.com/users/${userId.value}`); // apiUrl 是 ref

    const { data, loading, error, refetch } = useAsyncData(apiUrl);

    return {
      data,
      loading,
      error,
      userId,
    };
  },
  watch: {
    userId(newUserId) {
      apiUrl.value = `https://jsonplaceholder.typicode.com/users/${newUserId}`; // 更新 apiUrl
    },
  },
};
</script>

在这个例子中,apiUrl 是一个 ref,它的值会随着 userId 的变化而变化。watch 监听 apiUrl 的变化,并在 apiUrl 变化时重新加载数据。

总结

今天我们一起打造了一个强大的异步数据加载 Hook,它可以处理 loading 和 error 状态,支持自定义初始值、延迟加载和响应式 URL。通过这个 Hook,我们可以把异步数据加载的逻辑提取出来,让组件更加简洁、易于维护。

当然,这只是一个基础的 Hook,你可以根据自己的需求进行扩展,比如添加缓存、请求取消等功能。

希望今天的分享对你有所帮助!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注