mcpField
F
frontend-architecture

frontend-architecture

github.com/we-are-singular开发254 次安装3 周前更新
代码仓库

关于 frontend-architecture

面向 TypeScript 前端开发的框架感知型架构技能,适用于页面、组件、共享 UI 和数据层。它强调遵循项目约定、明确职责归属,并将应用接线与可复用 UI 分开。

主要功能

  1. 01优先检查项目约定
  2. 02梳理框架与应用边界
  3. 03分离控制器和 UI 基元
  4. 04组织类型化服务端状态查询
  5. 05采用无障碍交互模式

适用场景

  1. 01在陌生代码库中规划前端改动
  2. 02重构职责混杂的组件
  3. 03设计不依赖应用逻辑的共享 UI
  4. 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 的通用性,将产品行为放到功能组件中。
  • 以契约和设计令牌为准。

评论

支持 Markdown