跳到主要内容

v0.19:批量 Controller.set(),更快的 TypeScript

· 阅读需 29 分钟
Nathaniel Tucker
Creator of Reactive Data Client
备注

这篇翻译之后英文原文有更新,内容可能已过时。

v0.19 让 Manager 可以在一次 store 更新中写入一整批流式到达的行,把 endpoint 代码的类型检查速度提高约 2 倍,并修复了一批 TypeScript 和 Vue 问题。编码智能体还获得了 Claude Code 插件、Vue skill 和 llms.txt;请参阅面向编码智能体的 Data Client。

新 API:

性能:

其他改进:

  • 修复 TypeScript 7 对包 exports 的模块解析;导入现在会解析到声明文件(#4019)
  • renderDataHook() 在首次渲染挂起时也会运行 provider 的挂载副作用(#4099)
  • Controller.set() 的值按 schema 推导类型,因此 ctrl.set(new schema.All(Todo), 42) 会是 TypeScript 错误(#4133)
  • Controller.set() 可用 Invalidate schema 删除单个 entity,不再需要包成单行批量的变通写法(#4230)
  • 在 new RestEndpoint()、.extend() 和 resource().extend() 中,process() 获得带类型的 params,因此写错路径参数会是 TypeScript 错误,而不是运行时的 undefined(#4183、#4184)
  • getOptimisticResponse() 的返回值按 schema 推导类型,因此字段类型写错会是 TypeScript 错误,而不是闪现一下错误数据(#4198)
  • Vue 的 useSuspense() 和 useLive() 在新参数加载期间保留之前的数据,而不是返回 undefined(#4131)
  • Vue 的 useSuspense() 和 useLive() 把参数变化后的获取错误发送给 onErrorCaptured(),而不是产生未处理的 promise rejection(#4135)
  • Vue 的 useSuspense()、useDLE() 和 useFetch() 不再在每次 store 更新时重新获取过时数据,因此 controller.set() 不会被覆盖(#4134)
  • Vue 的 useSuspense() 在挂载时显示过时数据并在后台重新获取,与 React 一致,而不是在重新获取完成前一直显示 <Suspense> 的 fallback(#4169)
  • Vue 的 useDLE() 和 useCache() 在无关的 store 更新中保留已过期的 invalidIfStale 数据,而不是卡在加载状态(#4142)
  • Vue 的 DataClientPlugin 可以安装在 3.5 之前的 Vue 版本上,而不是抛出 app.onUnmount is not a function(#4146)
  • Vue 的 DataClientPlugin 默认进行垃圾回收,与 React 的 DataProvider 一致,因此你不再需要传入 gcPolicy;它如何决定移除什么(#4148)
  • Vue 组合式函数接受像 () => ({ id: props.id }) 这样的 getter 参数;之前类型上允许,但却把函数本身传给了 endpoint(#4115)
  • 修复在关闭 skipLibCheck 的 TypeScript 4.x 上出现的 Cannot find name 'NoInfer' 和 export type 错误(#4138)
  • 修复在关闭 skipLibCheck 的 TypeScript 4.0–4.5 上 Entity、Endpoint、Union 和 RestEndpoint 的类型错误(#4140)
  • 修复在 TypeScript 4.0–4.4 上导入 @data-client/react/redux、@data-client/react/nextjs 或 @data-client/test 时的 ',' expected 错误(#4232)
  • 修复在 TypeScript 4.0–4.4 上 @data-client/test 的 act 和 renderHook 被推导为 any 的问题(#4251)
  • Entity 类可以用在期望 EntityInterface 的地方(#4149);为未来版本准备好 pk() 重写
  • useCache() 和 useDLE() 对已删除且重新获取失败的 entity 返回 undefined,而不是一个能绕过 if (!data) 检查的真值 Symbol;移除所有变通代码(#4150)
  • Vue 的 useFetch() 在挂载期间保护其数据不被垃圾回收,与 useSuspense() 一致,因此配置的 gcPolicy 不再会清除组件稍后要读取的预取数据(#4152)
  • 打开 Redux DevTools 的应用在开发模式下遇到大型 store 或频繁更新时不再卡顿;每次更新的序列化快 40-60 倍(#4163)
  • 传入 endpoint 覆盖对象的 resource().extend() 会在类型中保留之前用 .extend('name', options) 添加的 endpoint,因此使用它们不再是 TypeScript 错误(#4184)
  • 当 Query 或 Lazy 包装 Collection 时,RestEndpoint.push、unshift 和 remove 接受 Entity 的字段作为 body,而不是只接受 FormData(#4226)
  • Vue 的 useFetch() 的类型现在是它返回的只读 Ref,因此 promise.resolved(始终为 undefined)现在是 TypeScript 错误;请改为读取 promise.value.resolved(#4114)
  • 传给 DataProvider 或 DataClientPlugin 的 Controller 子类不再需要类型断言就能通过类型检查(#4228)
  • prepareStore() 的 store 可以不加类型断言直接传给 react-redux 的 <Provider>,并且其类型中包含 dispatch()(#4238)
  • resource().create 接受 FormData,无需类型断言,与 update、partialUpdate 和 getList.push 一致(#4235)
  • RestEndpoint.push、unshift 和 remove 以 JSON 形式发送数组 body,因此一次创建或移除多个条目不再向服务器发送 "[object Object]";resource().create 也接受数组(#4262)
  • Vue 的 createDataClient() 已弃用,因为它是 DataClientPlugin 的内部实现;请用 app.use(DataClientPlugin, options) 安装插件,而不是直接调用它(#4193)
  • Vue 的 renderDataCompose() 的 waitForNextUpdate() 已弃用,因为它会在 1 秒后静默放弃,让仍处于挂起状态的测试通过;请改为 await result,或在变化之后 await allSettled(),后者现在也会等待由 prop 变化触发的获取(组合式函数的单元测试、#4221)

破坏性变更:

为未来版本做好准备:

批量 Controller.set()​

接收一连串行数据(比如通过 websocket 传来的价格行情)的 Manager,现在可以通过传入 Array schema,用一次 Controller.set() 把它们全部写入。在 v0.19 之前,[Ticker] 会是 TypeScript 错误,所以常见的变通做法是每行调用一次 set():

Before
ws.onmessage = event => {
const rows = JSON.parse(event.data);
for (const row of rows) {
ctrl.set(Ticker, { product_id: row.product_id }, row);
}
};
After
ws.onmessage = event => {
const rows = JSON.parse(event.data);
ctrl.set([Ticker], rows);
};

每一行都会与已存储的 entity 合并,列表中没有的 entity 则保持不变。Array schema 不接受 args,也不接受更新函数。要批量处理混合的 Entity 类型、删除操作或按 id 作为键的行,请传入 Union、 Invalidate 或 Values schema;示例请参阅 Controller.set()。 #4103

试试下面的两个按钮。这个浏览器测试从空的 store 开始,分别计时 500 次 set() 调用的 Promise.all 与一次批量 set()。两种方式都只产生一次 React 提交,并且各自写入 500 个新价格。

import React from 'react';
import { useController, useQuery } from '@data-client/react';
import { Ticker, newPrices } from './Ticker';

function PriceStream() {
  const ctrl = useController();
  const [timing, setTiming] = React.useState('');
  const first = useQuery(Ticker, { product_id: 'COIN-0' });

  const time = async (
    label: string,
    write: (rows: ReturnType<typeof newPrices>) => Promise<unknown>,
  ) => {
    const rows = newPrices();
    const start = performance.now();
    await write(rows);
    setTiming(`${label}: ${(performance.now() - start).toFixed(1)} ms`);
  };
  const perRow = () =>
    time('500 set() calls', rows =>
      Promise.all(
        rows.map(row => ctrl.set(Ticker, { product_id: row.product_id }, row)),
      ),
    );
  // highlight-next-line
  const batch = () => time('1 batch set()', rows => ctrl.set([Ticker], rows));

  return (
    <div>
      <button onClick={perRow}>set() per row</button>{' '}
      <button onClick={batch}>batch set()</button>
      <p>COIN-0: {first ? `$${first.price}` : 'no data yet'}</p>
      <p>{timing}</p>
    </div>
  );
}
render(<PriceStream />);
结果

性能​

每次 set() 都是一次独立的 store 更新,而每次 store 更新都会复制该 entity 类型的整张表。逐行写入会为每一行重复这次复制,因此开销随批量大小和 store 大小一起增长。批量写入只需付出一次复制的代价。

下图是 examples/benchmark/core.js 中的 node setMany 基准测试:一个已持有 500 个 entity 的 store,对比每行同步调用一次 set() 与一次 set([Ticker], rows)。它测量的是 store 更新:50 行时快 20 倍,500 行时快 95 倍。 #4103

Node 向 500 个 entity 的 store 执行 setMany
50 rows20xOne set() per row 10.8 ms → set([Ticker], rows) 0.54 ms
500 rows95xOne set() per row 103 ms → set([Ticker], rows) 1.08 ms

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

在 Manager 中,缓冲到达的消息并用一次 set() 刷出每一批,具体做法见批量处理高频更新。coin 应用的 StreamManager 现在就是这样刷出 Coinbase 的行情消息。

StreamManager.ts
handleMessage(msg: any) {
if (msg.type in this.entities) {
(this.buffer[msg.type] ??= {})[msg.product_id] = msg;
this.flushTimeout ??= setTimeout(this.flush, 50);
}
}

flush = () => {
const buffer = this.buffer;
this.buffer = {};
this.flushTimeout = undefined;
for (const type in buffer) {
this.controller.set([this.entities[type]], Object.values(buffer[type]));
}
};

更多演示

提示

如果你按 schema 过滤 DevToolsManager 的 action,批量写入的 action.schema 是 Array schema,因此对于 [Ticker] 应匹配 action.schema[0],而不是 Entity 本身。

更快的 TypeScript​

TypeScript 会在编辑器里的每次按键和每次 CI 构建时重新检查你的代码。在拥有大量 endpoint 的应用中,沉重的库类型表现为迟缓的自动补全、要等几秒才出现的红色波浪线,以及更慢的构建。 v0.19 让 RestEndpoint、resource() 和 .extend() 的检查开销大幅降低,你无需改动任何代码。TypeScript 仍然会报告之前报告过的每一个错误(#4173)。

结果​

我们最重的压力测试,一个包含 150 个带长路径、.extend() 和 .paginated() 的 RestEndpoint 的文件,现在在 TypeScript 6 上检查快 2 倍,在 TypeScript 7 上快 2.2 倍,内存用量减少约 40%。所有数字都是已发布的 v0.18.1 包与 v0.19 的对比;检查时间取 10 次运行中的最佳值。

检查 150 个带长路径的 RestEndpoint
TypeScript 61.9xv0.18 3.83 s → v0.19 1.98 s
TypeScript 72.2xv0.18 1.5 s → v0.19 0.69 s
同一文件的内存用量
TypeScript 61.7xv0.18 365 MB → v0.19 211 MB
TypeScript 71.7xv0.18 220 MB → v0.19 128 MB

endpoint 密集的代码所做的类型工作少了很多。类型实例化是 TypeScript 的工作单位:它们是确定性的,因此可以在不同机器间干净地比较。Union 比 v0.18 做了更多工作,因为 v0.19 现在会对 set() 的值做类型检查(见下文)。

各压力测试的类型实例化次数
Long paths2.4xv0.18 822 K → v0.19 341 K
Typical app1.5xv0.18 20.2 K → v0.19 13.4 K
React hooks1.2xv0.18 145 K → v0.19 122 K
Vue1.3xv0.18 62.6 K → v0.19 47.3 K
300 fields1.3xv0.18 17 K → v0.19 12.8 K
Schemas1.2xv0.18 118 K → v0.19 99.3 K
Union0.6xv0.18 9 K → v0.19 16.1 K

悬停或点击单元格可查看精确数字。

TS 6 check timeTS 7 check timeTS 6 memory
Long paths
150 RestEndpoints with 6-param paths, .extend() and .paginated()
-48%
3.83s → 1.98s
-54%
1.5s → 0.69s
-42%
365MB → 211MB
Typical app
A few resources with .extend(), .paginated() and hooks
same
0.42s → 0.42s
-15%
0.062s → 0.053s
+3%
106MB → 109MB
React hooks
40 resources through every hook, plus ctrl.fetch() and ctrl.set()
-10%
1.24s → 1.12s
-7%
0.41s → 0.38s
-6%
167MB → 157MB
Vue
The same 40 resources through every composable
same
0.68s → 0.68s
-12%
0.17s → 0.15s
-4%
145MB → 139MB
300 fields
One Entity with 300 fields, read and updated 100 times
-14%
0.44s → 0.38s
-40%
0.086s → 0.052s
-9%
111MB → 101MB
Schemas
All, Query, Invalidate, Array, Object and Collection
-3%
1.1s → 1.07s
-14%
0.36s → 0.31s
-4%
156MB → 150MB
Union
A 30-member Union in a Collection and Values
-2%
0.42s → 0.41s
-4%
0.071s → 0.068s
-6%
107MB → 101MB
set() values
1000 ctrl.set() calls on a 30-member Union, a Collection of it and a 300-field Entity
-28%
1.49s → 1.07s
-54%
0.54s → 0.25s
-26%
176MB → 131MB
set() updaters
1000 ctrl.set(Union, args, prev => ...) updaters on a 30-member Union
-4%
3.58s → 3.43s
+11%
1.67s → 1.85s
+1%
381MB → 384MB

set() 各行测量的是带类型的 set() 值,v0.18 完全不检查这些值。普通值的检查仍比之前更快,而大型 Union 上的更新函数检查耗时大致相同,尽管 TypeScript 现在会检查每个更新函数的返回值(#4179)。

小文件的开销主要来自 TypeScript 固定的启动成本(仅加载 lib.dom.d.ts 就要约 100MB),所以它们的时间和内存几乎不变。随着代码库增长,节省的开销会逐渐累积。

改了什么​

  • .extend() 和 .paginated() 在所有 endpoint 之间共享,而不是为每种 endpoint 类型重新创建。
  • endpoint 选项被推导为普通对象类型,因此 TypeScript 不再在每次使用时重新构建它们。
  • 像 /users/:id 这样的路径参数只需一遍即可读取完毕。

发布之前,我们在 TypeScript 4.0 到 7 上关闭了测试套件中的每一个 @ts-expect-error,并确认每个错误仍然出现在同一位置。

在 TypeScript 5.x 及更早版本上,传给 .extend() 的 process(value, params) 方法在 strict 下也不再报 "implicitly has an 'any' type" 错误。

其他改进​

带类型的 set() 值​

Controller.set() 之前对任何 schema 都接受任意值,所以拼写错误或字段类型写错只会在运行时表现为错误数据。现在值按 schema 推导类型: Entity 接受它的字段,而 Collection、All 或 Array 接受一个行列表。每个字段都是可选的,因为 set() 会合并到已存储的数据中;并且数字和字符串可以互换,就像在 API 响应中一样。 #4133

悬停在红色下划线上可查看每个错误。

import type { Controller } from '@data-client/react';
import { schema } from '@data-client/rest';
import { Todo, TodoResource } from './Todo';

export function updateTodos(ctrl: Controller) {
  // ✅ rows are partial Todos; ids may be strings or numbers
  ctrl.set(TodoResource.getList.schema, [{ id: '5', completed: true }]);
  ctrl.set(Todo, { id: 5 }, todo => ({ completed: !todo.completed }));
  ctrl.set([Todo], [{ id: 1, title: 'first' }, { id: 2 }]);

  // ❌ All takes a list of rows
  ctrl.set(new schema.All(Todo), 42);
  // ❌ completed is a boolean
  ctrl.set(Todo, { id: 5 }, { id: 5, completed: 'yes' });
  // ❌ Todo has no done field
  ctrl.set(TodoResource.getList.schema, [{ id: 5, done: true }]);
  // ❌ updaters must return Todo fields
  ctrl.set(Todo, { id: 5 }, todo => ({ title: todo.completed }));
}

Query 接受它所包装 schema 的输入,因为 set() 规范化的是那个 schema,而不是反转 process()。当每个成员都把判别字段声明为字面量(如 readonly type = 'post')时, Union 的行会按它所选中的成员进行检查,因此不能混用不同成员的字段;请参阅 Unions。

如果之前能编译的代码现在在这里报错,说明它写入的数据与其 schema 的描述不符。请修正该值,或者在数据确实可能是那种形状时放宽 Entity 的字段类型。

用 set() 删除单个 entity​

向 Controller.set() 传入 Invalidate schema 会删除其行所标识的 entity,但单个形式曾是 TypeScript 错误,所以代码只能把它包成单行批量。现在它可以通过类型检查,且行按 Entity 的字段推导类型(#4230):

Before
ctrl.set([new Invalidate(Todo)], [{ id: '5' }]);
After
ctrl.set(new Invalidate(Todo), { id: '5' });

与批量 set() 一样,它不接受 args,也不接受更新函数。

带类型的 process() params​

传给 new RestEndpoint()、.extend() 或 resource().extend() 的 process() 方法,其 params 之前被推导为 any,因此读取 endpoint 并不具备的参数也能编译通过,并在运行时返回 undefined。params 和 body 现在从 endpoint 推导类型,包括在同一个 .extend() 调用中设置的 path (#4183、#4184):

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

const getUser = new RestEndpoint({ path: '/users/:id' });

export const getUserById = getUser.extend({
  path: '/users/by-id/:userId',
  process(value, params) {
    const userId: string | number = params.userId;
    // @ts-expect-error 'id' is not a param of '/users/by-id/:userId'
    params.id;
    return { ...value, userId };
  },
});

当每个参数都是可选的时,params 可能为 undefined,因此要用 params?.page 读取。

这可能会在现有的 process() 方法中暴露出新的 TypeScript 错误。每一个错误都标记了一处可能为 undefined 或在运行时抛出异常的读取,因此请修正参数名或补上缺失的检查。

带类型的乐观响应​

除非 endpoint 有带类型的 process(),否则 getOptimisticResponse() 可以返回任何东西,因此字段类型写错只会在服务器响应之前表现为 UI 里的错误数据。返回值现在会按 endpoint 的 schema 检查,就像带类型的 set() 值一样:Entity 接受它的任意字段,而 Collection 接受它们的列表(#4198):

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

class Todo extends Entity {
  id = 0;
  title = '';
  completed = false;
}

export const toggleTodo = new RestEndpoint({
  path: '/todos/:id',
  method: 'PATCH',
  schema: Todo,
  // @ts-expect-error completed is a boolean; 'false' is truthy, so the checkbox would stay checked
  getOptimisticResponse(snap, { id }) {
    const todo = snap.get(Todo, { id });
    if (!todo) throw snap.abort;
    return { id, completed: todo.completed ? 'false' : 'true' };
  },
});

这可能会在现有的 getOptimisticResponse() 方法中暴露出新的 TypeScript 错误。每一个错误都标记了一个在服务器响应之前会以错误类型存储的值,因此请返回字段的真实类型(这里是 !todo.completed)。

排序后的 Collection 上带类型的 push()​

把 Collection 包在 Query 中进行排序或过滤,会让 push、unshift 和 remove 拒绝除 FormData 以外的所有 body,尽管它们在运行时会向列表添加数据。body 现在从 Collection 的 Entity 推导类型,因此可以去掉类型断言,而字段写错会是 TypeScript 错误。被 Lazy 包装的 Collection 也同样适用(#4226)。

Before
import { useController } from '@data-client/react';
import { getPosts } from './getPosts';

export function useAddPost() {
const ctrl = useController();
return (title: string, author: string) =>
ctrl.fetch(getPosts.push, { group: 'react' }, { title, author } as any);
}
After
import { useController } from '@data-client/react';
import { getPosts } from './getPosts';

export function useAddPost() {
const ctrl = useController();
return (title: string, author: string) =>
ctrl.fetch(getPosts.push, { group: 'react' }, { title, author });
}

其中 getPosts 用 Query 对其 Collection 排序:

getPosts.ts
import { Collection, Entity, Query, RestEndpoint } from '@data-client/rest';

export class Post extends Entity {
id = '';
title = '';
group = '';
author = '';
}

export const getPosts = new RestEndpoint({
path: '/:group/posts',
searchParams: {} as { orderBy?: 'title' | 'author' },
schema: new Query(
new Collection([Post], { nonFilterArgumentKeys: /orderBy/ }),
(posts, { orderBy }: { orderBy?: 'title' | 'author' } = {}) =>
orderBy ?
[...posts].sort((a, b) => a[orderBy].localeCompare(b[orderBy]))
: posts,
),
});

resource().create 中的 FormData​

resource() 的 create 会原样发送 FormData body,与 update、partialUpdate 和 getList.push 一致,但它的类型只允许对象,所以提交表单需要类型断言(#4235)。去掉断言即可:

Before
<form
onSubmit={e => {
e.preventDefault();
ctrl.fetch(PostResource.create, new FormData(e.currentTarget) as any);
}}
>
After
<form
onSubmit={e => {
e.preventDefault();
ctrl.fetch(PostResource.create, new FormData(e.currentTarget));
}}
>

create 已弃用;getList.push 是它的替代品,同样接受 FormData。

Vue getter 参数​

像 useSuspense() 和 useLive() 这样的 Vue 组合式函数在类型上接受 getter 函数作为参数,但它们把函数本身传给了 endpoint,而没有调用它。getter 现在与 ref 和 computed 的行为一致,并且当 getter 读取的任何内容发生变化时,组合式函数会重新获取(#4115)。如果你只是为了让参数具有响应性才用 computed() 包装,现在可以去掉它:

Before
import { computed } from 'vue';

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

这适用于每个接受 endpoint 参数的组合式函数:useSuspense()、 useLive()、useCache()、useDLE()、 useFetch()、useQuery() 和 useSubscription()。

Vue 默认进行垃圾回收​

Vue 的 DataClientPlugin 之前只有在你自己传入 gcPolicy 时才会移除未使用的数据。它现在默认使用 new GCPolicy(),与 React 的 DataProvider 相同,因此除非你要自定义它,否则可以去掉这个选项(#4148)。

Before
import { DataClientPlugin, GCPolicy } from '@data-client/vue';

app.use(DataClientPlugin, { gcPolicy: new GCPolicy() });
After
import { DataClientPlugin } from '@data-client/vue';

app.use(DataClientPlugin);

数据只有在没有任何已挂载组件读取它,_并且_自获取以来已经过了两倍新鲜期(至少两分钟)时才会被移除。清扫每五分钟检查一次。在此之前被组件再次读取的任何数据都会重新回到使用中。

在移除之后挂载的组件会把该数据视为从未获取过:useSuspense() 会重新获取它,而 useCache() 会返回 undefined。要让未使用的数据保留更久或减少清扫频率,请传入你自己的 gcPolicy。

Vue stale-while-revalidate​

当组件挂载时的数据已过时但仍然有效,Vue 的 useSuspense() 现在会立即渲染它并在后台重新获取,与 React 一致。之前,它会在重新获取完成前一直显示 <Suspense> 的 fallback(#4169)。

Entity 类可作为 EntityInterface 通过类型检查​

用 EntityInterface 标注为接受任意 Entity 的辅助函数曾经拒绝 Entity 类,因为 Entity.pk() 把它的 args 标注为可变数组。它现在是 readonly any[],因此下面的代码可以通过类型检查(#4149):

import { Entity } from '@data-client/rest';
import type { EntityInterface } from '@data-client/react';

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

function entityName(schema: EntityInterface) {
  return schema.key;
}

entityName(User);

如果你重写了 static pk() 并把 args 标注为可变数组,它目前仍能编译,但未来的破坏性版本将要求 readonly;现在就更新它。

已删除的 entity 读取为 undefined​

当一个 entity 被删除且重新获取失败时,useCache() 和 useDLE() (React 和 Vue)会把一个内部 Symbol 作为数据返回。Symbol 是真值,所以常见的"尚未加载"守卫会放它通过,组件就当作自己拿到了 entity 一样渲染(#4150):

function TodoDetail({ id }: { id: number }) {
const todo = useCache(TodoResource.get, { id });
// a deleted todo used to get past this guard, so todo.title.trim() threw
if (!todo) return <TodoPlaceholder />;
return <h3>{todo.title.trim()}</h3>;
}

现在 data 是 undefined,与它的类型和 Controller.get() 一致。 Controller.getResponse() 和 Controller.fetchIfStale() 也同样如此,例如在自定义 Manager 中。

如果你为此做过变通处理,现在可以去掉了。普通的真值检查就足够了:

Before
function TodoDetail({ id }: { id: number }) {
const todo = useCache(TodoResource.get, { id });
if (!todo || typeof todo === 'symbol') return <TodoPlaceholder />;
return <h3>{todo.title.trim()}</h3>;
}
After
function TodoDetail({ id }: { id: number }) {
const todo = useCache(TodoResource.get, { id });
if (!todo) return <TodoPlaceholder />;
return <h3>{todo.title.trim()}</h3>;
}

要在重新获取失败时显示特定内容(例如删除后的 404),请从 useDLE() 读取 error,而不是检查 data。

更快的 Redux DevTools​

打开 Redux DevTools 扩展后,开发模式下的每次 store 更新都会为扩展序列化整个 store,而其中每个时间戳都是用慢速方式格式化的。大型 store 或频繁更新,比如轮询、实时数据或大量 controller.set() 调用,会让页面卡顿。现在每次更新的序列化快 40-60 倍,而时间戳仍然显示为 10:42:07.123 AM 这样的形式(#4163)。生产构建不包含 DevTools,因此不受影响。

每次 store 更新的 DevTools 序列化
50 entities60xv0.18 18.6 ms → v0.19 0.31 ms
500 entities40xv0.18 201 ms → v0.19 5 ms

迁移指南​

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

npm install --save @data-client/react@^0.19.0 @data-client/rest@^0.19.0 @data-client/endpoint@^0.19.0 @data-client/core@^0.19.0 @data-client/vue@^0.19.0 @data-client/test@^0.19.0 @data-client/img@^0.19.0

TypeScript 4.0 或更高版本​

如果你已经在使用 TypeScript 4.0 或更高版本,请跳过本节。

TypeScript 3.x 的声明文件已被移除,因为它们在任何 TypeScript 3.x 版本上都已无法通过类型检查。请把 devDependencies 中的 typescript 升级到 ^4.0.0 或更高版本。#4151

为未来版本做好准备​

升级到 0.19 并不需要做这里的任何事情;你的代码可以原样继续工作。每一项都会在未来版本中失效,所以现在更新能让那次升级不费任何功夫。

Entity.pk() 的 args 是 readonly​

如果你没有用带类型的 args 参数重写 static pk(),请跳过这一项。

Entity.pk() 现在把 args 标注为 readonly any[]。把它标注为可变数组的重写目前仍能编译,但未来版本将要求 readonly。

Before
class User extends Entity {
static pk(value: any, parent?: any, key?: string, args?: any[]) {
return `${value.id}-${args?.[0]?.org}`;
}
}
After
class User extends Entity {
static pk(value: any, parent?: any, key?: string, args?: readonly any[]) {
return `${value.id}-${args?.[0]?.org}`;
}
}

Vue createDataClient()​

如果你用 app.use(DataClientPlugin) 安装 store,请跳过这一项。

createDataClient() 和 ProvidedDataClient 是 DataClientPlugin 的内部实现,将不再导出。请改为安装插件,并通过 useController() 读取 controller。#4193

Before
import { createDataClient } from '@data-client/vue';

const { controller } = createDataClient({ managers, initialState });
After
import { DataClientPlugin, useController } from '@data-client/vue';

app.use(DataClientPlugin, { managers, initialState });
// in a component's setup()
const controller = useController();

Vue renderDataCompose() 的 waitForNextUpdate()​

如果你的 Vue 测试没有调用 waitForNextUpdate(),请跳过这一项。

renderDataCompose() 的 waitForNextUpdate() 会在 1 秒后静默放弃,因此组合式函数仍处于挂起状态时测试也能通过。它将被移除。请改为 await 结果,并在修改 props 或调用 controller 之后调用 allSettled();它现在也会等待由 prop 变化触发的获取。#4221

Before
const { result, waitForNextUpdate } = await renderDataCompose(
(props: { id: number }) =>
useSuspense(ArticleResource.get, () => ({ id: props.id })),
{ props, resolverFixtures },
);
await waitForNextUpdate();
const article = await result;

props.id = 6;
await waitForNextUpdate();
After
const { result, allSettled } = await renderDataCompose(
(props: { id: number }) =>
useSuspense(ArticleResource.get, () => ({ id: props.id })),
{ props, resolverFixtures },
);
const article = await result;

props.id = 6;
await allSettled();

升级支持​

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