跳到主要内容

Resource

Resources 是一组 RestEndpoints 的集合,它们通过共享同一个 schema 来操作共同的数据

用法​

resources/Todo.ts
export class Todo extends Entity {
id = '';
title = '';
completed = false;

static key = 'Todo';
}

const TodoResource = resource({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
schema: Todo,
});
Resources start with 6 Endpoints
const todo = useSuspense(TodoResource.get, { id: '5' });
const todos = useSuspense(TodoResource.getList);
controller.fetch(TodoResource.getList.push, {
title: 'finish installing reactive data client',
});
controller.fetch(
TodoResource.update,
{ id: '5' },
{ ...todo, completed: true },
);
controller.fetch(
TodoResource.partialUpdate,
{ id: '5' },
{ completed: true },
);
controller.fetch(TodoResource.delete, { id: '5' });

参数​

{
path: string;
schema: Schema;
urlPrefix?: string;
body?: any;
searchParams?: any;
paginationField?: string;
optimistic?: boolean;
Endpoint?: typeof RestEndpoint;
Collection?: typeof Collection;
} & EndpointExtraOptions

path​

传给单条目 endpoint 的 RestEndpoint.path。使用 path-to-regexp v8 语法——关于可选参数、通配符、带引号的名称和转义的完整说明,请参阅 RestEndpoint.path。

创建类(getList.push/getList.unshift)和 getList 会移除最后一个 :param 或 *wildcard token。

const PostResource = resource({
schema: Post,
path: '/:group/posts/:id',
});

// GET /react/posts/abc
PostResource.get({ group: 'react', id: 'abc' });
// PATCH /react/posts/abc
PostResource.partialUpdate({ group: 'react', id: 'abc' }, { title: 'This new title' });
// GET /react/posts
PostResource.getList({ group: 'react' });

可选参数使用 {} 语法:

const PostResource = resource({
schema: Post,
path: '/:group/posts{/:id}',
});

PostResource.get({ group: 'react', id: 'abc' });
PostResource.getList({ group: 'react' });

也支持将通配符参数作为最后一个 token:

const FileResource = resource({
schema: File,
path: '/repos/:owner/*path',
});

// GET /repos/john/src/index.ts
FileResource.get({ owner: 'john', path: ['src', 'index.ts'] });
// GET /repos/john
FileResource.getList({ owner: 'john' });

schema​

传给 RestEndpoint.schema,表示单个条目。它通常是一个 Entity 或 Union。

urlPrefix​

传给 RestEndpoint.urlPrefix

searchParams​

传给 getList 和 getList.push 的 RestEndpoint.searchParams

body​

传给 getList.push、update 和 partialUpdate 的 RestEndpoint.body

paginationField​

如果指定,会在 Resource 上添加 Resource.getList.getPage 方法。

nonFilterArgumentKeys​

透传给 getList schema 的 Collection.nonFilterArgumentKeys 选项。

const PostResource = resource({
path: '/:group/posts/:id',
searchParams: {} as { orderBy?: string; author?: string },
schema: Post,
nonFilterArgumentKeys: ['orderBy'],
});

也支持 RegExp 和函数形式:

resource({
path: '/:group/posts/:id',
searchParams: {} as { orderBy?: string; author?: string },
schema: Post,
nonFilterArgumentKeys: /orderBy/,
});

optimistic​

设为 true 会让所有变更 endpoint 都变为乐观的,使 UI 立即更新,甚至无需等待请求完成。

Endpoint​

用于构造各个成员的类。

import { RestEndpoint } from '@data-client/rest';

export default class AuthdEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
async getRequestInit(body: any): Promise<RequestInit> {
return {
...(await super.getRequestInit(body)),
credentials: 'same-origin',
};
}
}
const TodoResource = resource({
path: '/todos/:id',
schema: Todo,
Endpoint: AuthdEndpoint,
});

Collection​

用于构造 getList schema 的 Collection 类。当你需要在 nonFilterArgumentKeys 之外进一步自定义 collection 的行为时(例如更改 move 的合并逻辑),请使用它。

import { resource, Collection, unshift } from '@data-client/rest';

