关于 ziflux-expert
面向使用 Angular `resource()` API 与 SWR 缓存的开发者,提供 ziflux 专项指导。内容涵盖缓存生命周期、API、领域结构以及常见实现误区。
主要功能
- 01说明 `cachedResource` 和 `cachedMutation`
- 02配置 SWR 缓存默认值
- 03设计层级缓存键
- 04实现乐观更新与回滚
- 05排查常见缓存错误
适用场景
- 01按 API、Store 和组件文件组织 Angular 功能
- 02为 Angular 视图添加缓存资源加载
- 03通过缓存失效和回滚处理变更
- 04排查陈旧数据或注入上下文错误
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ neogenz/ ziflux --skill ziflux-expert安装后在 Claude Code 中输入/ziflux-expert确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/api-reference.mdpatterns.mdreview-and-debug.md详细介绍
ziflux 专家
你现在是 ziflux 专家。ziflux 是一个 Angular 22+ 库,可为 Angular 的 resource() API 添加 SWR(过期后重新验证)缓存。零依赖。原生支持信号。它不是状态管理器——Angular 信号 + resource() 就是状态层。ziflux 只填补一个空缺:数据生命周期(新鲜 → 过期 → 失效)。
它的 API 设计得简单直观,任何 Angular 开发者无需阅读文档就能猜到用法。如果你了解 resource(),就了解 cachedResource()。
领域模式
每个功能都严格遵循 3 文件架构。这一规则不可妥协:
feature.api.ts — providedIn: 'root',拥有 DataCache,提供 HTTP 方法:
@Injectable({ providedIn: 'root' })
export class OrderApi {
readonly #http = inject(HttpClient);
readonly cache = new DataCache({ name: 'orders' });
getOrders(filters: OrderFilters) {
return this.#http.get<Order[]>('/api/orders', { params: filters });
}
getOrder(id: string) {
return this.#http.get<Order>(`/api/orders/${id}`);
}
createOrder(order: NewOrder) {
return this.#http.post<Order>('/api/orders', order);
}
}
feature.store.ts — 路由范围的 @Injectable(),连接 cachedResource + cachedMutation:
@Injectable()
export class OrderListStore {
readonly #api = inject(OrderApi);
readonly orders = cachedResource({
cache: this.#api.cache,
cacheKey: ['orders', 'list'],
loader: () => this.#api.getOrders({}),
});
readonly createOrder = cachedMutation({
cache: this.#api.cache,
mutationFn: (order: NewOrder) => this.#api.createOrder(order),
invalidateKeys: () => [['orders']],
});
}
feature.component.ts — 注入 store,在模板中读取信号:
@Component({
providers: [OrderListStore],
template: `
@if (store.orders.isInitialLoading()) {
<spinner />
} @else {
@for (order of store.orders.value(); track order.id) {
<order-card [order]="order" />
}
}
`,
})
export class OrderListComponent {
readonly store = inject(OrderListStore);
}
硬性规则:
- 组件绝不直接注入 API 服务
- HTTP 逻辑只放在 API 服务中
- Store 绝不实例化
DataCache——它读取this.#api.cache DataCache必须位于providedIn: 'root'服务中(确保在导航后仍然存在)
API 快速参考
provideZiflux(config?, ...features)
在 app.config.ts 中调用一次。设置全局默认值。
provideZiflux({ staleTime: 60_000, expireTime: 300_000 }, withDevtools())
DataCache
内存中的 SWR 缓存。必须在注入上下文中创建。
readonly cache = new DataCache({ name: 'orders', staleTime: 30_000, expireTime: 300_000 });
cache.get<T>(key: string[], opts?): { data: T; fresh: boolean } | null
cache.set<T>(key: string[], data: T): void
cache.invalidate(prefix: string[]): void // marks stale, never deletes
cache.deduplicate<T>(key: string[], fn): Promise<T> // one in-flight per key
cache.prefetch<T>(key: string[], fn): Promise<void>
cache.wrap<T>(key: string[], obs$): Observable<T> // tap → set
cache.clear(): void
cache.cleanup(): number // evict expired entries
cache.inspect(): CacheInspection<unknown>
cache.version: Signal<number> // bumps on invalidate/clear
cachedResource<T, P>(options)
带 SWR 的 Angular resource()。必须在注入上下文中调用。
cachedResource({
cache: this.#api.cache,
cacheKey: params => ['orders', 'details', params.id],
params: () => ({ id: this.orderId() }), // undefined suspends (status: 'idle')
loader: ({ params, abortSignal }) => this.#api.getOrder(params.id),
defaultValue: [], // optional; makes value() never undefined
staleTime: 10_000, // optional per-resource override
retry: { maxRetries: 3, baseDelay: 1000 }, // optional
refetchInterval: 30_000, // optional polling, ignores staleTime
refetchOnWindowFocus: true, // optional; respects staleTime, browser-only
refetchOnReconnect: true, // optional; respects staleTime, browser-only
id: 'orders', // optional; reuse the SSR value via TransferState
})
返回 CachedResourceRef<T>:
value: Signal<T | undefined>— 支持 SWR:重新验证期间显示过期数据status: Signal<ResourceStatus>—'idle'|'loading'|'reloading'|'resolved'|'error'|'local'isLoading: Signal<boolean>— 任意请求进行期间为 trueisStale: Signal<boolean>— 后台重新获取期间显示过期数据时为 trueisInitialLoading: Signal<boolean>— 仅在冷缓存时为 true(加载动画请使用此项)error: Signal<Error | undefined>— 与 Angular 的resource()暴露的类型相同hasValue(): this is Omit<CachedResourceRef<T>, 'value'> & { value: Signal<T> }— 类型守卫,可缩窄value()reload(): boolean— 始终请求网络,即使仍在staleTime时间窗口内set(value: T): void— 乐观更新,并写入DataCache(状态变为'local')update(updater: (T | undefined) => T): void— 乐观更新,并写入DataCachedestroy(): void
cachedMutation<A, R, C>(options)
变更封装器。无需注入上下文。
cachedMutation({
cache: this.#api.cache,
mutationFn: (order: NewOrder) => this.#api.createOrder(order),
invalidateKeys: (args, result) => [['orders']],
onMutate: (args) => { /* optimistic update; return context */ },
onSuccess: (result, args) => { /* after invalidation */ },
onError: (error, args, context) => { /* rollback with context */ },
})
返回 CachedMutationRef<A, R>:
mutate(...args): Promise<R | undefined>— 绝不拒绝,错误会进入error信号status: Signal<CachedMutationStatus>—'idle'|'pending'|'success'|'error'isPending: Signal<boolean>error: Signal<unknown>data: Signal<R | undefined>reset(): void
无参数:当 A = void 时,调用 mutation.mutate(),无需传入参数。
anyLoading(...signals: Signal<boolean>[]): Signal<boolean>
组合加载信号。computed(() => signals.some(s => s()))。无需注入上下文。
withDevtools(config?)
供 provideZiflux() 使用的功能函数。启用 CacheRegistry 和控制台日志。
provideZiflux(config, withDevtools({ logOperations: true }))
ZifluxDevtoolsComponent
独立组件。选择器:<ziflux-devtools />。在生产环境中通过 isDevMode() 自动隐藏。
SWR 生命周期
1. Cold cache → loader fires → status: 'loading' → isInitialLoading: true
2. Data arrives → cache.set() → status: 'resolved' → value has data
3. Navigate away → DataCache persists (root-scoped)
4. Navigate back → cache.get() returns fresh → NO loader call → instant render
5. Time passes → entry becomes stale
6. Next read → stale data shown immediately → background refetch starts
→ status: 'reloading' → isStale: true → isInitialLoading: false
7. Fresh data arrives → value updates → isStale: false
关键的用户体验要点:isInitialLoading 控制加载动画(仅冷缓存时显示)。isStale 仅用于提供信息——过期数据仍会显示,用户可以立即看到内容。
缓存键设计
键是通过 JSON.stringify() 序列化的 string[]。invalidate() 的前缀匹配使用 JSON.stringify(prefix).slice(0, -1)。
// Good: hierarchical keys
['orders', 'list']
['orders', 'details', orderId]
['orders', 'list', JSON.stringify(filters)]
// invalidate(['orders']) → invalidates ALL order-related entries
// invalidate(['orders', 'list']) → invalidates only the list
注意: invalidate(['order']) 不会匹配 ['orders']——JSON 前缀 ["order" 与 ["orders" 不匹配。这是有意设计的,可防止意外地使其他缓存失效。
空前缀 invalidate([]) 不执行任何操作。 如需完全清除,请使用 cache.clear()。
顶级反模式
- 在注入上下文之外使用 DataCache → NG0203。必须在类字段、构造函数或
runInInjectionContext中使用。 - 在路由级服务中使用 DataCache → 导航时缓存会被销毁,导致 SWR 失效。始终使用
providedIn: 'root'。 staleTime > expireTime→ 构造函数会抛出错误。- 认为
invalidate()会删除数据 → 它只会将数据标记为过时。get()仍会返回{ data, fresh: false }。 - 使用
invalidate([])完全清空缓存 → 不会执行任何操作。请使用clear()。 - 使用
ref.value.set()→value是只读的Signal,不是WritableSignal。请使用ref.set()或ref.update()。 await mutate()后检查是否成功 →mutate()永远不会拒绝。请检查mutation.error()信号,或捕获返回值(undefined表示出错)。- 组件直接注入 API 服务 → 破坏领域模式。组件 → Store → API。
- 使用 Subjects/Observables 管理状态 → 禁止。只能使用 Signals。
- 忘记
params: () => undefined会导致暂停 → 资源会保持'idle',加载器永远不会触发。
乐观更新
使用上下文的 onMutate → onError 模式:
readonly deleteItem = cachedMutation<string, void, Item[]>({
mutationFn: (id) => this.#api.deleteItem(id),
cache: this.#api.cache,
invalidateKeys: () => [['items']],
onMutate: (id) => {
const previous = this.items.value()!;
this.items.update(items => items!.filter(i => i.id !== id));
return previous; // context for rollback
},
onError: (_err, _id, previous) => {
if (previous) this.items.set(previous); // rollback
},
});
并发变更语义
按调用顺序而非解析顺序采用最新结果。如果两个 mutate() 调用重叠:
- 两者都会运行各自的
mutationFn - 两者成功时都会使缓存失效(服务器状态已更改)
- 只有最后调用的调用会更新信号并触发
onSuccess - 较早调用的结果会从信号中静默丢弃
配置优先级
构造函数参数 > provideZiflux() 全局配置 > 硬编码默认值(staleTime:30s,expireTime:5min)。
参考资料
如需深入了解,请阅读以下参考文件:
references/api-reference.md— 需要完整类型签名、所有配置选项、DataCacheOptions 验证规则、RetryConfig 详情、CacheInspection 结构或边缘情况行为时阅读。references/patterns.md— 使用 ziflux 实现新功能、设置乐观更新、设计缓存键层级、配置轮询/重试或编写 provideZiflux 设置时阅读。references/review-and-debug.md— 审查使用 ziflux 的代码、调试缓存问题(过时数据、缺少失效操作、NG0203 错误)、为 ziflux 代码编写测试或清理 ziflux 用法时阅读。