本文档定义前端项目的开发规范与最佳实践,供开发与 AI 助手共同遵循。具体依赖与脚本以 package.json 为准。
| 类别 | 技术 |
|---|---|
| 包管理 | Bun |
| 框架 | React 19、TypeScript |
| 数据与请求 | @tanstack/react-query、axios、Zustand |
| 路由 | @tanstack/react-router |
| 表格与列表 | @tanstack/react-table、@tanstack/react-virtual |
| 国际化 | i18next、react-i18next、i18next-browser-languagedetector |
| 日期 | Day.js |
| UI 与样式 | Base UI、Hugeicons、Tailwind CSS、clsx / class-variance-authority |
| 表单 | React Hook Form、Zod |
| 图表 | @visactor/vchart、@visactor/react-vchart |
| 工具 | qrcode.react、oxfmt、oxlint、vitest(可选) |
优先复用项目已有组件与能力(见 3.3 组件);项目内无合适实现时,再评估已安装依赖及成熟、维护良好的开源库。仅在复用、组合或合理扩展仍无法满足需求时自行实现,并说明具体能力缺口。
-
页面文本:所有面向用户的文案均需支持 i18n,使用
useTranslation()的t()进行翻译。 -
使用场景
- React 组件:必须使用
const { t } = useTranslation(),以保证语言切换时组件会重新渲染。 - 非 React 环境(工具函数、常量、类方法):可使用
import { t } from 'i18next';此类用法不会随语言切换自动更新,仅在不依赖响应式更新的场景使用。 - 即使父组件已使用
useTranslation(),子组件仍应自行使用,以保证独立性。
- React 组件:必须使用
-
专有名词:品牌、产品、技术术语等可保留英文(如 API、React、TypeScript);若有约定俗成的译法则使用翻译。
-
翻译键:使用有层级、语义清晰的键名,如
dashboard.overview.title,并保持命名一致。 -
枚举与文案(常量中的 i18n) 各 feature 的
constants.ts中常出现「枚举/状态 + 展示文案」或「成功/错误消息」,须统一约定以免遗漏 i18n、用法混乱:- 成功/错误/提示类消息(如
SUCCESS_MESSAGES、ERROR_MESSAGES):常量值仅表示 i18n 键(与英文 fallback 同字面量)。展示时必须通过t()使用,例如toast.success(t(SUCCESS_MESSAGES.API_KEY_CREATED))、toast.error(t(ERROR_MESSAGES.UNEXPECTED)),禁止直接toast.success(SUCCESS_MESSAGES.xxx)当作最终文案。 - 状态/选项的 label:在常量中统一用 labelKey(字符串,即 i18n 键),组件中通过
t(config.labelKey)渲染;或约定用label存与 en 一致的 key 字符串,组件用t(config.label)。同一 feature 内只采用一种方式,避免混用。 - 新增此类常量时:同步在
src/i18n/static-keys.ts中登记对应 key(若项目用其做提取),或确保文案以t('...')字面量形式出现以便扫描,避免遗漏翻译。
- 成功/错误/提示类消息(如
数字格式化与 Intl 语言参数(强制)
- 普通数字、紧凑数字展示必须优先复用
@/lib/format的formatNumber/formatCompactNumber;金额、余额与额度使用已有@/lib/currency或对应业务封装,保留精度、单位与换算语义。已有封装无法表达的格式选项才可直接使用Intl.NumberFormat,不能为统一调用方式改变数值显示精度。 zhCN/zhTW是项目的界面语言码,不是合法的 BCP 47 标签。来自界面语言的参数,无论直接引用、解构、变量别名或函数透传,在进入Intl.*、toLocaleString/toLocaleDateString/toLocaleTimeString或公共格式化函数前,必须使用@/i18n/languages的toIntlLocale()转换。禁止自行编写zhCN等语言码映射;只处理简体中文会遗漏繁体中文。- React 组件通过
useTranslation()订阅语言,在渲染时计算const locale = toIntlLocale(i18n.resolvedLanguage || i18n.language),例如formatNumber(value, locale)。不得把当前语言固定在模块常量或初始 state 中;语言切换后格式必须更新。明确使用运行环境默认语言或固定协议语言的场景,可省略 locale、传undefined或使用合法的固定标签。 bun run lint/bun run lint:fix启用project/intl-localeerror,检查上述调用中的原始language/resolvedLanguage、本地变量赋值与解构别名,以及非法固定标签;不得通过 lint-disable 绕过。静态检查不追踪跨函数或跨模块的数据流,调用方仍须遵守转换约定。- 修改语言转换或数字格式化时,回归测试必须覆盖
zhCN、zhTW、其余五种界面语言,以及非法语言的降级;涉及组件语言响应时还须覆盖切换语言。修改 lint 规则须验证违规代码报错且合法转换不误报,类型检查和构建通过不能替代这些验证。
- 表达式:禁止 2 层及以上嵌套三元表达式;改用
if-else、提前返回或抽取函数。单层三元可保留,但需简洁。 - 可读性:控制函数圈复杂度,复杂逻辑拆成小函数;变量与函数命名需有意义,遵循驼峰等常规约定。
- TypeScript:避免
any,优先具体类型或unknown;为参数与返回值显式标注类型;仅类型用途的导入使用import type { X } from '...'。 - 类型检查:每次改动 TypeScript 或 TSX 代码后都要执行类型检查(如
bun run typecheck);若出现类型错误,须修复至无错误为止,不得遗留。 - Lint 检查:每次完成代码改动前,必须对所涉及文件执行 lint 检查,并修复这些文件中的所有 lint error;不得遗留 error。warning 可按变更范围与风险评估处理。
- 解构:对象非必要不要进行解构,特别是组件的 props;直接使用
props.xxx更清晰,避免不必要的解构增加代码复杂度。
先检索,再复用,最后才新增(强制)
- 开发前检索:新增或修改 UI 前,必须读取项目
shadcn-ui技能,使用rg --files/rg检索src/components/和相关src/features/;涉及通用交互时同时检查src/hooks/。按用途和行为查找,不能只查准备新增的组件名。阅读候选组件的实现、props 和实际调用示例后再确定方案。 - 复用顺序:优先使用满足场景的项目业务封装;能力不足时,先评估现有 props、插槽、组合方式或兼容扩展。业务封装确实不适用时,再用
src/components/ui/中已有基础组件组合。项目内仍有能力缺口时,才评估引入组件或新增实现;查外部 registry 不能替代项目内检索。 - 复用到行为层:仅使用
Button、Dialog、AlertDialog等基础组件,不代表已经复用了对应的复制、确认、弹窗布局等通用能力。已有业务封装能覆盖时,必须使用该封装,禁止在 feature 内重新拼装同一套交互、状态和样式。 - 新增条件:新增替代实现前,必须明确候选组件缺少的具体能力,并在变更说明或 PR 中记录候选路径及不能复用、组合或兼容扩展的原因。文案、图标、尺寸、颜色或所在页面不同,不构成重复实现的理由;优先通过已有 API 和主题约定处理这些差异。
- 合理组合:允许新增承载业务数据、权限、事件和内容的 feature 组件,但其中的通用 UI 与交互必须继续复用。扩展公共组件需保持已有调用行为,不能为单个页面塞入无关业务逻辑,也不能为消除少量相似代码建立过度通用的抽象。
- 基础元素边界:业务代码中,已有基础组件覆盖的按钮、输入框、选择器、弹层等控件,必须使用项目组件,不能手写同等控件或直接绕过封装调用底层库。普通语义化布局、隐藏字段及现有组件无法表达的特殊控件可使用原生元素;特殊控件仍须说明能力缺口并满足可访问性要求。
以下为常用检索入口,不能把本表当作完整组件清单:
| 场景 | 优先检查的项目入口 |
|---|---|
| 通用弹窗布局 | @/components/dialog |
| 删除、危险操作及普通确认 | @/components/confirm-dialog |
| 复制按钮与剪贴板交互 | @/components/copy-button、@/hooks/use-copy-to-clipboard |
| 空状态、加载状态、错误状态 | @/components/empty-state、@/components/loading-state、@/components/error-state |
| 表格、分页、工具栏及列表布局 | @/components/data-table,先读该目录的 README.md 和公开导出 |
| 按钮、输入、选择、提示等基础控件 | @/components/ui/,以 components.json 和本地实现为准 |
- 使用函数式组件与 Hooks,单一职责;组件 props 须有明确类型(接口或类型别名)。
- Props 使用:组件 props 非必要不要解构,直接使用
props.xxx访问属性,保持代码清晰(详见 3.2 代码风格与类型)。 - 单文件超过约 200 行时考虑拆分子组件或将逻辑抽到自定义 Hooks;类型定义可与组件同文件或放在同模块的
types中。
- React:合理使用
useMemo、useCallback减少无效重渲染;避免在渲染路径中创建新对象/数组;必要时使用React.memo。 - 代码分割:使用
React.lazy与动态import做按需加载,控制首屏与路由体积。 - 资源:图片选用合适格式与尺寸,大列表考虑虚拟滚动(如 @tanstack/react-virtual),大量图片考虑懒加载。
- 使用 Zustand 的
create定义 store,并为 state 与 actions 定义清晰类型。 - 组件内优先用选择器订阅,避免整 store 订阅导致多余渲染,例如:
const user = useAuthStore((s) => s.auth.user)。 - 需持久化的状态在 store 内读写 localStorage,并在初始化时恢复。
- Store 按功能放在
src/stores/,单文件职责清晰,命名表意明确。
- React Query:数据获取用
useQuery,变更用useMutation;为每个查询配置唯一queryKey(建议数组形式、层级一致);在onSuccess中对相关 query 做invalidateQueries,可配合乐观更新。服务端错误统一通过handleServerError处理(详见 3.9 错误处理)。 - Axios:使用项目统一的
api实例(含baseURL、headers、withCredentials: true);GET 默认请求去重,特殊请求可通过配置关闭;认证刷新与请求重试在拦截器中处理;普通错误提示由 Query/Mutation 的最终失败回调或直接调用方负责。
- 使用 React Hook Form + Zod:在功能模块的
lib/下定义 schema,并用z.infer导出表单类型;useForm配合@hookform/resolvers/zod做校验。 - 提交逻辑放在
onSubmit,展示加载与错误状态;成功后视场景重置表单或关闭弹窗。服务端校验错误映射到对应字段并展示(字段级错误展示方式见 3.9 错误处理)。
- 使用 TanStack Router,路由文件位于
src/routes/,通过createFileRoute定义;搜索参数用 Zod schema +validateSearch校验。 - 在
beforeLoad中做认证与重定向,避免不必要的请求;嵌套结构用布局路由与_authenticated等前缀,子路由通过<Outlet />渲染。 - 导航使用
useNavigate或Link,保持类型安全,避免直接操作window.location。
- 服务端错误:统一使用
handleServerError(error, fallbackMessage?),优先保留服务端具体原因。相同失败沿cause传播时只提示一次;不同操作即使文案相同也分别提示。禁止把完整 Axios 错误对象(可能包含凭据)直接写入控制台。 - 提示归属:普通请求拦截器不弹 Toast。使用共享
createAppQueryClient在 Query/Mutation 最终失败时提示;Mutation 自定义onError负责自己的提示或表单错误。自动重试期间和主动取消时不报错。meta.errorToast: false关闭自动提示,页面仍可主动调用错误处理函数。 - 业务失败:保留原始 API 响应契约;Query 使用
requireServerSuccess检查success: false,或在业务层用createServerError(response, fallbackMessage?)抛错并保留来源。直接请求须明确处理失败响应和 Promise 拒绝;需要展示失败状态的 Mutation 不得把失败结果当作保存成功。 - 认证兼容:保留稳定错误码映射及
AuthOperationError的安全消息,不解开其来源来展示被屏蔽的细节。维护既有刷新、跳转和一次性授权不重放约束。 - 展示:使用
toast.error等统一方式;路由级错误由errorComponent承接,提供友好错误页并记录便于排查的信息。 - 表单:校验与服务端错误映射到字段后,在字段下方展示;使用
form.setError等与表单库一致的方式。
- 以 Tailwind 工具类为主,动态类名用
cn()合并;非动态场景避免内联样式。 - 响应式采用移动优先与 Tailwind 断点(
sm:、md:、lg:等);主题与暗色用 CSS 变量与dark:,自定义样式集中在src/styles/,组件内尽量少写自定义 CSS。
- 功能模块:置于
src/features/<feature>/,内含components/、lib/、hooks/,以及按需的api.ts、types.ts、constants.ts、入口组件等。 - 通用:通用组件放
src/components/,通用工具与类型放src/lib/;组件文件 PascalCase,工具/类型文件 kebab-case 或types.ts,类型使用 PascalCase 命名并export type。
- 使用语义化 HTML(如
header、nav、main、footer),表单用label关联输入。 - 保证键盘可操作与焦点顺序合理;必要时使用 ARIA(如
aria-label、aria-expanded、aria-hidden);装饰性图标加aria-hidden="true",重要信息提供文本等价。 - 对比度满足 WCAG 2.1 AA(正文至少 4.5:1)。
- 认证与权限在路由与接口层校验;敏感操作增加二次确认等。
- 前后端均做数据校验(如 Zod),不信任仅前端校验;敏感信息不落前端存储,配置用环境变量,禁止硬编码密钥。
- 依赖 React 默认转义,慎用
dangerouslySetInnerHTML;跨域与 Cookie 使用withCredentials并按后端要求处理 CSRF。
- 工具函数与纯逻辑优先单元测试(Vitest),测试文件
*.test.ts;组件用 React Testing Library 测交互与行为,避免测实现细节。 - 新增功能、修复缺陷或修改现有行为时,必须同步新增或更新测试;Bug 修复必须先编写能够稳定复现问题的失败用例,再实现修复并确认用例转为通过。
- 修改前端组件的布局、尺寸、滚动定位、焦点管理、键盘操作、选中状态、禁用状态、加载状态、空状态、错误状态或响应式行为时,必须补充对应的回归测试,覆盖本次变更保护的用户可见行为,防止后续调整重新引入问题。
- 功能模块或组件模块的测试必须放在该模块专属的
__tests__/目录中,例如src/components/model-group-selector/__tests__/layout.test.ts;禁止将新增测试文件与正式代码文件平铺在同一目录。 - 测试文件按被测职责命名,例如
layout.test.ts、selection.test.ts、validation.test.ts;一个测试文件只覆盖一个明确模块或职责,避免形成跨模块的超大测试文件。 - 每个测试用例应只保护一个可描述的行为,名称必须包含触发条件和预期结果;优先使用 Arrange、Act、Assert 的清晰结构,避免在单个用例中混合多个无关断言。
- 测试必须覆盖主要成功路径以及本次变更涉及的关键边界和失败路径,包括空数据、单项和多项数据、超长文本、无效输入、禁用状态、异步失败与降级逻辑;不得为了数量机械枚举不相关输入。
- 布局测试应断言明确且稳定的行为契约,例如固定尺寸、排列方向、溢出策略、滚动目标和降级路径;不要仅断言组件能够渲染,也不要依赖浏览器像素误差、浏览器私有实现或脆弱的完整 class 字符串快照。
- 组件交互测试应从用户视角查询元素并执行点击、输入、键盘和焦点操作,断言可见结果、可访问状态或对外回调;禁止直接断言组件内部 state、私有函数调用次数或无用户意义的 DOM 层级。
- 涉及可访问性的组件必须覆盖可访问名称、键盘可操作性,以及
aria-expanded、aria-selected、aria-disabled、aria-invalid等与视觉状态一致的属性。 - 涉及 i18n 文案的测试优先通过稳定的角色、label 或翻译键语义定位元素,避免将某一种语言的完整展示文案作为与业务无关的脆弱断言;若翻译内容本身是契约,则应明确覆盖语言切换或 fallback 行为。
- 异步测试必须等待明确的界面状态或 Promise 结果,不得使用固定
sleep、依赖执行耗时或制造竞态;定时器、网络请求和浏览器 API 仅在必要边界进行可控 mock,并在每个用例后恢复。 - 优先测试真实代码路径;只有外部网络、时间、随机数、存储或浏览器 API 等不可控边界可以 mock。禁止 mock 被测模块自身,也不要通过复制生产逻辑到测试中计算期望结果。
- 测试数据应使用最小且具有业务含义的显式 fixture,测试内部必须独立初始化并清理全局状态、缓存、localStorage、mock 和定时器,确保用例可单独运行且不依赖执行顺序。
- 快照测试仅适用于稳定且人工可审查的结构输出;交互组件、复杂 DOM 和 Tailwind class 列表不得使用大范围快照代替行为断言。
- 关键流程补充集成与 E2E(如 MSW 模拟 API、Playwright/Cypress);核心功能目标覆盖率 80% 以上,关注业务路径与关键分支。
- 测试必须保护真实用户行为、稳定 API 契约或明确回归路径;禁止为了覆盖率添加 smoke、sleep/timing、随机输入、日志输出或只证明代码运行的测试。
- 新增或大幅重写测试时优先使用 Vitest 与 React Testing Library 的标准断言和查询方式,避免手写通用断言辅助函数;只有表达项目特定业务不变量时才抽取测试 helper。
- 清理测试时先合并重复场景、删除不明不白的实现细节断言;若旧测试间接覆盖了真实契约,需替换为更小、更直接的行为测试。
- 提交前必须至少运行受影响测试文件,并根据影响范围执行相关测试集、
bun run typecheck和涉及文件的 lint;不得在未看到最新通过结果的情况下声明测试完成。
- 使用 Bun:
bun install、bun add <pkg>、bun add -d <pkg>、bun remove <pkg>、bun pm ls、bun update等。 - 新增依赖前评估维护情况、体积与许可;生产与开发依赖区分清楚,版本用
^/~控制,定期更新以获取安全修复。
- 使用 Rsbuild,配置见
rsbuild.config.ts;脚本以package.json为准(如bun run dev、bun run build、bun run typecheck、bun run lint、bun run format),包管理见 3.15 依赖管理。 - 代码分割与懒加载策略见 3.4 性能;资源使用合适格式与压缩,环境变量用
.env且以VITE_前缀,不在代码中硬编码。 - 发布前:执行 typecheck、lint、format 检查,完成生产构建并检查产物体积与环境变量配置。
- 提交信息清晰、符合项目约定,描述变更内容与原因,中英文统一即可。
- 变更需经过代码审查,符合本文档规范,并关注质量、性能与安全。
- UI 变更完成前必须检查新增组件、基础组件导入及手写交互是否绕过已有业务封装;发现重复实现应在本次变更范围内改为复用。保留的新实现需在变更说明中说明能力缺口,typecheck、lint 和测试通过不能替代此项检查。
- 重大功能或规范变更时更新相关文档与
AGENTS.md。
- 2026-01-28:初始版本(国际化、代码、组件、类型等基础规范)。
- 2026-01-28:补充状态管理、API、表单、路由、错误处理、样式、文件组织、可访问性、安全、测试、依赖与构建部署规范。
- 2026-01-29:重组文档结构,合并重复内容,明确主次与交叉引用。
- 2026-01-31:在 3.2 中补充「类型检查」要求:改动 TS/TSX 后须执行 typecheck 并修复至无错。
- 2026-06-21:在 3.2 中补充「Lint 检查」要求:完成代码改动前须修复所涉及文件的所有 lint error。
- 2026-09-06:明确组件复用的强制检索流程、业务封装优先级、新增条件、常用入口及审查要求。