关于 feature-systems-pattern
feature-systems-pattern 是用于在 TypeScript 项目中组织领域模块的开发技能,指导开发者构建 API、查询层、hooks、组件和公共导出,并规定依赖流向与缓存处理方式。
主要功能
- 01按领域组织系统目录
- 02类型化 API 适配器与错误
- 03TanStack Query 键与选项
- 04hooks、变更操作与乐观更新
- 05Context 与 XState 存储模式
- 06依赖流向与缓存范围规则
适用场景
- 01搭建新的领域模块
- 02添加查询与变更 hooks
- 03处理乐观缓存更新
- 04检查系统边界与导出
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ marcioaltoe/ skills --skill feature-systems-pattern安装后在 Claude Code 中输入/feature-systems-pattern确认已加载。
附带文件references/directory-layout.mdreferences/patterns.md
详细介绍
功能系统指南
“系统”是一个自包含、以领域为中心的模块,负责该领域相关的一切内容:API 调用、查询层、钩子、组件和公共 API。系统位于 systems/<domain>/ 目录下。
阅读 references/directory-layout.md,了解完整的目录结构和命名约定。
阅读 references/patterns.md,查看各层的注释实现模式。
快速参考
必须搭配的技能
使用此技能时一并启用这些技能——系统横跨多个技术领域:
| 情况 | 启用 |
|---|---|
| 任何钩子或组件 | react + tanstack-query |
| 数据获取/缓存 | tanstack-query |
| 变更 | tanstack-query |
| XState 存储 | xstate |
| 实用函数 | es-toolkit |
| 编写/修复测试 | testing-boss + vitest |
| Bug 修复 | systematic-debugging + no-workarounds |
系统目录一览
systems/<domain>/
├── index.ts # Public API barrel — required for every system
├── types.ts # TypeScript types for this domain
├── adapters/ # API service layer (HTTP calls, error types)
│ └── <domain>-api.ts
├── lib/ # Pure utilities, schemas, constants, query keys
│ ├── query-keys.ts # TanStack Query key factory
│ ├── query-options.ts # Reusable queryOptions / mutationOptions
│ ├── <domain>-schemas.ts
│ └── constants.ts
├── hooks/ # React hooks (queries, mutations, view-models)
│ ├── __tests__/
│ ├── use-<action>.ts # Query hooks
│ ├── use-create-<entity>.ts # Mutation hooks
│ ├── use-update-<entity>.ts
│ ├── use-delete-<entity>.ts
│ └── use-<domain>-view-model.ts
├── contexts/ # React contexts + providers
│ └── <domain>-context.tsx
├── stores/ # XState stores (complex async state machines)
│ └── <domain>-store.ts
├── components/ # React UI components
│ ├── stories/
│ └── index.ts
└── guards/ # Route guards / access checks
分步指南:创建新系统
步骤 1 — 定义 types.ts
- 导出清晰的领域类型;切勿暴露原始 API 响应结构。
- 有项目 API 合约类型时,优先基于这些类型派生。
- 使用 JSDoc 说明复杂的聚合类型,解释其派生规则和不变量。
步骤 2 — 构建 API 服务层
- 创建
adapters/<domain>-api.ts。 - 使用项目的 HTTP 客户端调用 API。
- 导出单个命名空间对象:
export const <domain>Api = { list, create, update, delete }。 - 导出带类型的错误类:
export class <Domain>ApiError extends Error { ... }。 - 每个函数都接受
signal?: AbortSignal,以支持查询取消。 - 将所有内部辅助函数(错误提取、响应规范化)设为模块私有。
步骤 3 — 添加 lib/query-keys.ts
export const <domain>Keys = {
all: ["<domain>"] as const,
lists: () => [...<domain>Keys.all, "list"] as const,
list: (scopeId: string | null) => [...<domain>Keys.lists(), scopeId] as const,
details: () => [...<domain>Keys.all, "detail"] as const,
detail: (id: string) => [...<domain>Keys.details(), id] as const,
};
- 使用分层键结构,以便精细失效。
- 使用任何能正确隔离缓存的标识符(userId、orgId 等)限定键的作用域。
- 对每个键元组都使用
as const。
步骤 4 — 添加 lib/query-options.ts
import { queryOptions } from "@tanstack/react-query";
import { <domain>Api } from "../adapters/<domain>-api";
import { <domain>Keys } from "./query-keys";
export function <domain>ListOptions(scopeId: string | null) {
return queryOptions({
queryKey: <domain>Keys.list(scopeId),
queryFn: ({ signal }) => <domain>Api.list(scopeId!, signal),
staleTime: 60_000,
enabled: Boolean(scopeId),
});
}
export function <domain>DetailOptions(id: string) {
return queryOptions({
queryKey: <domain>Keys.detail(id),
queryFn: ({ signal }) => <domain>Api.get(id, signal),
enabled: Boolean(id),
});
}
- 使用
queryOptions将queryKey和queryFn放在一起,确保类型安全并支持复用。 - 导出每个选项工厂,供钩子、预取和路由加载器使用。
- 始终将查询上下文中的
signal传递给 API 层。
步骤 5 — 编写钩子
- 查询钩子:使用
queryOptions工厂封装useQuery;接受一个作用域 ID 和可选的{ enabled? }。 - 变更钩子:使用
useMutation,并通过适当的onMutate/onError/onSettled回调实现乐观更新。 - 视图模型钩子:组合多个钩子,供页面/外壳组件使用;返回扁平对象。
- 将测试放在
hooks/__tests__/中,或与use-xxx.test.tsx一同存放。
阅读 references/patterns.md,了解完整的变更和乐观更新模式。
步骤 6 —(可选)添加上下文
当查询数据或组合状态需要在组件子树中共享,且不想逐层传递属性时,创建 contexts/<domain>-context.tsx。
// Always nullable context — consumer hook throws if used outside provider
export const <Domain>Context = createContext<<Domain>ContextValue | null>(null);
- 在同一文件中导出上下文、提供程序组件,以及重新导出的使用者钩子。
- 对于性能敏感的组件树,可拆分为 Core / UI / Operations 子上下文。
步骤 7 —(可选)添加 XState 存储
对于复杂的异步状态机(多步骤流程、轮询、事件发射),创建 stores/<domain>-store.ts。
export const <domain>Store = createStore({
context: { ... } as <Domain>Context,
emits: { ... },
on: {
someEvent: (context, event, enqueue) => {
enqueue.effect(async () => { ... });
return { ...context, isLoading: true };
},
},
});
步骤 8 — 配置 index.ts
使用带标签的分区和显式命名导出组织 barrel 文件:
// Types
export type { <Domain>Type } from "./types";
// Hooks
export { use<Domain>List, use<Domain>Detail } from "./hooks";
export { useCreate<Domain>, useUpdate<Domain>, useDelete<Domain> } from "./hooks";
// Components
export { <Domain>Component } from "./components";
// Utilities
export { <domain>HelperFn } from "./lib/<domain>-utils";
// Query Keys & Options
export { <domain>Keys } from "./lib/query-keys";
export { <domain>ListOptions, <domain>DetailOptions } from "./lib/query-options";
// API
export { <domain>Api, <Domain>ApiError } from "./adapters/<domain>-api";
关键规则
- 使用
queryOptions实现共置。 在可复用的选项工厂中共置queryKey和queryFn。切勿在多个文件中分散使用同一个查询键。 - 依赖关系单向流动。
adapters -> lib -> hooks -> components。Adapters 绝不能导入 hooks 或 components。 - 限定查询键的作用域。 任何依赖身份验证作用域(用户、组织、租户)的查询,都必须在其键中包含该作用域 ID,以防止跨作用域的陈旧数据。
- 在 API 层使用类型化错误。 绝不能从 adapters 抛出原始错误。使用类型化错误类,以便使用方无需检查错误消息字符串即可区分错误类型。
- 传递
AbortSignal。 将queryFn上下文中的signal传递给每个 API 调用,以正确取消查询。 - 变更后始终使查询失效。 在
onSettled中使用queryClient.invalidateQueries,确保最终与服务器保持一致。 - 乐观更新必须支持回滚。 使用基于缓存的乐观更新时,在
onMutate中保存之前的数据,并在onError中恢复。 - 乐观更新前取消正在进行的查询。 在
onMutate中调用queryClient.cancelQueries,防止重新获取的数据覆盖乐观状态。 - 将 Zod schemas 放在 lib/ 中。 将所有 Zod schemas 放在
lib/<domain>-schemas.ts中,以便在 API 边界进行运行时验证。
错误处理
- API 层抛出类型化错误:TanStack Query 会捕获该错误,并通过
query.error暴露。 - 带乐观更新的变更失败:
onError回调会将缓存回滚到onMutate保存的快照,然后onSettled使查询失效,以重新获取最新数据。 - 跨作用域的陈旧数据:将作用域 ID 添加到查询键中,并确认
enabled守卫会检查Boolean(scopeId)。 - 卸载时取消查询:组件卸载时,TanStack Query 会通过
signal自动取消正在进行的查询——确保将signal传递到 API 层。
详细参考
references/directory-layout.md— 完整的目录结构、文件命名和 barrel 约定references/patterns.md— API 层、查询选项、hooks、变更、乐观更新、上下文和 stores 的注释代码模式
评论
支持 Markdown