mcpField
S
sync-construction-async-property-ui-render-gate-pattern

sync-construction-async-property-ui-render-gate-pattern

github.com/epicenterhq开发4.8k52 次安装2 周前更新
代码仓库

关于 sync-construction-async-property-ui-render-gate-pattern

这项开发技能介绍同步构造客户端、异步执行初始化的模式,并涵盖可等待的就绪属性及带错误恢复的 Svelte 渲染门控。

主要功能

  1. 01同步构造客户端
  2. 02可等待的初始化属性
  3. 03Svelte 根级就绪门控
  4. 04加载与错误界面处理

适用场景

  1. 01无需顶层 await 导出客户端
  2. 02在 UI 组件间共享客户端
  3. 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 每个方法中等待的文章。

相关模式

参考资料

评论

支持 Markdown