关于 sync-construction-async-property-ui-render-gate-pattern
这项开发技能介绍同步构造客户端、异步执行初始化的模式,并涵盖可等待的就绪属性及带错误恢复的 Svelte 渲染门控。
主要功能
- 01同步构造客户端
- 02可等待的初始化属性
- 03Svelte 根级就绪门控
- 04加载与错误界面处理
适用场景
- 01无需顶层 await 导出客户端
- 02在 UI 组件间共享客户端
- 03就绪后再渲染 Svelte 界面
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ epicenterhq/ epicenter --skill sync-construction-async-property-ui-render-gate-pattern安装后在 Claude Code 中输入/sync-construction-async-property-ui-render-gate-pattern确认已加载。
详细介绍
同步构造,异步属性
客户端的初始化是同步的。异步工作作为一个属性存储,你可以等待它,同时传递对象引用。
何时应用此模式
在以下情况下使用此模式:
- 异步客户端初始化(IndexedDB、服务器连接、文件系统)
- 需要无需
await即可导入的模块导出 - 希望同步访问客户端的 UI 组件
- 希望在 SvelteKit 应用中根据就绪状态控制渲染
以下迹象表明异步构造正在给你带来麻烦:
- 到处都是
await getX()这种模式 - 打包工具提示顶层 await 问题
- 用 getter 函数包装单例访问
- 组件无法直接导入客户端
问题
异步构造函数无法导出:
// This doesn't work
export const client = await createClient(); // Top-level await breaks bundlers
因此,你只能采用 getter 模式:
let client: Client | null = null;
export async function getClient() {
if (!client) {
client = await createClient();
}
return client;
}
// Every consumer must await
const client = await getClient();
每个调用点都需要 await。你传递的是 Promise,而不是对象。
模式
让构造过程保持同步。将异步工作附加到对象上:
// client.ts
export const client = createClient();
// Sync access works immediately
client.save(data);
client.load(id);
// Await the async work when you need to
await client.whenSynced;
构造过程会立即返回。异步初始化(从磁盘加载、连接服务器)会在后台进行,并通过 whenSynced 跟踪。
UI 渲染门控
在 Svelte 中,在根节点使用 @epicenter/ui/spinner 显示加载状态,并使用 @epicenter/ui/empty 处理错误恢复,只进行一次门控:
<script>
import * as Empty from '@epicenter/ui/empty';
import { Spinner } from '@epicenter/ui/spinner';
import TriangleAlertIcon from '@lucide/svelte/icons/triangle-alert';
import { client } from '$lib/client';
</script>
{#await client.whenSynced}
<Empty.Root class="flex-1">
<Empty.Media>
<Spinner class="size-5 text-muted-foreground" />
</Empty.Media>
<Empty.Title>Loading…</Empty.Title>
</Empty.Root>
{:then}
{@render children?.()}
{:catch}
<Empty.Root class="flex-1">
<Empty.Media>
<TriangleAlertIcon class="size-8 text-muted-foreground" />
</Empty.Media>
<Empty.Title>Failed to load</Empty.Title>
<Empty.Description>
Something went wrong during initialization. Try reloading.
</Empty.Description>
</Empty.Root>
此门控可确保:在任何子组件的脚本运行时,异步工作都已完成。子组件可以直接同步访问,无需检查就绪状态。
务必包含 {:catch}:如果异步种子数据加载失败(例如,browser.windows.getAll 抛出异常),用户会看到可操作的错误提示,而不是无限期的加载动画。
实现
withCapabilities() 流式构建器会将异步工作附加到同步构造的对象上:
function createClient() {
const state = initializeSyncState();
return {
save(data) {
/* sync method */
},
load(id) {
/* sync method */
},
withCapabilities({ persistence }) {
const whenSynced = persistence(state);
return Object.assign(this, { whenSynced });
},
};
}
// Usage
export const client = createClient().withCapabilities({
persistence: (state) => loadFromIndexedDB(state),
});
前后对比
| 方面 | 异步构造 | 同步 + whenSynced |
|---|---|---|
| 模块导出 | 无法直接导出 | 导出对象 |
| 消费者代码 | 到处都要 await getX() | 直接导入,同步使用 |
| UI 集成 | Promise 处理很麻烦 | 单个 {#await} 门控 |
| 类型签名 | Promise<X> | 带有 .whenSynced 的 X |
真实示例:y-indexeddb
Yjs 生态系统到处都在使用此模式:
const provider = new IndexeddbPersistence('my-db', doc);
// Constructor returns immediately
provider.on('update', handleUpdate); // Sync access works
await provider.whenSynced; // Wait when you need to
它们绝不会阻塞构造过程。异步工作总是延后到一个可供等待的属性中。
替代模式:在每个方法中等待
或者,你可以完全跳过 whenReady 属性,并将初始化等待隐藏在每个方法中。典型示例是 idb:
const dbPromise = openDB('keyval-store', 1, { upgrade(db) { db.createObjectStore('keyval') } });
export async function get(key) { return (await dbPromise).get('keyval', key); }
export async function set(key, val) { return (await dbPromise).put('keyval', val, key); }
当客户端有依赖已初始化状态的同步方法时,使用 whenReady。当所有方法本来就是异步的(例如数据库访问)时,使用每个方法中等待的模式。有关更深入的比较,请参阅 idb 每个方法中等待的文章。
相关模式
参考资料
- 完整文章:附带图示的详细说明
- 综合指南:480 行的深入讲解,附带 idb 示例
- idb 每个方法中等待:纯异步 API 的对应模式