跳到主要内容

Endpoint

Endpoint 适用于任何异步函数(即返回 Promise 的函数)。

Endpoints 定义了一套强类型的标准接口,描述相关的元数据和生命周期,可供 Reactive Data Client 及其他 store 使用。

包:@data-client/endpoint

提示

Endpoint 是一个与协议无关的类。建议改用针对特定协议的模式: REST、GraphQL 或 getImage。

接口
export interface EndpointInterface<
F extends FetchFunction = FetchFunction,
S extends Schema | undefined = Schema | undefined,
M extends true | undefined = true | undefined,
> extends EndpointExtraOptions<F> {
(...args: Parameters<F>): InferReturn<F, S>;
key(...args: Parameters<F>): string;
readonly sideEffect?: M;
readonly schema?: S;
}

用法​

Endpoint 让现有的异步函数可以在任何 Reactive Data Client 上下文中使用,并享有完整的 TypeScript 类型约束。

▶interface
▶api
import { Endpoint } from '@data-client/rest';
import { Todo } from './interface';

const getTodoOriginal = (id: number): Promise<Todo> =>
  Promise.resolve({
    id,
    title: 'delectus aut autem ' + id,
    completed: false,
    userId: 1,
  });

export const getTodo = new Endpoint(getTodoOriginal);
▶React
import { useSuspense } from '@data-client/react';
import { getTodo } from './api';

function TodoDetail() {
  const todo = useSuspense(getTodo, 1);
  return <div>{todo.title}</div>;
}
render(<TodoDetail />);
结果
Store▶

共享配置​

请使用 Endpoint.extend(),而不是 {...getTodo}(展开)

const getTodoNormalized = getTodo.extend({ schema: Todo });
const getTodoUpdatingEveryFiveSeconds = getTodo.extend({ pollFrequency: 5000 });

生命周期​

成功​

错误​

Endpoint 成员​

成员同时也是选项(构造函数的第二个参数)。它们都不是必填的,其中前几个有默认值。

key: (params) => string​

序列化参数,用于在全局 store 中构建查找键。

默认值:

`${this.name} ${JSON.stringify(params)}`;
覆盖

覆盖 key 时,如果你打算使用 testKey 方法,务必同时提供一个相应更新的版本。

testKey(key): boolean​

如果提供的(fetch)key 与此 endpoint 匹配,则返回 true。

它用于配合 <MockResolver /> 使用的模拟 interceptor

name: string​

在 key 中用于区分不同的 endpoint。应当全局唯一。

默认为 this.fetch.name

注意

在会更改函数名的生产构建中,这可能会失效。这通常被称为函数名混淆(mangling)。

这种情况下,你可以覆盖 name,或禁用函数名混淆。

sideEffect: boolean​

用于表示该 endpoint 可能有副作用(非幂等)。这会禁止它与 useSuspense() 或 useFetch() 一起使用,因为它们请求该 endpoint 的次数不可预测。

schema: Schema​

以声明式的方式定义如何处理响应

不提供此选项意味着不会提取任何 Entity。

import { Endpoint, Entity } from '@data-client/endpoint';

class User extends Entity {
id = '';
username = '';
}

const getUser = new Endpoint(
({ id }) => fetch(`/users/${id}`),
{ schema: User }
);

dataExpiryLength?: number​

为所获取资源自定义的数据缓存有效期。会覆盖 NetworkManager 中设置的值。

进一步了解过期时间

errorExpiryLength?: number​

为所获取资源自定义的错误有效期。会覆盖 NetworkManager 中设置的值。

errorPolicy?: (error: any) => 'soft' | undefined​

'soft' 会在出错时使用过时数据(如果存在);返回 undefined 或不提供该选项时,结果将是错误。

进一步了解 errorPolicy

errorPolicy(error) {
return error.status >= 500 ? 'soft' : undefined;
}

invalidIfStale: boolean​

表示过时数据应被视为不可用,因此不会从缓存中返回。这意味着即使数据已经存在于缓存中,只要它过时了,useSuspense() 就会挂起。

pollFrequency: number​

轮询频率,单位为毫秒。需要配合 useSubscription() 或 useLive() 使用才会生效。

