Endpoint
Endpoint 适用于任何异步函数(即返回 Promise 的函数)。
Endpoints 定义了一套强类型的标准接口,描述相关的元数据和生命周期,可供 Reactive Data Client 及其他 store 使用。
接口
- Interface
- Class
- EndpointExtraOptions
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;
}
class Endpoint<F extends (...args: any) => Promise<any>>
implements EndpointInterface
{
constructor(fetchFunction: F, options: EndpointOptions);
key(...args: Parameters<F>): string;
readonly sideEffect?: true;
readonly schema?: Schema;
fetch: F;
extend(options: EndpointOptions): Endpoint;
}
export interface EndpointOptions extends EndpointExtraOptions {
key?: (params: any) => string;
sideEffect?: true | undefined;
schema?: Schema;
}
export interface EndpointExtraOptions<F extends FetchFunction = FetchFunction> {
/** Default data expiry length, will fall back to NetworkManager default if not defined */
readonly dataExpiryLength?: number;
/** Default error expiry length, will fall back to NetworkManager default if not defined */
readonly errorExpiryLength?: number;
/** Poll with at least this frequency in milliseconds */
readonly pollFrequency?: number;
/** Marks cached resources as invalid if they are stale */
readonly invalidIfStale?: boolean;
/** Enables optimistic updates for this request - uses return value as assumed network response */
readonly getOptimisticResponse?: (
snap: SnapshotInterface,
...args: Parameters<F>
) => ResolveType<F>;
/** Determines whether to throw or fallback to */
readonly errorPolicy?: (error: any) => 'soft' | undefined;
/** User-land extra data to send */
readonly extra?: any;
}
用法
Endpoint 让现有的异步函数可以在任何 Reactive Data Client 上下文中使用,并享有完整的 TypeScript 类型约束。
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);
import { useSuspense } from '@data-client/react'; import { getTodo } from './api'; function TodoDetail() { const todo = useSuspense(getTodo, 1); return <div>{todo.title}</div>; } render(<TodoDetail />);
共享配置
请使用 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(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, }; }, });
update()
(normalizedResponseOfThis, ...args) =>
({ [endpointKey]: (normalizedResponseOfEndpointToUpdate) => updatedNormalizedResponse) })
试试改用 Collections。
它们更易用,也更健壮!
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] };
最简单的情况:
const createUser = new RestEndpoint({
path: '/user',
method: 'POST',
schema: User,
update: (newUserId: string) => ({
[userList.key()]: (users = []) => [newUserId, ...users],
}),
});
更多更新:
const allusers = useSuspense(userList);
const adminUsers = useSuspense(userList, { admin: true });
下面的 endpoint 确保新用户会立即出现在上述用法中。
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 来覆盖获取函数。
示例
- Basic
- With Schema
- List
import { Endpoint } from '@data-client/endpoint';
const UserDetail = new Endpoint(
({ id }) => fetch(`/users/${id}`).then(res => res.json())
);
import { Endpoint, Entity } from '@data-client/endpoint';
class User extends Entity {
id = '';
username = '';
}
const UserDetail = new Endpoint(
({ id }) => fetch(`/users/${id}`).then(res => res.json()),
{ schema: User }
);
import { Endpoint, Entity } from '@data-client/endpoint';
class User extends Entity {
id = '';
username = '';
}
const UserList = new Endpoint(
() => fetch(`/users/`).then(res => res.json()),
{ schema: [User] }
);
- React
- JS/Node Schema
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)} />;
}
const user = await UserDetail({ id: '5' });
console.log(user);
更多内容
动机
以下两者是有区别的:
- 网络 API 是什么
- 如何发起请求、预期的响应字段等
- 它如何被使用
- 绑定数据、轮询、触发命令式获取等
因此,在这两个概念之间清晰地分离关注点会带来很多好处。
借助 TypeScript Standard Endpoints,我们定义了一套在
TypeScript 中声明网络 API 定义的标准。
- 让 API 作者可以发布包含其 API 接口的 npm 包
- 任何支持该标准的库都可以使用这些定义,便于在 Vue、React、Angular 等库之间通用
- 由于输出非常精简,编写代码生成流程会容易得多
- 产品开发者可以在行为各异的多种场景中使用这些定义
- 产品开发者可以轻松地在行为需求不同的平台(如 React Native 和 React Web)之间共享代码
Endpoint 包含什么
- 一个用于解析结果的函数
- 一个用于唯一地存储这些结果的函数
- 可选:关于如何将数据存储到规范化缓存中的信息
- 可选:该请求是否可能有副作用——以防止重复调用