class MyCollection<
S extends any[] | PolymorphicInterface = any,
Parent extends any[] = [urlParams: any, body?: any],
> extends Collection<S, Parent> {
constructor(schema: S) {
super(schema);
// prepend moved items instead of appending
this.move = this.moveWith(unshift);
}
}
const TodoResource = resource({
path: '/todos/:id',
searchParams: {} as { userId?: string; orderBy?: string } | undefined,
schema: Todo,
Collection: MyCollection,
});

EndpointExtraOptions​

包括 dataExpiryLength、errorExpiryLength、errorPolicy、invalidIfStale 和 pollFrequency

成员​

这些成员提供了 REST API 中常见的标准 CRUD endpoint。你可以根据自己的 API 随意自定义或添加新的 endpoint。

const PostResource = resource({
schema: Post,
path: '/:group/posts/:id',
searchParams: {} as { author?: string },
paginationField: 'page',
});
名称方法参数Schema
getGET[{group: string; id: string}]Post
getListGET[{group: string; author?: string}]Collection([Post])
getList.pushPOST[{group: string; author?: string}, Partial<Post>]Collection([Post]).push
getList.unshiftPOST[{group: string; author?: string}, Partial<Post>]Collection([Post]).unshift
getList.getPageGET[{group: string; author?: string; page: string}]Collection([Post]).addWith
getList.movePATCH[{group: string; id: string }, Partial<Post>]Collection([Post]).move
updatePUT[{group: string; id: string }, Partial<Post>]Post
partialUpdatePATCH[{group: string; id: string }, Partial<Post>]Post
deleteDELETE[{group: string; id: string }]Invalidate(Post)

get​

获取单个 Entity。

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

export const PostResource = resource({
  schema: Post,
  path: '/:group/posts/:id',
  searchParams: {} as { author?: string },
});
▶Usage
import { PostResource } from './Resource';
PostResource.get({
  group: 'react',
  id: '1',
});
请求
GET /react/posts/1
content-type: application/json
响应200 OK
{
"id": "1",
"group": "react",
"title": "this post",
"author": "clara"
}
字段值
method'GET'
pathpath
schemaschema

通常与 useSuspense()、Controller.invalidate、Controller.expireAll 一起使用

getList​

获取 Entity 列表。

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

export const PostResource = resource({
  schema: Post,
  path: '/:group/posts/:id',
  searchParams: {} as { author?: string },
});
▶Usage
import { PostResource } from './Resource';
PostResource.getList({
  group: 'react',
  author: 'clara',
});
请求
GET /react/posts?author=clara
content-type: application/json
响应200 OK
[
{
"id": "1",
"group": "react",
"title": "this post",
"author": "clara"
}
]
字段值
method'GET'
pathremoveLastArg(path)
searchParamssearchParams
paginationFieldpaginationField
schemanew Collection([schema])
resource({ path: '/:first/:second' }).getList.path === '/:first';
resource({ path: '/:first' }).getList.path === '/';
resource({ path: '/:owner/*path' }).getList.path === '/:owner';

通常与 useSuspense()、Controller.invalidate、Controller.expireAll 一起使用

getList.push​

RestEndpoint.push 会创建一个新的 Entity,并将其添加到 getList 的末尾。如果想放在开头,请改用 getList.unshift。将数组作为 body 传入即可一次创建多个。

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

export const PostResource = resource({
  schema: Post,
  path: '/:group/posts/:id',
  searchParams: {} as { author?: string },
});
▶Usage
import { PostResource } from './Resource';
PostResource.getList.push(
  { group: 'react', author: 'clara' },
  { title: 'winning' },
);
请求
POST /react/posts?author=clara
content-type: application/json
Body: {"title":"winning"}
响应201 Created
{
"id": "2",
"group": "react",
"title": "winning",
"author": "clara"
}
字段值
method'POST'
pathremoveLastArg(path)
searchParamssearchParams
bodybody
schemagetList.schema.push

通常与 Controller.fetch 一起使用

getList.unshift​

RestEndpoint.unshift 会创建一个新的 Entity,并将其添加到 getList 的开头。

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

