Skip to content

在 uni-app 里使用 @tanstack/vue-query

tanstack-query-uni-app

本文仅针对 Vue v3 抛砖引玉,不考虑 v2,感兴趣可自行探索。

背景

uni-app 是一个基于 Vue.js 开发多平台前端应用的框架,支持 iOS、Android、Web、各小程序、快应用等多个平台,多见于小程序开发。

要在 uni-app 中发起请求,可以使用 uni-app 自带提供的 API,如 uni.request、uni.downloadFile、uni.uploadFile 等,对应 Web 端的 XHR、fetch 等 API。

但是这一类 API 相对较为底层,且对 Promise 不太友好,使用起来不太方便,因此不少开发者在实际开发中使用 luch-requestuni-ajax@uni-helper/uni-network 等库,它们基于自带提供的 API 实现了封装,对 Promise 支持更好,支持拦截器等更多功能,使用起来更加方便,对应 Web 端的 axiosky 等库。

使用这些库意味着将组件状态和请求副作用拼凑在一起,或者使用更通用的状态管理库在整个应用程序中存储和提供异步数据。

对于前者,也就是将组件状态和请求副作用拼凑在一起,通常涉及在单个组件中管理数据获取逻辑,以下是一个使用 @uni-helper/uni-network 的例子。在这个例子中,我们在组件内部管理 usersloading 状态,使用 onMounted 生命周期钩子来触发数据获取。这种方法虽然简单,但可能导致代码重复,同时难以在组件之间共享数据。

如果你没有使用过 @uni-helper/uni-network,请将它理解为支持 uni-app 的类 axios 库,并着重关心这个例子中的代码设计与实现。

vue
<script setup>
import { ref, onMounted } from 'vue';
import un from '@uni-helper/uni-network';

const users = ref([]);
const loading = ref(true);

const fetchUsers = async () => {
  try {
    users.value = await un.get('https://jsonplaceholder.typicode.com/users');
  } catch (error) {
    console.error('Failed to fetch users:', error);
  } finally {
    loading.value = false;
  }
};

onMounted(fetchUsers);
</script>

<template>
  <div>
    <h1>用户列表</h1>
    <ul v-if="!loading">
      <li v-for="user in users" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
    <p v-else>加载中...</p>
  </div>
</template>

对于后者,也就是使用状态管理库在整个应用程序中存储和提供异步数据,这种方法将数据获取逻辑抽象到一个中央存储中,使得数据可以在多个组件之间共享,更容易管理应用的整体状态,以下是一个使用 Pinia + @uni-helper/uni-network 的例子。

javascript
// stores/users.js
import { defineStore } from 'pinia';
import un from '@uni-helper/uni-network';

export const useUsersStore = defineStore('users', () => {
  const users = ref([]);
  const loading = ref(true);

  const fetchUsers = async () => {
    try {
      users.value = await un.get('https://jsonplaceholder.typicode.com/users');
    } catch (error) {
      console.error('Failed to fetch users:', error);
    } finally {
      loading.value = false;
    }
  };

  return { users, loading, fetchUsers };
});
vue
<script setup>
import { onMounted } from 'vue';
import { useUsersStore } from '@/stores/users';
const usersStore = useUsersStore();

onMounted(() => {
  usersStore.fetchUsers();
});
</script>

<template>
  <div>
    <h1>用户列表</h1>
    <ul v-if="!usersStore.loading">
      <li v-for="user in usersStore.users" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
    <p v-else>加载中...</p>
  </div>
</template>

这两种方法在小型项目中运行得很好,但随着项目规模扩大、参与人数增多,问题和挑战也随之而来。

为什么需要 @tanstack/vue-query

首先我们需要认识到,异步状态,或者说服务器状态,本质上跟组件状态不一样。

  1. 组件状态保存在你可以直接控制的本地组件,而服务器状态保存在你无法直接控制的远端;
  2. 你可以手动直接修改组件状态,而服务器状态要求你使用异步 API 来获取或更新
  3. 你可以确定组件状态的修改原因,而服务器状态可能在你不知情的情况下被其他人修改,进而导致服务器状态过时

