mcpField
Z
ziflux-expert

ziflux-expert

github.com/neogenz开发178 次安装15 周前更新
代码仓库

关于 ziflux-expert

面向使用 Angular `resource()` API 与 SWR 缓存的开发者,提供 ziflux 专项指导。内容涵盖缓存生命周期、API、领域结构以及常见实现误区。

主要功能

  1. 01说明 `cachedResource` 和 `cachedMutation`
  2. 02配置 SWR 缓存默认值
  3. 03设计层级缓存键
  4. 04实现乐观更新与回滚
  5. 05排查常见缓存错误

适用场景

  1. 01按 API、Store 和组件文件组织 Angular 功能
  2. 02为 Angular 视图添加缓存资源加载
  3. 03通过缓存失效和回滚处理变更
  4. 04排查陈旧数据或注入上下文错误

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/neogenz/ziflux --skill ziflux-expert

安装后在 Claude Code 中输入/ziflux-expert确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
api-reference.md
patterns.md
review-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> — 任意请求进行期间为 true
  • isStale: Signal<boolean> — 后台重新获取期间显示过期数据时为 true
  • isInitialLoading: 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 — 乐观更新,并写入 DataCache
  • destroy(): 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()。

顶级反模式

  1. 在注入上下文之外使用 DataCache → NG0203。必须在类字段、构造函数或 runInInjectionContext 中使用。
  2. 在路由级服务中使用 DataCache → 导航时缓存会被销毁,导致 SWR 失效。始终使用 providedIn: 'root'。
  3. staleTime > expireTime → 构造函数会抛出错误。
  4. 认为 invalidate() 会删除数据 → 它只会将数据标记为过时。get() 仍会返回 { data, fresh: false }。
  5. 使用 invalidate([]) 完全清空缓存 → 不会执行任何操作。请使用 clear()。
  6. 使用 ref.value.set() → value 是只读的 Signal,不是 WritableSignal。请使用 ref.set() 或 ref.update()。
  7. await mutate() 后检查是否成功 → mutate() 永远不会拒绝。请检查 mutation.error() 信号,或捕获返回值(undefined 表示出错)。
  8. 组件直接注入 API 服务 → 破坏领域模式。组件 → Store → API。
  9. 使用 Subjects/Observables 管理状态 → 禁止。只能使用 Signals。
  10. 忘记 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 用法时阅读。

评论

支持 Markdown