export const PostResource = resource({
  schema: Post,
  path: '/:group/posts/:id',
  searchParams: {} as { author?: string },
});
▶Usage
import { PostResource } from './Resource';
PostResource.getList.unshift(
  { group: 'react', author: 'clara' },
  { title: 'winning' },
);
请求
POST /react/posts?author=clara
content-type: application/json
Body: {"title":"winning"}
响应201 Created
{
"id": "2",
"group": "react",
"title": "winning",
"author": "clara"
}
字段值
method'POST'
pathremoveLastArg(path)
searchParamssearchParams
bodybody
schemagetList.schema.unshift

通常与 Controller.fetch 一起使用

getList.getPage​

RestEndpoint.getPage 会获取另一页数据并追加到 getList 中,同时确保没有重复项。

只有指定了 paginationField 时才会提供此成员。

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

export const PostResource = resource({
  schema: Post,
  path: '/:group/posts/:id',
  searchParams: {} as { author?: string },
  paginationField: 'page',
});
▶Usage
import { PostResource } from './Resource';
PostResource.getList.getPage({
  group: 'react',
  author: 'clara',
  page: 2,
});
请求
GET /react/posts?author=clara&page=2
content-type: application/json
响应200 OK
[
{
"id": "5",
"group": "react",
"title": "second page",
"author": "clara"
}
]
字段值
method'GET'
pathremoveLastArg(path)
searchParamssearchParams
paginationFieldpaginationField
schemagetList.schema.addWith

args: PathToArgs(shortenPath(path)) & searchParams & \{ [paginationField]: string | number \}

通常与 Controller.fetch 一起使用

getList.move​

RestEndpoint.move 会在 Collections 之间移动 Entity:将其从与旧状态匹配的 collection 中移除,并添加到与 body 中新值相匹配的 collection 中。

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

export const PostResource = resource({
  schema: Post,
  path: '/:group/posts/:id',
  searchParams: {} as { author?: string },
});
▶Usage
import { PostResource } from './Resource';
PostResource.getList.move(
  { group: 'react', id: '1' },
  { group: 'vue' },
);
请求
PATCH /react/posts/1
content-type: application/json
Body: {"group":"vue"}
响应200 OK
{
"id": "1",
"group": "vue",
"title": "this post",
"author": "clara"
}
字段值
method'PATCH'
pathpath
bodybody
schemagetList.schema.move

通常与 Controller.fetch 一起使用

update​

更新一个 Entity。

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

export const PostResource = resource({
  schema: Post,
  path: '/:group/posts/:id',
  searchParams: {} as { author?: string },
});
▶Usage
import { PostResource } from './Resource';
PostResource.update(
  { group: 'react', id: '1' },
  { title: 'updated title', author: 'clara' },
);
请求
PUT /react/posts/1
content-type: application/json
Body: {"title":"updated title","author":"clara"}
响应200 OK
{
"id": "1",
"group": "react",
"title": "updated title",
"author": "clara"
}
字段值
method'PUT'
pathpath
bodybody
schemaschema

通常与 Controller.fetch 一起使用

partialUpdate​

更新 Entity 的部分字段。

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

export const PostResource = resource({
  schema: Post,
  path: '/:group/posts/:id',
  searchParams: {} as { author?: string },
});
▶Usage
import { PostResource } from './Resource';
PostResource.partialUpdate(
  { group: 'react', id: '1' },
  { title: 'updated title' },
);
请求
PATCH /react/posts/1
content-type: application/json
Body: {"title":"updated title"}
响应200 OK
{
"id": "1",
"group": "react",
"title": "updated title",
"author": "clara"
}
字段值
method'PATCH'
pathpath
bodybody
schemaschema

通常与 Controller.fetch 一起使用

delete​

删除一个 Entity。

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

export const PostResource = resource({
  schema: Post,
  path: '/:group/posts/:id',
  searchParams: {} as { author?: string },
});
▶Usage
import { PostResource } from './Resource';
PostResource.delete({ group: 'react', id: '1' });
请求
DELETE /react/posts/1
content-type: application/json
响应200 OK
{
"id": "1"
}
字段值
method'DELETE'
pathpath
schemanew Invalidate(schema)
process
(value, params) {
return value && Object.keys(value).length ? value : params;
},

通常与 Controller.fetch 一起使用

响应​

{ "id": "xyz" }

响应应当是字符串形式的 pk(例如 'xyz'),或者是一个包含计算 Entity.pk 所需成员的对象(例如 {id: 'xyz'})。

