我厌倦了 useQuery/Promise.all 的 Spaghetti,于是我构建了这个 🫖🦡
Source: Dev.to
我使用 React。我不喜欢 React。我被它束缚住了。
我的后端是一堆热闹的微服务。很多很多。到处都是引用数据——工单、用户、团队、角色、观察者、负责人、权限。随着产品规模的扩大,前端变成了一个请求中继:
- 获取工单。
- 获取受让人。
- 获取受让人的团队。
- 获取团队负责人。
- 获取观察者。
- 获取角色。
每个字段都是一个 ID,每个 ID 都要再来一次往返。有时同一个用户 ID 在树中出现三次,导致三次请求。幸好有 TanStack Query,否则我早就崩溃了。
在更理想的世界里,我们会有 Redis、GraphQL,或者一个统一的 API。相反,我们只有没有共同基础的妖怪微服务。还有 Elasticsearch,但如果用错了会让人抓狂。所以我们只能这样。
我厌倦了,于是我做了一个东西。
任务
你的 REST API 返回 ID。你的 UI 需要对象。所以你编写了解析代码。
一开始很简单:
const ticket = await fetchTicket('t-1');
const assignee = await fetchUser(ticket.assigneeId);
随后代码膨胀。Ticket 现在有了 watcher。Assignee 有了 team。Team 又有了 lead。最终你得到一堆 Promise.all、useQueries、空值检查以及单独的 fetch。没有批量请求。没有去重。类型一团糟。每新增一个字段就要增加样板代码和额外的渲染。
const assignee = ticket.assigneeId ? await fetchUser(ticket.assigneeId) : null;
const watchers = await Promise.all(
(ticket.watcherIds ?? []).map(id => (id ? fetchUser(id) : null))
);
const team = assignee?.teamId ? await fetchTeam(assignee.teamId) : null;
const lead = team?.leadUserId ? await fetchUser(team.leadUserId) : null;
// … 你明白我的意思了
页面最终会出现数十个引用字段,代码变得让人疲惫不堪。
我制作的东西
@nimir/references – 一个类型安全的嵌套引用解析器。你只需一次性定义 sources(批量获取的方式),随后声明哪些字段是 ID。库会自动处理批处理、去重、缓存以及嵌套遍历(最多 10 层)。它对 null 安全并且完整提供类型。
定义 sources
import { defineReferences } from '@nimir/references';
const refs = defineReferences(c => ({
User: c.source({ batch: ids => fetchUsers(ids) }),
Team: c.source({ batch: ids => fetchTeams(ids) }),
Role: c.source({ batch: ids => fetchRoles(ids) }),
}));
声明引用映射
const result = await refs.inline(ticket, {
fields: {
assigneeId: {
source: 'User',
fields: {
teamId: {
source: 'Team',
fields: { leadUserId: 'User' },
},
roleIds: 'Role',
},
},
watcherIds: 'User',
},
});
所有 User 的获取(受理人、观察者、团队负责人)都会被合并为一次调用。重复的 ID 只会获取一次。解析后的值会附加 T 后缀(assigneeIdT、watcherIdTs …),TypeScript 会自动推断它们的类型。
React + TanStack Query
Since you’re stuck with React, there’s a React entry point.
import { defineReferences } from '@nimir/references/react';
const useTicket = refs.hook(useGetTicket, {
fields: { assigneeId: 'User', watcherIds: 'User' },
});
function TicketCard({ id }: { id: string }) {
const { result, status, error, invalidate } = useTicket(id);
// result.assigneeIdT → User | null
}
或者以响应式方式解析内联数据:
const resolved = refs.use(data, { fields: { assigneeId: 'User' } });
支持 TanStack Query、SWR,或任何返回数据的 hook。
缓存
Sources 支持可插拔缓存:内存缓存、IndexedDB(通过 idb-keyval)或 Redis。您可以配置 TTL、负向缓存和缓存预热。如果后端已有 Redis,直接接入即可;否则,使用内存缓存或 IndexedDB 仍能显著减少重复获取。
注意事项
- 深度限制为 10 层(防止循环配置导致无限循环)。
- 未知的来源名称会被静默跳过——
fields中的拼写错误会导致没有数据。TypeScript 有帮助,但运行时不会警告。
可能是我的单一使用案例
我为自己的混乱构建了它:REST 微服务,ID 随处可见,没有 GraphQL,也没有统一的后端。如果你正在处理遗留 API、第三方服务或混合数据源,这可能会有所帮助。如果你能够控制 API 并使用 GraphQL,请改用 GraphQL。
另说
构建过程非常有趣。我在工作时开始了它,随后它演变成了这个开源包。特别感谢我的女朋友,虽然她不懂这东西,但在精神上一直支持我。干杯!