上面提及的两种方法,实际上是把服务器状态缓存下来,将它转化成组件状态,你仍然需要手动处理以上提及的服务器状态所会遇到的问题,你可能需要调用 API 更新后重新请求数据、在多个生命周期重新请求数据、定时请求数据等,以确保这些数据是最新的。

但挑战还不止于此:

  1. 我们该如何管理服务器状态的缓存?键值对怎么确定?过时时间多少合适?手动过时怎么做?
  2. 我们如何合并重复请求?怎么确定是不是重复请求?
  3. 我们如何重试失败请求?不同的失败请求可以有不同的处理吗?重试间隔和次数怎么确定?
  4. 我们如何在后台更新数据?更新后数据能及时反映到页面上吗?
  5. 我们怎么确定数据已经过时?过时后可以自动更新数据吗?

Two Hard Things

如果你已经完美解决了以上所有问题,或者你正在尝试解决以上所有问题,你就会知道这不是一件容易的事情,你可能需要几百行甚至几千行代码来处理这些问题,而这些问题还只是冰山一角,这就是为什么我们需要 @tanstack/vue-query

@tanstack/vue-query 零配置开箱即用,可根据需要定制,可以说它是一个服务器状态管理库,也可以说是一个请求策略库,它可以做到:

  1. 用几行更统一、更声明式的 Vue 代码替换掉大量复杂代码;
  2. 更易于维护、更容易构建新功能,无需担心连接新的服务器状态数据源,将组件状态和服务器状态明确区分开;
  3. 让应用程序感觉比以往响应更快,直接提高用户体验;
  4. 可能节省带宽并提高内存性能。

相对应地,由于它功能非常强大,不可避免地会带来一些问题,比如:

  1. 概念相对较多,需要一点时间来学习;
  2. 体积问题,npm 显示 @tanstack/vue-query 本身有 792kB,而它依赖的 @tanstack/query-core 有 2.02MB,即使使用了摇树和压缩,至少也会占用 10+kB,对于小程序、快应用等平台开发来说值得商榷取舍一番,如果简单处理已经可以满足需求,不建议引入;
  3. @tanstack/vue-query 本身只考虑 Web 和 SSR 环境,并没有考虑兼容小程序、快应用等平台,所以直接在 uni-app 里使用它开发会有部分功能无法正常工作。

@tanstack/vue-query Size@tanstack/query-core Size

本文主要针对第 3 点来探讨解决方案,也就是要在 uni-app 里正常使用 @tanstack/vue-query。

适配 uni-app

我们首先要确定哪些部分不兼容非 Web 平台。如果要手动确认会非常麻烦,我们可以先问一下 AI 确定方向,这里我使用的是 Claude。

claude-tanstack-vue-query-unsupported

然后我们再结合文档和源码来确认 AI 给出的方向。

简单通读文档,我们可以确定没有使用 Fetch API,因为文档中并没有要求一定使用 Fetch,可以使用 axios、@uni-helper/uni-network、uni.request 或其它来实现底层的请求。

而通过搜索源码,我们可以确定没有在核心部分使用 Navigator,我们不需要操心这部分。

@tanstack/vue-query Navigator

采用类似的方法搜索,我们还可以确定在核心部分中的焦点管理器 FocusManager 和在线管理器 OnlineManager 使用了 window,在其它核心部分使用了 AbortController。

@tanstack/vue-query window 1

@tanstack/vue-query window 2

@tanstack/vue-query AbortController

对于焦点管理器 FocusManager,它为聚焦后重新请求数据 Window Focus Refetching 提供支持;而对于在线管理器 OnlineManager,它为根据网络模式 Network Mode 决定请求行为提供支持。你不需要完全理解这两个地方的实际含义,只需要明白这两个地方都依赖特定的 API,所以要在 uni-app 里使用 @tanstack/vue-query 需要针对这两者做适配。