getOptimisticResponse: (snap, ...args) => expectedResponse​

提供该函数后,使用此 endpoint 的任何获取都会表现得如同该函数返回的 expectedResponse 是一次成功的网络响应。当实际请求完成时(无论失败还是成功),乐观更新都会被实际的网络响应替换。

import { resource } from '@data-client/rest';
import { Post } from './Post';

export { Post };

export const PostResource = resource({
  path: '/posts/:id',
  searchParams: {} as { userId?: string | number } | undefined,
  schema: Post,
}).extend('vote', {
  path: '/posts/:id/vote',
  method: 'POST',
  body: undefined,
  schema: Post,
  getOptimisticResponse(snapshot, { id }) {
    const post = snapshot.get(Post, { id });
    if (!post) throw snapshot.abort;
    return {
      id,
      votes: post.votes + 1,
    };
  },
});
结果
Store▶
乐观更新指南

update()​

(normalizedResponseOfThis, ...args) =>
({ [endpointKey]: (normalizedResponseOfEndpointToUpdate) => updatedNormalizedResponse) })
提示

试试改用 Collections。

它们更易用,也更健壮!

UpdateType.ts
type UpdateFunction<
Source extends EndpointInterface,
Updaters extends Record<string, any> = Record<string, any>,
> = (
source: ResultEntry<Source>,
...args: Parameters<Source>
) => { [K in keyof Updaters]: (result: Updaters[K]) => Updaters[K] };

最简单的情况:

userEndpoint.ts
const createUser = new RestEndpoint({
path: '/user',
method: 'POST',
schema: User,
update: (newUserId: string) => ({
[userList.key()]: (users = []) => [newUserId, ...users],
}),
});

更多更新:

Component.tsx
const allusers = useSuspense(userList);
const adminUsers = useSuspense(userList, { admin: true });

下面的 endpoint 确保新用户会立即出现在上述用法中。

userEndpoint.ts
const createUser = new RestEndpoint({
path: '/user',
method: 'POST',
schema: User,
update: (newUserId, newUser) => {
const updates = {
[userList.key()]: (users = []) => [newUserId, ...users],
];
if (newUser.isAdmin) {
updates[userList.key({ admin: true })] = (users = []) => [newUserId, ...users];
}
return updates;
},
});

extend(options): Endpoint​

可用于进一步自定义 endpoint 的定义

const getUser = new Endpoint(({ id }) => fetch(`/users/${id}`));


const getUserNormalized = getUser.extend({ schema: User });

除了这些成员之外,还可以传入 fetch 来覆盖获取函数。

示例​

import { Endpoint } from '@data-client/endpoint';

const UserDetail = new Endpoint(
({ id }) => fetch(`/users/${id}`).then(res => res.json())
);
import { useSuspense, useController } from '@data-client/react';
import { UserDetail } from './api/User';
import UserForm from './UserForm';

function UserProfile({ id }: { id: string }) {
const user = useSuspense(UserDetail, { id });
const ctrl = useController();

return <UserForm user={user} onSubmit={() => ctrl.fetch(UserDetail)} />;
}

更多内容​

动机​

以下两者是有区别的:

  • 网络 API 是什么
    • 如何发起请求、预期的响应字段等
  • 它如何被使用
    • 绑定数据、轮询、触发命令式获取等

因此,在这两个概念之间清晰地分离关注点会带来很多好处。

借助 TypeScript Standard Endpoints,我们定义了一套在 TypeScript 中声明网络 API 定义的标准。

  • 让 API 作者可以发布包含其 API 接口的 npm 包
  • 任何支持该标准的库都可以使用这些定义,便于在 Vue、React、Angular 等库之间通用
  • 由于输出非常精简,编写代码生成流程会容易得多
  • 产品开发者可以在行为各异的多种场景中使用这些定义
  • 产品开发者可以轻松地在行为需求不同的平台(如 React Native 和 React Web)之间共享代码

Endpoint 包含什么​

  • 一个用于解析结果的函数
  • 一个用于唯一地存储这些结果的函数
  • 可选:关于如何将数据存储到规范化缓存中的信息
  • 可选:该请求是否可能有副作用——以防止重复调用