跳到主要内容

渲染异步数据

只需一行 useSuspense(),就能在使用数据的地方绑定数据,让你的组件可复用;它能 配合 await 一样保证数据的存在。

<script setup lang="ts">
  import { useSuspense } from '@data-client/vue';
  import PostItem from './PostItem.vue';
  import { PostResource } from './Resources';

  const emit = defineEmits<{ setRoute: [route: string] }>();
  const posts = await useSuspense(PostResource.getList);
</script>

<template>
  <div>
    <PostItem
      v-for="post in posts"
      :key="post.pk()"
      :post="post"
      @setRoute="emit('setRoute', $event)"
    />
  </div>
</template>
在许多上下文中使用的 Endpoint在许多上下文中使用的 Endpoint

不要进行 prop 逐层传递。而是在渲染数据的组件中直接调用 useSuspense()。这被称为 数据就近放置(data co-location)。

不要把数据绑定 hook 藏在自定义 hook 中。而应把紧密耦合的数据转换放在 Query 中——数据逻辑应当与数据模型放在一起,这样它保持可见、可复用,并且可以独立于视图自由修改。

Reactive Data Client 会在数据变化时立即自动更新绑定的组件,无需编写复杂的更新函数或级联失效逻辑。这被称为 响应式编程。

加载与错误​

你可能已经注意到,返回类型表明这个值总是存在的。useSuspense() 的工作方式非常配合 await。这让我们可以把错误和加载的处理与数据的使用分离开来。

异步边界​

我们会在**页面、路由或模态框**等导航边界处或其上层放置 Vue 内置的 <Suspense /> 以及 onErrorCaptured(),用来处理加载和错误状态。

Dashboard.vue
<script setup lang="ts">
import { onErrorCaptured, ref } from 'vue';

const error = ref<Error | null>(null);
onErrorCaptured(err => {
error.value = err;
return false;
});
</script>

<template>
<div>
<h1>Dashboard</h1>
<section>
<div v-if="error">Error: {{ error.message }}</div>
<Suspense v-else>
<template #default>
<RouterView />
</template>
<template #fallback>
<Loading />
</template>
</Suspense>
</section>
</div>
</template>

以这种方式集中管理 fallback,可以在保持组件可复用的同时消除冗余的加载指示器。加载 fallback 通过 <Suspense /> 的 #fallback 插槽定制,错误 fallback 则通过渲染你在 onErrorCaptured() 中选择的内容来定制。

有状态的方式​

在某些情况下,你可能会发现使用有状态的方式处理 fallback 仍然有用。对于这些情况,或者为了兼容某些组件库,我们提供了 useDLE() - [D]ata [L]oading [E]rror。

<script setup lang="ts">
  import { useDLE } from '@data-client/vue';
  import { ProfileResource } from './ProfileResource';

  const { data, loading, error } = useDLE(ProfileResource.getList);
</script>

<template>
  <div v-if="error">Error {{ error.status }}</div>
  <Loading v-else-if="loading || !data" />
  <div v-else>
    <div class="listItem" v-for="profile in data" :key="profile.pk()">
      <Avatar :src="profile.avatar" />
      <div>
        <h4>{{ profile.fullName }}</h4>
        <p>{{ profile.bio }}</p>
      </div>
    </div>
  </div>
</template>

由于 useDLE 不使用 useSuspense,你将无法轻松地集中编排加载和错误处理的 代码。

条件获取​

条件依赖

将 null 作为任意 Data Client hook 的第二个参数,表示“什么都不做”。

// todo could be undefined if id is undefined
const todo = await useSuspense(
TodoResource.get,
computed(() => (id.value ? { id: id.value } : null)),
);

订阅​

当数据可能因外部因素而发生变化时,useSubscription() 可以确保组件挂载期间持续更新。useLive() 会同时调用 useSubscription() 和 useSuspense(),让你能够非常轻松地使用最新数据。

<script setup lang="ts">
  import { useLive } from '@data-client/vue';
  import { getTicker } from './Ticker';
  import NumberFlow from '@number-flow/vue';

  const props = defineProps<{ productId: string }>();
  const ticker = await useLive(getTicker, () => ({ productId: props.productId }));
</script>

<template>
  <div style="text-align: center">
    {{ productId }}
    <NumberFlow
      :value="ticker.price"
      :format="{ style: 'currency', currency: 'USD' }"
    />
  </div>
</template>

订阅由 Managers 编排。开箱即用地,只需在 Endpoint 或 Resource 上添加 pollFrequency,就可以使用基于轮询的订阅。对于 SSE 和 websockets 这类基于推送的网络协议,请参阅数据流 manager 示例。

export const getTicker = new RestEndpoint({
urlPrefix: 'https://api.exchange.coinbase.com',
path: '/products/:productId/ticker',
schema: Ticker,
pollFrequency: 2000,
});