对于 AbortController,由于部分平台可能没有原生支持,我们需要手动添加 Polyfill。

焦点管理器 FocusManager

原本 @tanstack/vue-query 通过监听 visibilitychange 事件来监听焦点的变化,但是在 uni-app 里这个事件可能不存在,我们可以修改为使用 uni-app 应用生命周期处理。

我们可以使用条件编译,在 Web 外调整处理,Web 仍然保留原有逻辑。

vue
<script setup>
// App.vue
// 在 Web 外调整处理
// #ifndef WEB
import { focusManager } from '@tanstack/vue-query';
import { onShow, onHide } from '@dcloudio/uni-app';

// onShow 时记为聚焦
onShow(() => {
  focusManager.setFocused(true);
});
// onHide 时记为失焦
onHide(() => {
  focusManager.setFocused(false);
});
// #endif
</script>

也可以统一调整处理。

vue
<script setup>
// App.vue
// 应用生命周期处理
import { focusManager } from '@tanstack/vue-query';
import { onShow, onHide } from '@dcloudio/uni-app';

// onShow 时记为聚焦
onShow(() => {
  focusManager.setFocused(true);
});
// onHide 时记为失焦
onHide(() => {
  focusManager.setFocused(false);
});
</script>

在线管理器 OnlineManager

原本 @tanstack/vue-query 通过监听 onlineoffline 事件来监听网络变化,但是在 uni-app 里这两个事件可能不存在,我们可以修改为使用 uni-app API 处理。

我们可以使用条件编译,在 Web 外调整处理,Web 仍然保留原有逻辑。

vue
<script setup>
// App.vue
// 在 Web 外调整处理
// #ifndef WEB
import { onlineManager } from '@tanstack/vue-query';

uni.getNetworkType({
  success: ({ networkType }) => {
    onlineManager.setOnline(networkType !== 'none');
  },
});
uni.onNetworkStatusChange(({ isConnected, networkType }) => {
  // 优先使用 isConnected 判断网络状态
  // 回退到 networkType 判断
  onlineManager.setOnline(
    isConnected != null ? isConnected : networkType !== 'none',
  );
});
// #endif
</script>

也可以统一调整处理。

vue
<script setup>
// App.vue
import { onlineManager } from '@tanstack/vue-query';

uni.getNetworkType({
  success: ({ networkType }) => {
    onlineManager.setOnline(networkType !== 'none');
  },
});
uni.onNetworkStatusChange(({ isConnected, networkType }) => {
  // 优先使用 isConnected 判断网络状态
  // 回退到 networkType 判断
  onlineManager.setOnline(
    isConnected != null ? isConnected : networkType !== 'none',
  );
});
</script>

AbortController

@tanstack/vue-query 内部使用了 AbortController,但是在部分平台中可能不受支持,我们需要手动补充 Polyfill。安装 abortcontroller-polyfill@^1.7.5 后,调整 App.vue 内容,尽可能早地导入,后续可全局使用。

vue
<script setup>
// App.vue
// 尽可能早地导入,后续可全局使用
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
</script>

部分环境下,该 Polyfill 可能无法正常使用,如 支付宝小程序。此时可以考虑使用 abort-controller@^3.0.0,导入后局部使用。对于 @tanstack/vue-query,可以尝试使用 patch-package 或者 pnpm patch,这里不再过多展开,请自行探索。

至此,@tanstack/vue-query 应该已经可以在 uni-app 里正常使用了。

小结

本文从在 uni-app 中使用请求出发,引出 @tanstack/vue-query,讨论了在 uni-app 中使用 @tanstack/vue-query 的优劣势和兼容性适配方案。

希望本文能带给你一点启发和帮助!

欢迎关注我的 个人站、我的掘金号 _狗鸽_、我的公众号 程序员想退休 和我的知乎号 狗鸽,获取更多分享。

Released under the MIT License.