跳到主要内容

Endpoint 过期策略

默认情况下,Reactive Data Client 的缓存策略可以描述为 stale-while-revalidate。这意味着当数据可用时,它可以使用过时的数据,从而避免阻塞应用。不过在后台,如果数据足够旧,它仍然会刷新数据。

过期状态​

新鲜(Fresh)​

处于这种状态的数据被认为足够新,无需获取。

过时(Stale)​

数据仍然可以展示,但 Reactive Data Client 可能会尝试重新获取以重新验证。

useSuspense() 会在挂载时以及参数变化时考虑是否获取。在这些情况下,如果数据被视为过时,它就会发起获取。

无效(Invalid)​

数据不应展示。任何需要这些数据的组件都会触发获取(在获取完成之前,已挂载的组件会保留其数据)。如果没有组件关心这些数据,则不会执行任何操作。

过期时间​

Endpoint.dataExpiryLength​

Endpoint.dataExpiryLength 用于设置数据从“新鲜”状态转变为“过时”状态所需的时间(毫秒)。试着把它设置为很小的数字,比如 '50',让数据几乎立即变为过时;或者设置为很大的数字,让数据保留很长时间。

在 'first' 和 'second' 之间切换会改变参数。如果数据仍被视为新鲜,你会一直看到旧的时间,而不会发生任何刷新。

import { lastUpdated } from './api/lastUpdated';

export const getUpdated = lastUpdated.extend({ dataExpiryLength: 10000 });
<script setup lang="ts">
  import { useSuspense } from '@data-client/vue';
  import { getUpdated } from './getUpdated';

  const props = defineProps<{ id: string }>();
  const time = await useSuspense(getUpdated, () => ({ id: props.id }));
</script>

<template>
  <div>
    API time for {{ id }}:
    <time>{{ time.updatedAt.toLocaleString('en-US', { timeStyle: 'long' }) }}</time>
  </div>
</template>
@data-client/rest

较长的缓存寿命

LongLivingResource.ts
import {
RestEndpoint,
RestGenerics,
resource,
} from '@data-client/rest';

// We can now use LongLivingEndpoint to create endpoints that will be cached for one hour
class LongLivingEndpoint<
O extends RestGenerics,
> extends RestEndpoint<O> {
dataExpiryLength = 60 * 60 * 1000; // one hour
}

const LongLivingResource = resource({
path: '/:id',
Endpoint: LongLivingEndpoint,
});

出错时从不重试

NoRetryResource.ts
import {
RestEndpoint,
RestGenerics,
resource,
} from '@data-client/rest';

// We can now use NoRetryEndpoint to create endpoints that will be cached for one hour
class NoRetryEndpoint<
O extends RestGenerics,
> extends RestEndpoint<O> {
errorExpiryLength = Infinity;
}

const NoRetryResource = resource({
path: '/:id',
Endpoint: NoRetryEndpoint,
});

Endpoint.invalidIfStale​

Endpoint.invalidIfStale 去掉了“过时”状态,使得过期的数据立即被视为“无效”。

在演示中,组件的数据一旦过时,组件就会挂起。如果数据仍在过期时间之内,组件就会继续展示它。

import { lastUpdated } from './api/lastUpdated';

export const getUpdated = lastUpdated.extend({
  invalidIfStale: true,
  dataExpiryLength: 5000,
});
<script setup lang="ts">
  import { useSuspense } from '@data-client/vue';
  import { getUpdated } from './getUpdated';

  const props = defineProps<{ id: string }>();
  const time = await useSuspense(getUpdated, () => ({ id: props.id }));
</script>

<template>
  <div>
    API time for {{ id }}:
    <time>{{ time.updatedAt.toLocaleString('en-US', { timeStyle: 'long' }) }}</time>
  </div>
</template>

强制刷新​

有时我们想要获取新数据,同时继续展示旧的(过时的)数据。

某个特定的 endpoint​

Controller.fetch 可以用来触发获取,同时仍展示之前的数据。即使数据是“新鲜”的,也可以这样做。

<script setup lang="ts">
  import { useController, useSuspense } from '@data-client/vue';
  import { lastUpdated } from './api/lastUpdated';

  const time = await useSuspense(lastUpdated, { id: '1' });
  const ctrl = useController();
</script>

