跳到主要内容

v0.15:Vue 3、Collection.remove、useDebounce 升级

· 阅读需 10 分钟
Nathaniel Tucker
Creator of Reactive Data Client

新平台:

  • Vue 3,提供完整的组合式函数:useSuspense、useCache、useDLE、useController、useQuery

新特性:

性能:

破坏性变更:

使用自动化 codemod 完成常见破坏性变更的升级:

npx jscodeshift -t https://dataclient.io/codemods/v0.15.js --extensions=ts,tsx,js,jsx src/

Vue 3 支持​

全新的 @data-client/vue 包以原生组合式函数的形式,将 Reactive Data Client 的全部能力带到了 Vue 3。#3549、#3585

安装​

npm install @data-client/vue @data-client/rest

设置​

在你的应用上安装 DataClientPlugin:

main.ts
import { createApp } from 'vue';
import { DataClientPlugin } from '@data-client/vue';

const app = createApp(App);
app.use(DataClientPlugin);
app.mount('#app');

组合式函数​

所有你熟悉的 hook 都以 Vue 组合式函数的形式提供:

ArticleDetail.vue
<script setup lang="ts">
import { useSuspense } from '@data-client/vue';
import { ArticleResource } from './resources';

const props = defineProps<{ id: string }>();

const article = await useSuspense(
ArticleResource.get,
computed(() => ({ id: props.id })),
);
</script>

<template>
<article>
<h1>{{ article.title }}</h1>
<p>{{ article.content }}</p>
</article>
</template>

可用的组合式函数:

使用 MockPlugin 进行测试​

test setup
import { createApp } from 'vue';
import { DataClientPlugin } from '@data-client/vue';
import { MockPlugin } from '@data-client/vue/test';

const app = createApp(App);
app.use(DataClientPlugin);
app.use(MockPlugin, {
fixtures: [
{
endpoint: MyResource.get,
args: [{ id: 1 }],
response: { id: 1, name: 'Test' },
},
],
});

Collection.remove​

新的 schema,用于在不删除实体本身的情况下从 Collection 中移除条目。#3560

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

const TodoCollection = new schema.Collection([Todo]);
const TodoResource = resource({
path: '/todos/:id',
schema: Todo,
}).extend({
getList: { schema: TodoCollection },
});

// Remove from collection without deleting
ctrl.set(TodoResource.getList.schema.remove, { id: '123' });

当某个条目应从列表视图中移除、但仍需存在于其他地方时,这非常有用:

// Remove user from a specific group list
ctrl.set(UserResource.getList.schema.remove, { id: userId, group: 'admins' });

RestEndpoint.remove​

RestEndpoint.remove 将 PATCH 更新与集合移除结合在一起——非常适合“归档”或“移动”操作。#3623

const getTodos = new RestEndpoint({
path: '/todos',
schema: new schema.Collection([Todo]),
});

// Removes Todo from collection AND updates it with new data
await ctrl.fetch(
getTodos.remove,
{},
{ id: '123', title: 'Done', completed: true, archived: true }
);
// Move user between groups
await ctrl.fetch(
UserResource.getList.remove,
{ group: 'team-a' },
{ id: 2, username: 'bob', group: 'team-b' }
);
// User removed from 'team-a' list AND entity updated with group: 'team-b'

Union 查询​

Union 现在无需指定类型判别字段即可查询。#3558

Before
// Type discriminator required
// @ts-expect-error - missing 'type'
const event = useQuery(EventUnion, { id });
// event is undefined

const newsEvent = useQuery(EventUnion, { id, type: 'news' });
// newsEvent is found
After
// Works without type discriminator
const event = useQuery(EventUnion, { id });
// event is found!

const newsEvent = useQuery(EventUnion, { id, type: 'news' });
// newsEvent is also found

Invalidate 支持 Union​

schema.Invalidate 现在接受 Union schema,用于多态删除操作。此外,resource().delete 会自动用 Invalidate 包装 Union schema。#3559

const FeedUnion = new schema.Union(
{ posts: Post, comments: Comment },
'type',
);

// resource() automatically handles Union delete
const FeedResource = resource({
path: '/feed/:id',
schema: FeedUnion,
});
await ctrl.fetch(FeedResource.delete, { id: '123' });

// For standalone endpoints, use schema.Invalidate directly
const deleteFeedItem = new RestEndpoint({
path: '/feed/:id',
method: 'DELETE',
schema: new schema.Invalidate(FeedUnion),
});
await ctrl.fetch(deleteFeedItem, { id: '123' });

测试工具​

mockInitialState()​

用于在测试中创建预填充状态的新工具。a4092a1

import { mockInitialState } from '@data-client/react/mock';
import { ArticleResource } from './resources';

const state = mockInitialState([
{
endpoint: ArticleResource.get,
args: [{ id: 5 }],
response: { id: 5, title: 'Hello', content: 'World' },
},
]);

@data-client/core/mock​

新的 mock 入口,提供用于构建自定义 mock 实现的工具:

import {
MockController,
collapseFixture,
createFixtureMap,
} from '@data-client/core/mock';
  • MockController - 用于 mock endpoint 的 Controller 包装器
  • collapseFixture - 解析 fixture 响应(处理函数形式的响应)
  • createFixtureMap - 将 fixture 拆分为静态映射和 interceptor

性能​

移除 ImmutableJS 自动检测开销并优化反规范化代码路径,为 get/denormalize 操作带来了 10-20% 的提升。#3421、#3468

历史基准测试 | 查看基准测试