如果没有提供响应,process 的实现会尝试使用作为对象发送的 url 参数来计算 Entity.pk。这样,只要使用的是标准参数,即使没有响应,默认实现也依然可以正常工作。

这使得 Invalidate 能够将该 Entity 从 Entity 表中移除

extend()​

resource 提供了一个很好的起点,但 endpoint 往往还需要进一步自定义。

extend() 是多态的,有三种形式:

函数形式(用于获取 BaseResource/super)​

这种形式最灵活,但也最冗长。

export const IssueResource= resource({
path: '/repos/:owner/:repo/issues/:number',
schema: Issue,
pollFrequency: 60000,
searchParams: {} as IssueFilters | undefined,
}).extend(BaseResource => ({
search: BaseResource.getList.extend({
path: '/search/issues?{q=:q}%20repo\\::owner/:repo{&page=:page}',
schema: {
results: {
incompleteResults: false,
items: BaseIssueResource.getList.schema.results,
totalCount: 0,
},
link: '',
},
})
)});

批量扩展已有成员​

这种形式只适用于已有的成员。

export const CommentResource = resource({
path: '/repos/:owner/:repo/issues/comments/:id',
schema: Comment,
}).extend({
getList: { path: '/repos/:owner/:repo/issues/:number/comments' },
update: { body: { body: '' } },
});

添加新成员​

这种形式一次只能添加一个 endpoint。

export const UserResource = createGithubResource({
path: '/users/:login',
schema: User,
}).extend('current', {
path: '/user',
schema: User,
});

Github CommentResource​

探索 github-app 示例

更多演示

函数继承模式​

要复用与 Resource 定义相关的代码,你可以创建自己的函数来调用 resource()。这与基于类的继承效果类似,而且还有一个额外的好处:允许完全覆盖类型。

import {
resource,
RestEndpoint,
Collection,
type EndpointExtraOptions,
type RestGenerics,
type ResourceGenerics,
type ResourceOptions,
} from '@data-client/rest';

export class AuthdEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
urlPrefix = process.env.API_SERVER ?? 'http://localhost:8000';

async getRequestInit(body: any): Promise<RequestInit> {
return {
...(await super.getRequestInit(body)),
credentials: 'same-origin',
};
}
}

export function myResource<O extends ResourceGenerics = any>({
schema,
Endpoint = AuthdEndpoint,
...extraOptions
}: Readonly<O> & ResourceOptions) {
return resource({
Endpoint,
schema,
...extraOptions,
}).extend({
getList: {
schema: {
results: new Collection([schema]),
total: 0,
limit: 0,
skip: 0,
},
},
});
}

GraphQL + REST Hybrid​

当你的 API 同时提供 REST 和 GraphQL endpoint 时,可以在同一个 resource 中混合使用它们。使用 Entity.process() 来规范化不同形态的响应。

import { GQLEndpoint } from '@data-client/graphql';
import { Entity, resource } from '@data-client/rest';

const gql = new GQLEndpoint('https://api.myservice.com/graphql');

export class Repository extends Entity {
id = '';
name = '';
owner = { login: '' };
stargazersCount = 0;
forksCount = 0;

pk() {
return `${this.owner.login}/${this.name}`;
}

static key = 'Repository';
}

/** Normalizes GraphQL response shape to match REST Entity */
export class GqlRepository extends Repository {
static process(input: any, parent: any, key: string | undefined) {
// GraphQL uses different field names than REST
if ('stargazerCount' in input) {
return {
...input,
stargazersCount: input.stargazerCount,
forksCount: input.forkCount,
};
}
return input;
}
}

export const RepositoryResource = resource({
path: '/repos/:owner/:repo',
schema: Repository,
}).extend(base => ({
// REST endpoint for single repo
get: base.get,
// GraphQL endpoint for user's pinned repos
getByPinned: gql.query(
(v: { login: string }) => `query ($login: String!) {
user(login: $login) {
pinnedItems(first: 6, types: REPOSITORY) {
nodes {
... on Repository {
id
name
owner { login }
stargazerCount
forkCount
}
}
}
}
}`,
{ user: { pinnedItems: { nodes: [GqlRepository] } } },
),
}));

Github 示例​

探索 github-app 示例

更多演示