关于 frontend-architecture
面向 TypeScript 前端开发的框架感知型架构技能,适用于页面、组件、共享 UI 和数据层。它强调遵循项目约定、明确职责归属,并将应用接线与可复用 UI 分开。
主要功能
- 01优先检查项目约定
- 02梳理框架与应用边界
- 03分离控制器和 UI 基元
- 04组织类型化服务端状态查询
- 05采用无障碍交互模式
适用场景
- 01在陌生代码库中规划前端改动
- 02重构职责混杂的组件
- 03设计不依赖应用逻辑的共享 UI
- 04组织类型化 API 请求与缓存更新
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ we-are-singular/ skills --skill frontend-architecture安装后在 Claude Code 中输入/frontend-architecture确认已加载。
详细介绍
前端架构
TypeScript 前端开发时请使用此技能。它适配各种框架,但不依赖特定项目:可将相同职责映射到本地框架,应用于 React 应用、Astro islands、Next.js 页面、共享 UI 包、设计系统组件、类型化 API 客户端、表单密集型界面,以及面向浏览器的集成模块。
第一步
修改代码前:
- 先阅读仓库指南:
AGENTS.md、软件包 README、组件文档、本地测试设置和附近代码。 - 确定框架边界:静态页面、服务端渲染页面、React island、仅客户端组件、路由处理程序、共享 UI 包或生成的客户端包。
- 检查现有导入别名、命名方式、测试辅助工具、查询客户端设置、store 辅助工具、表单工具、分析约定和共享 UI 基础组件。
- 如果仓库约定明确,优先遵循。对于新代码、约定不明确的代码,或需要进行架构清理的代码,请将此技能作为默认做法。
- 创建新组件、hook、服务、store、查询键、架构、变体或辅助工具前,先进行搜索。合并相似代码,而不是再添加一个近似重复项。
- 先找到 DTO、架构、路由契约、生成的客户端和设计令牌的权威来源,再进行复制。
默认原则:让 UI 易于使用,明确数据归属,并确保共享组件不包含特定应用的行为。
核心原则
- 前端代码使用 TypeScript。优先采用严格的 props、显式的导出返回类型,以及稳定的领域类型,避免使用宽泛的对象类型。
- 优先基于本地原语构建:现有 UI 组件、
cn()辅助函数、变体辅助函数、表单封装、查询辅助函数和测试工具。 - 组件应小巧且专注。当组件承担多项职责、长度超过约 100 行,或某种模式出现在两个地方时,将其拆分。
- 优先使用组合,而不是逐层传递 props 或设计包含大量 props 的 API。对于可渲染区域和操作,使用
children、插槽、渲染 props 或复合组件。 - 不要通过不使用这些 props 的组件,将 props 逐层传递超过一层。将组合逻辑上移到调用方,或引入职责明确的 provider/store。
- 在合适的情况下,让组件通过既有 provider 或框架本地上下文读取或派生应用数据、路径、区域设置、身份验证/会话和其他应用上下文,而不是将每个值都逐层传给中间组件。props 应用于当前边界和最终的展示组件。
- 根据 UI 或领域职责命名:
ResourceTable、UserMenu、createResourceQueryOptions、resourceService、$activePanel。 - 避免使用
Manager、Helper、Util或Wrapper等含糊的名称,除非文件确实只是框架胶水代码。 - 优先使用清晰的名称,而非缩写。尽可能保持函数纯粹,并将转换逻辑与渲染和副作用分离。
- 当调用方需要根据预期失败采取不同分支时,使用结果类型。对于 React Query、tRPC、表单库和外部集成边界,如果本地技术栈采用常规的错误/异常处理流程,就遵循该流程。
- 将控制器代码与展示原语拆分。控制器可以了解路由、查询、分析和特定功能的回调;原语则应接收数据和回调。
- 对于复杂交互,优先采用无头 hook 搭配小型原语树,而不是使用混合 DOM、状态机和数据获取的大型组件。
- 清晰标记生成文件,除非仓库明确要求,否则不要手动编辑。
- 只有在确有复用时才建立本地抽象。除非它们能明确所有权或支持实际的软件包边界,否则不要添加辅助文件、barrel 文件或单一用途的间接层。
- 避免“意大利面式 DRY”:只有在集中逻辑能简化调用点和所有权时,才值得这么做。
- 不要臆造未来状态。如果未来的字段或行为尚未实现,就留下明确的 TODO,而不要编写推测性逻辑。
- 相较于个人偏好,应遵循仓库本地的格式化、lint、导入排序、无障碍和文件命名规范。
应用边界
编写代码前,先梳理各项职责。
页面、布局和路由
页面和路由文件应负责路由相关事务:
- 路由参数、搜索参数、重定向、状态码和元数据。
- 选择页面级数据依赖。
- 组合布局和功能组件。
- 只向子边界传递其所需的数据。
尽可能不要在路由文件中放置大量交互逻辑、缓存更新、表单和渲染分支。应将它们移至职责明确的功能组件、服务或钩子中。
对于 Astro 风格的应用,使用 Astro 处理文档结构、元数据、静态内容和顶层布局组合。使用 React islands 实现有状态或可交互的部分。从 Astro 向 React 传递少量可序列化的属性;不要将大型服务端对象直接传入 islands。
Astro 页面可以使用 Astro.locals 获取请求上下文,例如 locale、i18n、auth/session 和应用自有的路径构建器。表单和交互流程可以使用 React islands;应将服务端工作流放在 Astro Actions、路由处理程序或控制器中,而不是仅为了内部提交而创建公共 API。
对于 Next 风格的应用,将仅限服务端的代码放在服务端组件、加载器、路由处理程序或操作中。客户端组件应专注于交互和浏览器状态。不要让客户端组件意外导入仅限服务端的模块。
组件
组件应负责渲染和本地交互:
- 标记、无障碍、布局和视觉状态。
- 组件本地状态,例如打开/关闭、草稿输入、选中标签页、悬停、焦点和展开状态。
- 调用提供的回调或专用钩子。
- 展示加载、空、错误、禁用和等待状态。
除非组件明确是相关职责的边界,否则组件不应负责持久化的服务端状态契约、跨功能缓存策略、分析规范或供应商 SDK 配置。
建议采用以下拆分方式:
- 共享 UI 组件:结构、样式、无障碍、变体和稳定的 DOM 契约。
- 通用应用组件:可复用的应用外壳或跨功能组合。
- 功能组件:领域行为、查询钩子、变更操作、特定路由条件。
- 服务/钩子/存储:API 调用、查询选项、缓存变更、共享客户端状态、外部浏览器集成。
共享组件需要接入应用逻辑时,使用包装器/控制器组件。控制器可以获取数据、读取上下文、延迟加载依赖浏览器的子组件、将领域值映射为 UI 属性,并传递渲染插槽。不要将这些应用接线逻辑放进共享组件。
控制器 + 基元的结构:
export function ResourcePickerController(): React.JSX.Element {
const { data = [] } = useQuery(resourceListQueryOptions)
const create = useCreateResourceMutation()
return (
<ResourcePicker
resources={data}
onCreate={(name) => create.mutateAsync({ name })}
renderEmpty={() => <EmptyState title="No resources yet" />}
/>
)
}
export function ResourcePicker({
resources,
onCreate,
renderEmpty,
}: {
resources: ResourceSummary[]
onCreate: (name: string) => Promise<void>
renderEmpty: () => React.ReactNode
}): React.JSX.Element {
if (resources.length === 0) return <>{renderEmpty()}</>
return <ResourceList items={resources} onCreate={onCreate} />
}
对于可搜索选择器、命令菜单、自动补全、文件上传和可编辑文本:
- 优先使用成熟的交互基元或无头钩子来处理键盘行为、焦点、选择和 ARIA。
- 在钩子中处理筛选、定位和受控/非受控状态。
- 在具有明确插槽(如
Root、Input、Label、List和Item)的基元中处理 DOM 标记和样式。 - 在相关场景中支持 Escape、Enter、方向键、焦点、禁用状态、标签、描述和仅供屏幕阅读器读取的文本。
- 将仅限浏览器的测量和全局对象置于事件处理程序或客户端 effect 中。
共享 UI 包
当代码会在多个功能或应用中复用、封装了视觉或交互模式,且不依赖特定应用的数据模型、路由、分析或 API 客户端时,将其放入共享 UI 包中。
共享 UI 包中的组件应保持简单且易于组合:
- 使用函数式 React 组件。需要访问 DOM 或组合原语时,使用
forwardRef。 - 布尔属性使用
is*或has*命名,回调使用on*命名。 - 精简属性列表。如果组件需要许多用于渲染内容的属性,请改用组合。
- 使用语义化变体,例如
primary、secondary、danger、ghost、muted、sm、md和lg。 - 只有在使用者确实需要时才导出变体。否则只导出组件。
- 确保键盘行为、焦点管理、标签、ARIA 和禁用状态正确。
- 当使用者需要提供渲染元素且不丢失样式时,使用
asChild或插槽组合。 - 使用生成的 ID 来关联标签、描述和错误区域,而不是要求调用方到处传递 ID。
当可复用布局包含具名区域时,复合组件是很好的选择:
type PanelProps = {
children: React.ReactNode
className?: string
}
function PanelRoot({ children, className }: PanelProps): React.JSX.Element {
return <section className={cn("rounded-md border bg-card", className)}>{children}</section>
}
function PanelActions({ children, className }: PanelProps): React.JSX.Element {
return <div className={cn("flex items-center gap-2", className)}>{children}</div>
}
export const Panel = Object.assign(PanelRoot, { Actions: PanelActions })
如果使用子组件能让调用方更清晰,就不要向通用容器添加诸如 buttonLabel、actionSlotClassName 或 isSpecialFeatureMode 这样的一次性属性。
数据获取与服务器状态
将服务器状态视为服务器状态。除非客户端存储代表独立的客户端工作流,否则不要将服务器状态镜像到客户端存储中。
默认模式:
- 使用仓库的服务器状态库处理 API 数据,通常是 TanStack Query、tRPC 的 React Query 集成,或仓库自有的 SWR/nanoquery 封装。
- 按领域或功能在服务模块中集中管理查询键和查询选项。
- 保持请求函数精简且类型明确。它们应调用类型明确的 fetcher、生成的客户端、tRPC 过程或应用自有的 API 适配器。
- 组件使用查询选项或服务 Hook;不要在各处内联构造字符串键。
- 变更应执行服务器请求,并在明确的位置更新或使缓存失效。
- 后续步骤依赖操作完成时,使用
mutateAsync。对于无需等待结果的 UI 事件,使用mutate。
当许多功能直接调用 REST 端点时,创建共享 API fetcher。它应负责凭据、JSON 标头、204 响应处理、JSON 解析回退以及类型明确的 HTTP 错误。功能文件应调用类型明确的 createFetcher 或生成的客户端,而不是重复编写 fetch 机制。
类型明确的查询选项可将键、fetcher 和缓存策略集中在一起:
import { queryOptions } from "@tanstack/react-query"
export const resourceListQueryKey = ["resources", "list"] as const
export const resourceDetailQueryKey = (id: string) => ["resources", "detail", id] as const
export const resourceListQueryOptions = queryOptions({
queryKey: resourceListQueryKey,
queryFn: fetchResources,
staleTime: 60_000,
})
export const resourceDetailQueryOptions = (id: string | undefined) =>
queryOptions({
queryKey: resourceDetailQueryKey(id ?? ""),
queryFn: () => {
if (!id) throw new Error("Resource ID is required")
return fetchResource(id)
},
enabled: Boolean(id),
staleTime: 30_000,
})
对于 tRPC 风格的客户端,即使生成的 Hook 已提供查询键和缓存失效辅助函数,也要保持相同的职责划分。功能服务 Hook 可以归纳相关过程和缓存更新,但应避免演变成一个隐藏业务逻辑且混杂无关事项的层。
对于操作式 API,服务对象可以归纳缓存键、fetcher 存储、变更方法和小型工作流辅助函数。保持结构清晰一致:
export const resourceService = {
keys: {
list: () => ["resources"],
detail: (id: string | null) => ["resource", String(id)],
},
useList: () => useLiveQuery($resources, []),
async update(id: string, payload: UpdateResourceInput) {
const { data, error } = await actions.resource.update({ id, ...payload })
if (error) throw error
mutateCache(resourceService.keys.detail(id), data)
revalidateKeys(resourceService.keys.list())
return data
},
}
不要让这些服务变成无关内容的大杂烩。如果服务归纳了缓存操作、API 调用和派生选择器,它们就应全部归属于同一用户工作流或领域。
缓存更新:
- 当数据结构较宽泛或更新会影响多个视图时,优先在变更后使缓存失效。
- 对于即时 UI 修正、详情与列表同步以及小型且明确的更新,优先使用针对性的
setQueryData。 - 对于切换、重命名、重新排序、小型编辑以及感觉像本地操作的控件,使用乐观更新以提升 UX。
- 在乐观更新中,取消受影响的查询,捕获先前的值,更新所有可见的缓存条目,出错时回滚,并在操作结束时使缓存失效。
- 将分析、提示和导航放在 UI 或功能边界处的变更回调中,而不是放在底层 fetcher 中。
- 当变更会影响搜索、列表、详情、派生计数或外部缓存视图时,有意识地更新或使所有可见缓存失效。不要依赖一次详情缓存更新来修复所有界面。
预期内的请求失败应以类型明确的错误或结果结构呈现,以便 UI 显示。避免在可见 UI 中直接输出原始 JSON 或传输错误。
乐观变更结构:
const renameMutation = useMutation<Resource, Error, string, { previous?: ResourceList }>({
mutationFn: (name) => updateResourceName({ id, name }),
onMutate: async (name) => {
await queryClient.cancelQueries({ queryKey: resourceListQueryKey })
const previous = queryClient.getQueryData<ResourceList>(resourceListQueryKey)
queryClient.setQueryData<ResourceList>(resourceListQueryKey, (current) =>
current ? current.map((item) => (item.id === id ? { ...item, name } : item)) : current,
)
return { previous }
},
onError: (error, _name, context) => {
if (context?.previous) queryClient.setQueryData(resourceListQueryKey, context.previous)
toast.error(error.message)
},
onSuccess: (resource) => {
queryClient.setQueryData(resourceDetailQueryKey(resource.id), resource)
track("resource:update_name")
},
onSettled: () => {
void queryClient.invalidateQueries({ queryKey: resourceListQueryKey })
},
})
在应用边界设置查询客户端,并使用稳定实例,通常是 useState(() => new QueryClient())。在测试中,使用禁用重试的确定性查询客户端。
客户端状态
组件自身的状态问题使用本地 React 状态。只有在状态需要在相距较远的组件间共享、跨路由切换保留、协调模态框与编辑器面板,或适合使用明确的 Store 操作时,才使用客户端 Store。
Store 指南:
- 如果仓库采用此约定,请为 nanostores 加上
$前缀。 - 有意设计 Store API:如果逻辑并不简单,应公开
open、close、reset、select、setDraft或mutateCache等操作,而非原始 setter。 - 如果相关 Store 属于同一个 UI 工作流,请将它们归在同一文件中。
- 将服务端数据保留在服务端状态缓存中,除非 Store 封装本身就是仓库选用的服务端状态缓存。
- 如果在渲染期间根据动态输入创建 fetcher Store,请使用
useMemo将其记忆化,或在订阅前将其保存在useRef中。每次渲染都创建新的 Store 实例可能会导致重复订阅循环。 - 对于对 SSR 敏感的 Store,请支持初始值,并在挂载后再订阅,以避免水合不匹配。
- 成功获取数据后,清除过时的错误。
Store 适用于模态框服务、编辑器状态、当前标签页集合、选中的 ID、活动面板、本地草稿协调和应用级偏好设置。默认情况下,不要用它来隐藏 API 契约或重复存储查询数据。
对于类似编辑器的状态:
- 保留有类型定义的默认配置,并在所属路由/资源变化时重置为该配置。
- 跟踪
dirty标志,避免 Store 监听器写回刚加载的服务端状态并造成循环。 - 对高频编辑的后台保存进行防抖处理。
- 如果模块级监听器会触发 Store 副作用,请使用
typeof window !== "undefined"进行保护。 - 如果派生验证钩子依赖相同的建议/缓存状态,请将其放在 Store 附近。
脏编辑器 Store 的结构:
const $editor = map<EditorState>({
dirty: false,
config: structuredClone(DEFAULT_CONFIG),
})
const saveConfig = debounce((config: EditorConfig) => {
void actions.resourceConfig.update({ config }).then(() => previewStore.invalidate())
}, 100)
$editor.listen((state) => {
if (typeof window === "undefined") return
if (!state.dirty) return
saveConfig(state.config)
})
export const editorService = {
useState: () => useStore($editor),
reset: () => $editor.set({ dirty: false, config: structuredClone(DEFAULT_CONFIG) }),
updateConfig: (config: EditorConfig) => $editor.setKey("config", config),
markDirty: () => $editor.setKey("dirty", true),
}
对于需要持久化的 UX 状态,请使用简短且命名清晰的 Cookie、本地存储辅助函数,或封装在 saveDraft、getDraft 和 clearDraft 等服务方法之后的持久化原子。不要在各个组件中散落存储键。
表单与契约
表单应使用规范的 schema 或 DTO 进行边界校验。当存在共享 schema 或生成的契约时,不要手写与后端规则重复的前端校验器。如果 schema 不存在,且属于当前工作范围,应先在规范的契约层添加。
表单指南:
- 使用仓库的表单库和字段组件。通过标签、说明、错误信息和
aria-invalid确保字段的无障碍属性正确关联。 - 每个表单只采用一种提交流程。将持久化变更放在
onSubmit中。 - 在提交边界处修剪并规范化值,不要在各个输入框中零散处理。
- 如果某个字段在 UX 中是可选的,应将可选/空字符串行为组合一次并重复使用。
- 对于失焦时自动保存,应明确流程:字段失焦、校验、提交。
- 对于更改时自动保存,应使用表单库的防抖支持或稳定的防抖回调。
- 请求处理中应禁用提交,并为重要操作显示简洁的加载反馈。
- 按共享字段组件要求的格式,在字段附近显示错误。
- 对于服务端操作或表单提交,当应用重复使用相同的状态、清理、字段错误、常规错误和成功通知流程时,应使用可复用的操作处理程序。
- 对于服务端操作技术栈,应将服务端工作流放在操作/控制器中,而不是仅为内部表单另行创建公开 API 路由。
- 在 Astro 风格的应用中,表单可以使用 React 岛屿。如果周边应用已使用 Astro Actions,则通过 Astro Actions 提交。
- 在 Astro Actions 中,如果
catch行为相同,优先使用一个统括的try/catch。保留显式的操作错误,并通过应用共享的getErrorMessage风格工具转换意外错误。 - 对于文件输入和拖放区域,应在组件边界通过 schema 校验文件元数据,并提供
onBeforeValidation、onValidationError和onFileSelect回调。 - 即使使用自定义样式,也要确保表单控件具备无障碍性:标签、
aria-describedby、aria-invalid、键盘操作、隐藏的原生输入框,以及状态变化时所需的实时区域。
精简模式:
const optionalNameSchema = z.union([z.literal(""), ResourceSchema.shape.name])
export function ResourceForm({
defaultValues,
onSubmit,
}: {
defaultValues?: { name?: string }
onSubmit: (values: { name?: string }) => Promise<void>
}): React.JSX.Element {
const form = useForm({
defaultValues: { name: defaultValues?.name ?? "" },
onSubmit: async ({ value }) => {
await onSubmit({ name: value.name.trim() || undefined })
},
})
return (
<form
onSubmit={(event) => {
event.preventDefault()
void form.handleSubmit()
}}
>
<form.Field name="name" validators={{ onBlur: optionalNameSchema, onSubmit: optionalNameSchema }}>
{(field) => (
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)}
aria-invalid={field.state.meta.errors.length > 0}
/>
)}
</form.Field>
</form>
)
}
框架与集成模块
前端应用通常包含集成代码:分析、功能标记、身份验证会话客户端、路由重写、请求日志记录、仅限浏览器的 SDK,以及服务端渲染适配器。应将它们视为边界。
- 将第三方浏览器 SDK 封装在应用自有的服务、提供程序或 hook 后面。
- 在 React 组件中使用提供程序/hook。在 React 之外,仅当仓库已支持这种拆分时才使用单例服务。
- 在
onSuccess中跟踪用户发起的变更结果,在onError中跟踪失败。除非产品明确需要跟踪尝试的操作,否则不要在乐观的onMutate中跟踪。 - 默认情况下,只读查询无需分析事件。
- 使用一致的事件名称格式。优先采用
domain:action,其中操作名称使用 snake_case,例如project:create或settings:update_name。 - 仅在分析事件中传递相关且非敏感的上下文。
- 绝不要在分析上下文中包含 PII 或敏感载荷。
- 集成模块应保持幂等。开发服务器和 HMR 可能会重新执行模块。
- 对仅限浏览器的全局对象和仅限服务端的模块使用运行时保护。
- 将身份验证/会话保护放在路由或布局边界上。应明确呈现加载、重试/错误、重定向和已接受政策等状态,而不是在每个功能组件中零散添加身份验证检查。
- 对于 SSR 数据,在可能的情况下,将精确的初始数据传递给客户端查询。对于不影响首次绘制或元数据的纯客户端后续查询,应禁用 SSR。
- 如果第三方客户端库要求访问 hook,但不应渲染任何 DOM,可使用小型的“虚拟”组件,将服务端计算出的状态同步到该库。
- 从控制器组件延迟加载体积较大的仅限浏览器的组件,并提供最简加载状态。
配置驱动的 UI
对于图表、编辑器、仪表板、筛选器构建器或生成式表单等复杂 UI 领域,优先使用类型化配置注册表,而不是散落的条件判断。
良好的配置驱动 UI 包括:
- 一个中央注册表,将类型键映射到渲染器、表单编辑器、解析函数或运行时构建器。
- 一个类型化的默认配置对象,以确保创建行为稳定。
- 纯转换函数,用于将配置和数据转换为运行时对象。
- 针对转换函数、强制转换规则、默认值和无效配置行为编写有重点的测试。
- 为不受支持的组合提供逃逸机制,返回
null或类型化失败,而不是渲染出异常 UI。 - 仅在代码中记录了命名约定且通过行为测试验证时,才按该约定对生成的编辑器字段分组。
- 将默认值和强制转换规则集中管理。UI 字段不应各自定义对同一配置值的不同解释。
注册表 + 转换函数的结构:
const widgetConfig = {
table: [TableEditor, buildTableWidget],
chart: [ChartEditor, buildChartWidget],
metric: [MetricEditor, buildMetricWidget],
} as const
export type WidgetType = keyof typeof widgetConfig
export type WidgetConfigValue = { id: string; type: WidgetType; title?: string }
export function buildWidget(config: WidgetConfigValue, data: ResultData): Widget | null {
const entry = widgetConfig[config.type]
if (!entry) return null
const [, build] = entry
return build(config, data)
}
对于不变量、强制转换规则和不明显的执行顺序,使用注释说明。不要注释显而易见的 JSX 机制。
样式
使用仓库的样式系统和设计令牌。
- 优先使用
bg-card、text-foreground、border-input或本地等效项等语义化令牌,而不是任意颜色。 - 使用 Tailwind 工具类,并通过
cn()或仓库的合并辅助函数处理条件类名。 - 在 Astro 组件中使用
class:list或框架原生的等效功能。 - 使用
cva或已有的变体辅助工具处理尺寸、色调、密度和状态变体。 - 如果
cn()调用过长或包含大量条件判断,请提取变体定义或拆分为更小的组件。 - 除非运行时值确实无法用令牌或类表示,否则避免使用内联样式。
- 响应式行为采用移动优先,并与现有断点保持一致。
- 保留可见的焦点状态和键盘操作方式。
文件和命名
优先遵循仓库现有的命名规范。有歧义时,优先采用以下做法:
- 使用 ESM 导入;导入类型时使用仅类型导入。
- 在仓库支持的前端相关工具或服务端前端代码中,使用
node:协议导入 Node 内置模块。 - 导入顺序:React/框架和外部库优先,其次是共享 UI/设计系统包,再其次是应用/包别名,然后是相对导入,最后是类型导入。如果格式化工具或 lint 工具要求更严格的顺序,请遵循其规则。
- 文件名使用 kebab-case:
resource-card.tsx、settings-dialog.astro、query-service.ts。 - 在有帮助时,按文件类型添加后缀:
.store.ts、.service.ts、.provider.ts、.module.ts、.test.tsx、.route.ts。 - React 组件使用 PascalCase 导出,并放在 kebab-case 命名的文件中。
- 功能文件夹负责管理特定功能的组件、钩子、存储和测试。
- 共享 UI 包负责管理通用基础组件和组合式交互模式。
- 除非仓库已经依赖 barrel 文件,否则避免使用。直接导入有助于明确归属。
- 只有在多个应用、包或功能确实可以复用时,才将共享可复用基础组件放入共享包。
- 不要为单个简单函数创建辅助文件。在提取逻辑能让调用方更清晰或避免实际重复之前,应将简单的本地逻辑内联。
常见的前端目录结构包括 components/feature-name、components/common、services/domain、stores、hooks、layouts、pages、routes、utils 和 integrations。重点在于明确归属,而非固定的目录名称。
测试
优先编写关注行为的测试。不要测试类型系统已经保证的内容。
使用仓库现有的技术栈,通常是 Vitest、Testing Library、Playwright 和本地渲染辅助工具。 对于 i18n、路由、查询客户端和身份验证/会话等依赖较多上下文的场景,使用真实的提供方设置。尤其是 i18n,优先使用真实设置或默认语言,而不是在测试中重新实现翻译逻辑。
测试:
- 用户可见的渲染、交互、导航、加载、空状态和错误状态。
- 重要的条件分支和边界情况。
- 容易发生回归的查询/服务缓存行为。
- 乐观更新、回滚、失效和变更副作用。
- 表单:验证、规范化、禁用状态、提交流程和字段错误。
- 纯配置转换和生成式 UI 构建器。
- 菜单、对话框、标签页、组合框和复合控件中对无障碍至关重要的键盘行为。
避免:
- 大型快照。
- 测试实现细节,或在语义足以验证时测试精确的类名字符串。
- 假定存在 jest-dom 匹配器,除非包设置中已导入它们。
- 超过两层嵌套的
describe。 - 重新测试 TypeScript 已保证的静态类型、属性形状或不可能状态。
对于需要提供方的组件,创建精简的测试包装器,以提供查询客户端、路由器、存储、主题或表单上下文。在测试中使用确定性的查询客户端:不重试、缓存可控,并显式清理。
对于无头钩子和交互繁多的组件,编写小型测试载体组件,而不是模拟钩子内部实现。通过 userEvent 与 DOM 交互,然后断言可见输出、选中值、缓存效果或回调调用。
拿不准时
- 尽量让改动小而局部。
- 先复用现有的功能模式,再考虑创建新的抽象。
- 在大型文件中添加临时分支前,先提取聚焦的组件、钩子或服务。
- 保持共享 UI 的通用性,将产品行为放到功能组件中。
- 以契约和设计令牌为准。