<template>
  <div>
    <time>{{ time.updatedAt.toLocaleString('en-US', { timeStyle: 'long' }) }}</time>
    <button @click="ctrl.fetch(lastUpdated, { id: '1' })">Refresh</button>
  </div>
</template>

刷新可见的 endpoint​

Controller.expireAll() 会把所有与 testKey 匹配的响应的过期状态设置为过时。

<script setup lang="ts">
  import { useController } from '@data-client/vue';
  import { lastUpdated } from './api/lastUpdated';
  import ShowTime from './ShowTime.vue';

  const ctrl = useController();
</script>

<template>
  <div>
    <Suspense v-for="id in ['1', '2', '3']" :key="id">
      <ShowTime :id="id" />
      <template #fallback><div>{{ id }} Loading...</div></template>
    </Suspense>

    <button @click="ctrl.expireAll(lastUpdated)">Expire All</button>
    <button @click="ctrl.fetch(lastUpdated, { id: '1' })">
      Force Refresh First
    </button>
  </div>
</template>

使失效​

endpoint 和 Entity 都可以作为失效的目标。

失效的数据总是会重新获取,即使它仍然新鲜。组件的 setup 一旦运行,Vue 就无法再次挂起该组件,因此已挂载的组件会继续展示之前的数据,直到重新获取完成。在此期间,useCache() 返回 undefined,useDLE() 的 loading 为 true。在失效之后挂载的组件会挂起,直到新数据到达。

某个特定的 endpoint​

在这个示例中,使 endpoint 失效会重新获取它,即使其数据仍然新鲜。

<script setup lang="ts">
  import { useController } from '@data-client/vue';
  import { lastUpdated } from './api/lastUpdated';
  import ShowTime from './ShowTime.vue';

  const ctrl = useController();
</script>

<template>
  <div>
    <Suspense v-for="id in ['1', '2', '3']" :key="id">
      <ShowTime :id="id" />
      <template #fallback><div>{{ id }} Loading...</div></template>
    </Suspense>

    <button @click="ctrl.invalidateAll(lastUpdated)">Invalidate All</button>
    <button @click="ctrl.invalidate(lastUpdated, { id: '1' })">
      Invalidate First
    </button>
  </div>
</template>

包含某个 Entity 的任意 endpoint​

使用 Invalidate schema,我们可以让响应中依赖该 Entity 的_任意_ endpoint 失效。如果 endpoint 在 Array 中使用了该 Entity,它只会被从该 Array 中移除。

<script setup lang="ts">
  import { Invalidate, RestEndpoint } from '@data-client/rest';
  import { useController, useLoading } from '@data-client/vue';
  import { TimedEntity } from './api/lastUpdated';
  import TimePage from './TimePage.vue';

  const InvalidateTimedEntity = new Invalidate(TimedEntity);
  const deleteLastUpdated = new RestEndpoint({
    path: '/api/currentTime/:id',
    method: 'DELETE',
    schema: InvalidateTimedEntity,
  });

  const ctrl = useController();
  const [handleDelete, loadingDelete] = useLoading(() =>
    ctrl.fetch(deleteLastUpdated, { id: '1' }),
  );
</script>

<template>
  <div>
    <Suspense>
      <TimePage id="1" />
      <template #fallback><div>loading...</div></template>
    </Suspense>
    <button @click="handleDelete">
      {{ loadingDelete ? 'loading...' : 'Invalidate' }}
    </button>
    <button
      @click="ctrl.setResponse(deleteLastUpdated, { id: '1' }, { id: '1' })"
    >
      Invalidate (without fetching DELETE)
    </button>
    <button @click="ctrl.set([InvalidateTimedEntity], [{ id: '1' }])">
      Invalidate Entity with ctrl.set
    </button>
  </div>
</template>

Controller.fetch() 让我们可以同时更新服务器和 store。当我们想直接修改本地 store 时,可以使用 Controller.setResponse() 或 Controller.set()。

根据数据有条件地失效​

如果 invalidation 只应在某些情况下根据响应数据发生,我们可以从 Entity.process 返回 undefined。

class PriceLevel extends Entity {
price = 0;
amount = 0;

pk() {
return this.price;
}

static process(
input: [number, number],
parent: any,
key: string | undefined,
): any {
const [price, amount] = input;
if (amount === 0) return undefined;
return { price, amount };
}
}