# Frontend Engineering Standards (Next_Edu) **Status**: ACTIVE **Owner**: Frontend Team **Scope**: Next.js App Router 前端工程规范(编码、目录、交互、样式、数据流、质量门禁) **Applies To**: `src/app/*`, `src/modules/*`, `src/shared/*`, `docs/design/*` --- ## 0. 目标与非目标 ### 0.1 目标 - 让新加入的前端工程师在 30 分钟内完成对齐并开始稳定迭代 - 保证 UI 一致性(Design Token + Shadcn/UI 复用) - 充分利用 App Router + RSC(Server-First)降低 bundle、提升性能 - 保证类型安全与可维护性(Vertical Slice、数据访问边界清晰) - 形成可执行的质量门禁(lint/typecheck/build 与评审清单) ### 0.2 非目标 - 不规定具体业务模块的需求细节(业务规则以 `docs/design/*` 与 PRD 为准) - 不引入与当前仓库技术栈不一致的新框架/库(新增依赖需明确收益与替代方案) --- ## 1. 接手流程(Onboarding Checklist) ### 1.1 先读什么(按顺序) - 设计系统与 UI 规范:[docs/design/design_system.md](file:///c:/Users/xiner/Desktop/CICD/docs/design/design_system.md) - 角色路由与目录规范:[docs/architecture/002_role_based_routing.md](file:///c:/Users/xiner/Desktop/CICD/docs/architecture/002_role_based_routing.md) - 项目架构总览:[ARCHITECTURE.md](file:///c:/Users/xiner/Desktop/CICD/ARCHITECTURE.md) - 你将要改动的模块实现文档:`docs/design/00*_*.md` ### 1.2 开发前对齐(必须) - 核对 Design Tokens 与暗色模式变量: - Tailwind 语义色映射:[tailwind.config.ts](file:///c:/Users/xiner/Desktop/CICD/tailwind.config.ts) - CSS 变量定义:[src/app/globals.css](file:///c:/Users/xiner/Desktop/CICD/src/app/globals.css) - 盘点可复用 UI 组件:`src/shared/components/ui/*` - 盘点通用工具(`cn` 等):[src/shared/lib/utils.ts](file:///c:/Users/xiner/Desktop/CICD/src/shared/lib/utils.ts) ### 1.3 环境变量与配置校验(必须) - 统一使用 `@t3-oss/env-nextjs` 的 `env` 入口读取环境变量,禁止在业务代码中散落 `process.env.*` - Schema 定义与校验入口:[src/env.mjs](file:///c:/Users/xiner/Desktop/CICD/src/env.mjs) - 任何新增环境变量: - 必须先在 `src/env.mjs` 增加 schema - 必须在 docs 中更新部署/运行说明(就近更新对应模块文档或全局架构文档) ### 1.3 本地跑通(推荐顺序) - 安装依赖:`npm install` - 启动开发:`npm run dev` - 质量检查: - `npm run lint` - `npm run typecheck` - `npm run build` --- ## 2. 核心工程原则(必须遵守) ### 2.1 Vertical Slice(按业务功能组织) - 业务必须放在 `src/modules//*` - `src/app/*` 是路由层,只负责: - 布局组合(layout) - 读取 `searchParams` / `params` - 调用模块的数据访问函数(`data-access.ts`) - 组合模块组件渲染 - 通用能力放在 `src/shared/*`: - 通用 UI:`src/shared/components/ui/*` - 通用工具:`src/shared/lib/*` - DB 与 schema:`src/shared/db/*` ### 2.2 Server-First(默认 Server Component) - 默认写 Server Component - 只有在需要以下能力时,才把“最小子组件”标记为 Client Component: - `useState/useEffect/useMemo`(与交互/浏览器相关) - DOM 事件(`onClick/onChange` 等) - `useRouter/usePathname` 等客户端导航 hooks - Radix/Portal 类组件需要客户端(Dialog/Dropdown 等通常在 client 内组合使用) ### 2.3 不重复造轮子(Shadcn/UI 优先) - 禁止手写 Modal/Dropdown/Tooltip 等基础交互容器 - 优先组合 `src/shared/components/ui/*`(Button/Card/Dialog/DropdownMenu/AlertDialog/Skeleton/EmptyState 等) - 若现有基础组件无法满足需求: 1. 优先通过 Composition 在业务模块里封装“业务组件” 2. 仅在存在 bug 或需要全局一致性调整时,才考虑改动 `src/shared/components/ui/*`(并在 PR 中明确影响面) ### 2.4 Client Component 引用边界(强制) - 禁止在 Client Component 中导入任何“服务端实现”代码(例如 DB 实例、data-access、server-only 模块) - Client Component 允许导入: - `src/shared/components/ui/*`(基础 UI) - `src/shared/lib/*`(纯前端工具函数) - Server Actions(`"use server"` 导出的 action 函数) - 类型定义必须使用 `import type`(避免把服务端依赖带入 client bundle) - 所有 `data-access.ts` 必须包含 `import "server-only"`,并将其视为强制安全边界(不是可选优化) --- ## 3. 目录与路由规范 ### 3.1 路由目录(App Router) - 认证域:`src/app/(auth)/*` - 控制台域(共享 App Shell):`src/app/(dashboard)/*` - 角色域:`src/app/(dashboard)/teacher|student|admin/*` - `/dashboard` 作为入口页(重定向/分发到具体角色 dashboard):参考 [002_role_based_routing.md](file:///c:/Users/xiner/Desktop/CICD/docs/architecture/002_role_based_routing.md) ### 3.2 页面文件职责 - `page.tsx`:页面组装(RSC),不承载复杂交互 - `loading.tsx`:路由级加载态(Skeleton) - `error.tsx`:路由级错误边界(友好 UI) - `not-found.tsx`:路由级 404 ### 3.3 错误处理与用户反馈(Error Handling & Feedback) - 路由级错误(404/500/未捕获异常): - 交由 `not-found.tsx` / `error.tsx` 处理 - 禁止在 `error.tsx` 里弹 Toast 堆栈刷屏,错误页输出必须友好且可恢复(例如提供 retry / 返回入口) - 业务操作反馈(表单提交/按钮操作/行级动作): - 统一由 Client Component 在调用 Server Action 后触发 `sonner` toast - 只在“成功”或“明确失败(业务/校验错误)”时触发 toast;未知异常由 action 归一为失败 message ### 3.4 异步组件与 Suspense(Streaming) - 对于数据加载超过 300ms 的非核心 UI 区块(例如:仪表盘某张统计卡片/图表/第三方数据块): - 必须用 `}>` 包裹,以避免全页阻塞 - 禁止在 `page.tsx` 顶层用多个串行 `await` 造成瀑布请求: - 多个独立请求必须使用 `Promise.all` - 或拆分为多个 async 子组件并行流式渲染(用 `Suspense` 分段展示) ### 3.3 动态渲染策略(避免 build 阶段查库) 当页面在渲染时会查询数据库或依赖 request-time 数据,且无法安全静态化时: - 在页面入口显式声明: - `export const dynamic = "force-dynamic"` - 该策略已用于教师端班级与作业相关页面,见相应 design 文档(例如教师班级模块更新记录) --- ## 4. 模块内文件结构(强制) 每个业务模块使用统一结构(可按复杂度增减,但命名必须一致): ``` src/modules// ├── components/ # 仅该模块使用的 UI 组件(可含 client 组件) ├── actions.ts # Server Actions(写入/变更 + revalidatePath) ├── data-access.ts # 数据查询与聚合(server-only + cache) ├── schema.ts # Zod schema(若需要) └── types.ts # 类型定义(与 DB/DTO 对齐) ``` 约束: - `actions.ts` 必须包含 `"use server"` - `data-access.ts` 必须包含 `import "server-only"`(防止误导入到 client bundle) - 复杂页面组件必须下沉到 `src/modules//components/*`,路由层只做组装 --- ## 5. Server / Client 边界与拆分策略 ### 5.1 最小化 Client Component 的落地方式 - 页面保持 RSC - 把需要交互的部分抽成独立 `components/*` 子组件并标记 `"use client"` - Client 组件向上暴露“数据变化事件”,由 Server Action 完成写入并 `revalidatePath` ### 5.4 Hydration 一致性(必须) - 所有 Client Component 的首屏渲染必须保证与 SSR 产出的 HTML 一致 - 禁止在 render 分支中使用: - `typeof window !== "undefined"` 之类的 server/client 分支 - `Date.now()` / `Math.random()` 等不稳定输入 - 依赖用户 locale 的时间格式化(除非服务端与客户端完全一致并带 snapshot) - 对于 Radix 等组件生成的动态 aria/id 导致的属性差异: - 优先通过组件封装确保首屏稳定 - 若确认差异不可避免且不影响交互,可在最小范围使用 `suppressHydrationWarning` ### 5.2 页面必须只做“拼装”,功能模块必须独立 - 任何功能模块都必须在 `src/modules//components/*` 内独立实现 - `page.tsx` 只负责: - 读取 `params/searchParams` - 调用 `data-access.ts` 获取数据 - 以组合方式拼装模块组件(不在 page 内实现具体交互与复杂 UI) - 行数不是拆分依据,只是“路由层变厚”的信号;一旦出现成块的功能 UI,应立即下沉到模块组件 ### 5.3 什么时候允许在 Client 中做“局部工作台” 当交互复杂到“页面需要类似 SPA 的局部体验”,允许将工作台容器作为 Client: - 典型场景:三栏工作台、拖拽排序编辑器、复杂筛选器组合、富交互表格 - 但仍要求: - 初始数据由 RSC 获取并传入 Client - 写操作通过 Server Actions - UI 状态尽量 URL 化(能分享/回溯) --- ## 6. 样式与 UI 一致性(Design System 强制项) ### 6.1 Token 优先(语义化颜色/圆角) - 颜色必须使用语义 token: - `bg-background`, `bg-card`, `bg-muted`, `text-foreground`, `text-muted-foreground`, `border-border` 等 - 禁止硬编码颜色值(`#fff`/`rgb()`)与随意引入灰度(如 `bg-gray-100`) - 圆角、边框、阴影遵循设计系统: - 常规组件使用 `rounded-md` 等语义半径(由 `--radius` 映射) ### 6.2 className 规范 - 所有条件样式必须使用 `className={cn(...)}` - `cn` 入口为 `@/shared/lib/utils` ### 6.3 禁止 Arbitrary Values(默认) - 默认禁止 `w-[123px]` 等任意值 - 只有在设计系统或现有实现明确允许、并且无法用 token/栅格解决时,才可使用,并在 PR 描述说明原因 ### 6.4 微交互与状态(必须有) - 按钮 hover:必须有 transition(现有 Button 组件已内置) - 列表项 hover:使用 `hover:bg-muted/50` 等轻量反馈 - Loading:必须使用 `Skeleton`(路由级 `loading.tsx` 或组件内 skeleton) - Empty:必须使用 `EmptyState` - Toast:统一使用 `sonner` --- ## 7. 图标规范(lucide-react) - 统一使用 `lucide-react` - 图标尺寸统一:默认 `h-4 w-4`,需要强调时 `h-5 w-5` - 颜色使用语义化:例如 `text-muted-foreground` --- ## 8. 数据流规范(查询、写入、状态) ### 8.1 查询(data-access.ts) - 所有查询放在 `src/modules//data-access.ts` - 需要复用/去重的查询优先用 `cache` 包裹(React cache) - 查询函数返回“UI 直接可消费的 DTO”,避免页面层再做复杂映射 ### 8.2 写入(actions.ts) - 所有写操作必须通过 Server Actions - 每个 action: - 校验输入(Zod 或手写 guard) - 执行 DB 写入 - 必须 `revalidatePath`(以页面为单位) ### 8.3 Server Action 返回结构(统一反馈协议) - 所有 Server Action 必须返回统一结构,用于前端统一处理 toast 与表单错误 - 统一使用类型:[src/shared/types/action-state.ts](file:///c:/Users/xiner/Desktop/CICD/src/shared/types/action-state.ts) ```ts export type ActionState = { success: boolean message?: string errors?: Record data?: T } ``` 约束: - `errors` 必须对齐 `zod` 的 `error.flatten().fieldErrors` 结构 - 禁止在各模块内重复定义自有的 ActionState 类型 ### 8.4 Toast 触发时机(强制) - Client Component 在调用 Server Action 后: - `success: true`:触发 `toast.success(message)`(或使用模块内约定的成功文案) - `success: false`: - 存在 `errors`:优先渲染表单字段错误;可选触发 `toast.error(message)` - 不存在 `errors`:触发 `toast.error(message || "Action failed")` - 对于路由级异常与边界错误,禁止用 toast 替代 `error.tsx` ### 8.5 URL State(nuqs 优先) - 列表页筛选/分页/Tab/排序等“可分享状态”必须放 URL - 使用 `nuqs` 做类型安全的 query state 管理 ### 8.6 Data Access 权限边界(Security / IDOR 防护) - `data-access.ts` 不是纯 DTO 映射层,必须承担数据归属权校验 - 允许两种合规方式(二选一,但模块内必须统一): - **方式 A(强制传参)**:所有 data-access 函数显式接收 `actor`(userId/role)并在查询条件中约束归属(例如 teacherId) - **方式 B(函数内获取)**:data-access 函数首行获取 session/user 并校验 role/归属,再执行查询 - 禁止把权限校验放在 page.tsx 或 client 组件中作为唯一屏障 --- ## 9. 数据完整性与 Seed 规则(禁止 Mock) 项目默认不使用 Mock 数据。 当某功能缺失实际数据,开发者必须把数据补齐到数据库与种子数据中,而不是在前端临时模拟。 执行规范: - 若缺失的是“表结构/字段/关系”: - 修改 `src/shared/db/schema.ts` 与 `src/shared/db/relations.ts`(按既有模式) - 生成并提交 Drizzle migration(`drizzle/*.sql`) - 若缺失的是“可演示的业务数据”: - 更新 `scripts/seed.ts`,确保 `npm run db:seed` 可一键生成可用数据 - 文档同步(必须): - 在 [schema-changelog.md](file:///c:/Users/xiner/Desktop/CICD/docs/db/schema-changelog.md) 记录本次新增/变更的数据表、字段、索引与外键 - 在对应模块的 `docs/design/00*_*.md` 中补充“新增了哪些数据/为什么需要/如何验证(db:seed + 页面路径)” ### 9.1 Seed 分层(降低阻塞) - Seed 分为两类: - **Baseline Seed**:全项目必备的最小集合(核心用户/角色/基础字典数据等),保证任何页面都不因“数据空”而无法进入流程 - **Scenario Seed(按模块)**:面向具体模块的可演示数据包(例如:班级/题库/试卷/作业),用于复现与验证该模块交互 - 任何模块新增数据依赖,必须以 “Scenario Seed” 的形式落到 `scripts/seed.ts`,而不是把数据要求隐含在前端逻辑里 ### 9.2 Seed 可复现与数据锚点(保证跨模块联动) - Seed 必须可重复执行(idempotent),避免开发环境多次执行后产生脏数据与重复数据 - 对跨模块联动依赖的关键实体,必须提供可稳定引用的数据锚点: - 固定标识(如固定 email/slug/title 组合)或可预测 ID(按现有 seed 约定) - 文档必须写明锚点是什么、依赖它的模块有哪些、如何验证 - 禁止在 UI 里依赖“随机生成数据顺序”来定位实体(例如 “取第一条记录作为 demo 用户” 这类逻辑应退化为明确锚点) ### 9.3 外部服务的例外(仅限 Adapter Mock) - 内部业务数据严格遵守“DB + Migration + Seed”,不允许 Mock - 仅当对接外部不可控服务(支付/短信/第三方 AI 流式等)且无法用本地 seed 复现时: - 允许在 `src/shared/lib/mock-adapters/*` 建立 mock 适配器 - 必须先定义 Adapter 接口,再提供真实实现与 mock 实现(业务模块只能依赖接口,不可直接依赖某个具体实现) - 该 mock 仅用于外部服务交互层,禁止承载内部业务数据 --- ## 10. 表单规范(react-hook-form + zod) - 表单统一使用 `react-hook-form` + `@hookform/resolvers` + `zod` - 错误提示放在输入框下方: - 字号 `text-xs` - 颜色 `text-destructive` - 破坏性操作必须二次确认(`AlertDialog`) - 提交中按钮禁用并展示 loading(可使用 `useFormStatus` 或本地 state) --- ## 11. 质量门禁与评审清单(PR 必须过) ### 11.1 本地必须通过 - `npm run lint` - `npm run typecheck` - `npm run build` ### 11.2 代码评审清单(Reviewer 逐项检查) - 目录结构是否符合 Vertical Slice(路由层是否保持“薄”) - 页面是否只做拼装(功能 UI 是否全部下沉到模块组件) - Server/Client 边界是否最小化(是否把整页误标 client) - 是否复用 `src/shared/components/ui/*`,是否重复实现基础交互 - 是否使用语义化 token(颜色/圆角/间距),是否引入硬编码颜色与大量 arbitrary values - Loading/Empty/Error 是否齐全(Skeleton/EmptyState/error.tsx) - 列表页筛选是否 URL 化(nuqs),是否支持刷新/分享 - 写操作是否通过 Server Action 且正确 `revalidatePath` - 是否避免 Mock(数据是否通过迁移 + seed 补齐,且 docs/db 与模块文档已同步) - 是否引入不必要的依赖与重型客户端逻辑 ### 11.3 Commit 规范(Git History) - 推荐遵循 Conventional Commits: - `feat:` 新功能 - `fix:` 修复 bug - `docs:` 文档更新 - `refactor:` 重构(无功能变化) - `chore:` 工程杂项 - 约束: - 单次提交必须聚焦一个意图,避免把大范围格式化与功能修改混在一起 - 涉及 DB 迁移与 seed 变更时,commit message 必须包含模块/领域关键词,便于追溯 --- ## 12. 文档同步规则(Docs Sync) 以下情况必须同步更新文档(就近放在 `docs/design/*` 或 `docs/architecture/*`): - 新增“全局交互模式”(例如:新的工作台/拖拽范式/跨模块复用交互) - 新增“全局组件”或改变基础 UI 行为(影响 `src/shared/components/ui/*`) - 新增关键路由结构或权限/角色策略 ### 12.1 业务组件可发现性(可选但推荐) - 对 `src/modules//components` 内的复杂业务组件(例如:试卷编辑器、排课表、工作台): - 推荐在对应的 `docs/design/00*_*.md` 增加“用法示例 + 关键 props + 截图” - 若团队资源允许,可引入 Storybook 作为可视化组件目录(不作为硬性门禁) --- ## 13. Performance Essentials(必须遵守) - 图片: - 强制使用 `next/image` 替代 ``(SVG 或已明确无需优化的极小图标除外) - 头像等外部域名资源必须配置并明确缓存策略 - 字体: - 强制使用 `next/font` 管理字体加载 - 禁止在 CSS 中 `@import` 外部字体 URL(避免 CLS 与阻塞渲染) - 依赖: - 禁止引入重型动画库作为默认方案;复杂动效需按需加载并解释收益 - 大体积 Client 组件必须拆分与动态加载,并通过 `Suspense` 提供 skeleton fallback --- ## 14. 参考实现(从现有代码学习的路径) - 设计系统与 UI 组件清单:[docs/design/design_system.md](file:///c:/Users/xiner/Desktop/CICD/docs/design/design_system.md) - Auth:路由层 RSC + 表单 client 拆分模式:[docs/design/001_auth_ui_implementation.md](file:///c:/Users/xiner/Desktop/CICD/docs/design/001_auth_ui_implementation.md) - 教师端班级模块:URL state + client 交互组件 + server actions 的组合:[docs/design/002_teacher_dashboard_implementation.md](file:///c:/Users/xiner/Desktop/CICD/docs/design/002_teacher_dashboard_implementation.md) - 教材工作台:RSC 拉初始数据 + client 工作台容器接管交互:[docs/design/003_textbooks_module_implementation.md](file:///c:/Users/xiner/Desktop/CICD/docs/design/003_textbooks_module_implementation.md) - 题库:nuqs 驱动筛选 + TanStack Table + CRUD actions:[docs/design/004_question_bank_implementation.md](file:///c:/Users/xiner/Desktop/CICD/docs/design/004_question_bank_implementation.md) - 考试组卷:拖拽编辑器(@dnd-kit)+ structure JSON 模型:[docs/design/005_exam_module_implementation.md](file:///c:/Users/xiner/Desktop/CICD/docs/design/005_exam_module_implementation.md) - 作业:冻结 exam → assignment 的域模型 + 学生作答/教师批改闭环:[docs/design/006_homework_module_implementation.md](file:///c:/Users/xiner/Desktop/CICD/docs/design/006_homework_module_implementation.md)