其他改进​

  • 修复 paginationField 位于 body 中时 getPage 的类型
  • 修复 schema.All() 对已失效实体的多态处理
  • 修复 controller.get 和 controller.getQueryMeta 的 'state' 参数类型

迁移指南​

本次升级需要同时更新所有包的版本。

npm install --save @data-client/react@^0.15.0 @data-client/rest@^0.15.0 @data-client/test@^0.15.0 @data-client/img@^0.15.0

自动化 codemod 可以处理下面这些破坏性变更:

npx jscodeshift -t https://dataclient.io/codemods/v0.15.js --extensions=ts,tsx,js,jsx src/

useDebounce() 返回 [val, isPending]​

useDebounce() 现在返回一个包含 pending 状态的元组,与 React 19 的 useTransition 模式保持一致。#3459

Before
import { useDebounce } from '@data-client/react';

const debouncedQuery = useDebounce(query, 100);
After
import { useDebounce } from '@data-client/react';

const [debouncedQuery] = useDebounce(query, 100);
// Or use the pending state
const [debouncedQuery, isPending] = useDebounce(query, 100);
提示

旧的签名仍可通过 @data-client/react/next 使用,以便逐步迁移。

ImmutableJS 移至 /imm 导出​

ImmutableJS 支持不再自动检测。请使用显式的 /imm 导出来处理 ImmutableJS 状态。#3421、#3468

Before
import { MemoCache, denormalize } from '@data-client/normalizr';

const memo = new MemoCache();
// Auto-detected ImmutableJS
After
import { MemoCache } from '@data-client/normalizr';
import { MemoPolicy, denormalize, normalize } from '@data-client/normalizr/imm';

const memo = new MemoCache(MemoPolicy);

/imm 入口现在同时包含 normalize() 和 denormalize(),以完整支持 ImmutableJS 状态处理:

import { normalize } from '@data-client/normalizr/imm';
import { fromJS } from 'immutable';

const result = normalize(Article, responseData, args, {
entities: fromJS({}),
indexes: fromJS({}),
entitiesMeta: fromJS({}),
});
// result.entities is an ImmutableJS Map

这一变化为不使用 ImmutableJS 的用户移除了 ImmutableJS 检测开销,带来 10-20% 的性能提升。

Schema delegate 接口变更​

如果你有重写了 normalize() 或 queryKey() 的自定义 schema,这些回调参数已被整合为一个 delegate 对象。#3449

normalize()​

Before
normalize(
input: any,
parent: any,
key: string | undefined,
args: any[],
visit: (...args: any) => any,
addEntity: any,
getEntity: any,
checkLoop: any,
): string {
addEntity(this, processedEntity, id);
}
After
normalize(
input: any,
parent: any,
key: string | undefined,
args: any[],
visit: (...args: any) => any,
delegate: INormalizeDelegate,
): string {
delegate.mergeEntity(this, id, processedEntity);
}

queryKey()​

Before
queryKey(args, queryKey, getEntity, getIndex) {
getIndex(schema.key, indexName, value)[value];
getEntity(this.key, id);
return queryKey(this.schema, args, getEntity, getIndex);
}
After
queryKey(args, unvisit, delegate) {
delegate.getIndex(schema.key, indexName, value);
delegate.getEntity(this.key, id);
return unvisit(this.schema, args);
}

delegate.getEntity → delegate.getEntities #3501​

遍历某一类型的全部实体时,请使用 delegate.getEntities(),它返回一个受限接口,提供 keys() 和 entries() 迭代器方法:

Before
const entities = delegate.getEntity(this.key);
if (entities)
Object.keys(entities).forEach(collectionPk => {
if (!filterCollections(JSON.parse(collectionPk))) return;
delegate.mergeEntity(this, collectionPk, normalizedValue);
});
After
const entities = delegate.getEntities(this.key);
if (entities)
for (const collectionKey of entities.keys()) {
if (!filterCollections(JSON.parse(collectionKey))) continue;
delegate.mergeEntity(this, collectionKey, normalizedValue);
}

移除 INVALID 符号​

INVALID 符号不再导出。请改用 delegate 的方法。#3461

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

normalize(..., delegate) {
delegate.setEntity(this, pk, INVALID);
}

queryKey(args, unvisit, delegate) {
if (!found) return INVALID;
}
After
normalize(..., delegate) {
delegate.invalidate({ key: this.key }, pk);
}

queryKey(args, unvisit, delegate) {
if (!found) return delegate.INVALID;
}

state.entityMeta -> state.entitiesMeta​

为保持一致性而重命名的内部状态属性。这只影响直接访问 store 的代码。#3451

NetworkManager 数据结构变更​

NetworkManager 的 fetched、rejectors、resolvers、fetchedAt 已被整合为 fetching。#3394

Before
if (action.key in this.fetched)
After
if (this.fetching.has(action.key))

MemoCache API 变更(normalizr)​

面向直接使用 @data-client/normalizr 的用户:

MemoCache.query() 和 buildQueryKey() 以单个参数接收 state #3372​

Before
this.memo.buildQueryKey(schema, args, state.entities, state.indexes, key);
this.memo.query(schema, args, state.entities, state.indexes);
After
this.memo.buildQueryKey(schema, args, state, key);
this.memo.query(schema, args, state);

MemoCache.query() 返回 { data, paths }​

const { data } = this.memo.query(schema, args, state);
return typeof data === 'symbol' ? undefined : (data as any);

升级支持​

一如既往,如果你遇到任何问题或有任何疑问,欢迎加入我们的 Chat 或提交 bug