v0.19:批量 Controller.set(),更快的 TypeScript
这篇翻译之后英文原文有更新,内容可能已过时。
v0.19 让 Manager 可以在一次 store 更新中写入一整批流式到达的行,把 endpoint 代码的类型检查速度提高约 2 倍,并修复了一批 TypeScript 和 Vue 问题。编码智能体还获得了 Claude Code 插件、Vue skill 和 llms.txt;请参阅面向编码智能体的 Data Client。
新 API:
- 支持 Array schema 的 Controller.set() - 用
ctrl.set([Entity], rows)在一次 store 更新中写入多个 entity,比每行调用一次set()最多快 95 倍
性能:
- 更快的 TypeScript - 编辑器和
tsc检查 RestEndpoint 与 resource() 代码的速度提高约 2 倍,内存减少 40%,并且之前能捕获的错误一个都不少
其他改进:
- 修复 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)
- 需要 TypeScript 4.0 或更高版本 - 移除了 TypeScript 3.x 的声明文件(#4151)
- 在
Entity.pk()重写中把args标注为readonly - 用
DataClientPlugin代替 Vue 的createDataClient() - 替换 Vue 测试中的
waitForNextUpdate()
批量 Controller.set()
接收一连串行数据(比如通过 websocket 传来的价格行情)的 Manager,现在可以通过传入
Array schema,用一次 Controller.set() 把它们全部写入。在 v0.19 之前,[Ticker] 会是 TypeScript 错误,所以常见的变通做法是每行调用一次 set():
ws.onmessage = event => {
const rows = JSON.parse(event.data);
for (const row of rows) {
ctrl.set(Ticker, { product_id: row.product_id }, row);
}
};
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
在 Manager 中,缓冲到达的消息并用一次 set() 刷出每一批,具体做法见批量处理高频更新。coin 应用的 StreamManager 现在就是这样刷出 Coinbase 的行情消息。
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]));
}
};
探索 coin-app 示例
如果你按 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 次运行中的最佳值。
endpoint 密集的代码所做的类型工作少了很多。类型实例化是 TypeScript 的工作单位:它们是确定性的,因此可以在不同机器间干净地比较。Union 比 v0.18 做了更多工作,因为 v0.19 现在会对 set() 的值做类型检查(见下文)。
悬停或点击单元格可查看精确数字。
| TS 6 check time | TS 7 check time | TS 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):
ctrl.set([new Invalidate(Todo)], [{ id: '5' }]);
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)。
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);
}
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 排序:
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)。去掉断言即可:
<form
onSubmit={e => {
e.preventDefault();
ctrl.fetch(PostResource.create, new FormData(e.currentTarget) as any);
}}
>
<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() 包装,现在可以去掉它:
import { computed } from 'vue';
const props = defineProps<{ id: number }>();
const article = await useSuspense(
ArticleResource.get,
computed(() => ({ id: props.id })),
);
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)。
import { DataClientPlugin, GCPolicy } from '@data-client/vue';
app.use(DataClientPlugin, { gcPolicy: new GCPolicy() });
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 中。
如果你为此做过变通处理,现在可以去掉了。普通的真值检查就足够了:
function TodoDetail({ id }: { id: number }) {
const todo = useCache(TodoResource.get, { id });
if (!todo || typeof todo === 'symbol') return <TodoPlaceholder />;
return <h3>{todo.title.trim()}</h3>;
}
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,因此不受影响。
迁移指南
本次升级需要同时更新所有包的版本。
- NPM
- Yarn
- pnpm
- esm.sh
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
yarn add @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
pnpm add @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
<script type="importmap">
{
"imports": {
"@data-client/react": "https://esm.sh/@data-client/react@^0.19.0",
"@data-client/rest": "https://esm.sh/@data-client/rest@^0.19.0",
"@data-client/endpoint": "https://esm.sh/@data-client/endpoint@^0.19.0",
"@data-client/core": "https://esm.sh/@data-client/core@^0.19.0",
"@data-client/vue": "https://esm.sh/@data-client/vue@^0.19.0",
"@data-client/test": "https://esm.sh/@data-client/test@^0.19.0",
"@data-client/img": "https://esm.sh/@data-client/img@^0.19.0"
}
}
</script>
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。
class User extends Entity {
static pk(value: any, parent?: any, key?: string, args?: any[]) {
return `${value.id}-${args?.[0]?.org}`;
}
}
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
import { createDataClient } from '@data-client/vue';
const { controller } = createDataClient({ managers, initialState });
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
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();
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();
升级支持
一如既往,如果你遇到任何问题或有任何疑问,欢迎加入我们的 或提交 bug
