Files
CICD/docs/architecture/003_frontend_engineering_standards.md
SpecialX 57807def37
Some checks failed
CI / build-and-test (push) Failing after 3m50s
CI / deploy (push) Has been skipped
完整性更新
现在已经实现了大部分基础功能
2026-01-08 11:14:03 +08:00

20 KiB
Raw Blame History

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 + RSCServer-First降低 bundle、提升性能
  • 保证类型安全与可维护性Vertical Slice、数据访问边界清晰
  • 形成可执行的质量门禁lint/typecheck/build 与评审清单)

0.2 非目标

  • 不规定具体业务模块的需求细节(业务规则以 docs/design/* 与 PRD 为准)
  • 不引入与当前仓库技术栈不一致的新框架/库(新增依赖需明确收益与替代方案)

1. 接手流程Onboarding Checklist

1.1 先读什么(按顺序)

1.2 开发前对齐(必须)

1.3 环境变量与配置校验(必须)

  • 统一使用 @t3-oss/env-nextjsenv 入口读取环境变量,禁止在业务代码中散落 process.env.*
  • Schema 定义与校验入口: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/<feature>/*
  • src/app/* 是路由层,只负责:
    • 布局组合layout
    • 读取 searchParams / params
    • 调用模块的数据访问函数(data-access.ts
    • 组合模块组件渲染
  • 通用能力放在 src/shared/*
    • 通用 UIsrc/shared/components/ui/*
    • 通用工具:src/shared/lib/*
    • DB 与 schemasrc/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 Shellsrc/app/(dashboard)/*
  • 角色域:src/app/(dashboard)/teacher|student|admin/*
  • /dashboard 作为入口页(重定向/分发到具体角色 dashboard参考 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 异步组件与 SuspenseStreaming

  • 对于数据加载超过 300ms 的非核心 UI 区块(例如:仪表盘某张统计卡片/图表/第三方数据块):
    • 必须用 <Suspense fallback={<Skeleton />}> 包裹,以避免全页阻塞
  • 禁止在 page.tsx 顶层用多个串行 await 造成瀑布请求:
    • 多个独立请求必须使用 Promise.all
    • 或拆分为多个 async 子组件并行流式渲染(用 Suspense 分段展示)

3.3 动态渲染策略(避免 build 阶段查库)

当页面在渲染时会查询数据库或依赖 request-time 数据,且无法安全静态化时:

  • 在页面入口显式声明:
    • export const dynamic = "force-dynamic"
  • 该策略已用于教师端班级与作业相关页面,见相应 design 文档(例如教师班级模块更新记录)

4. 模块内文件结构(强制)

每个业务模块使用统一结构(可按复杂度增减,但命名必须一致):

src/modules/<feature>/
├── 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/<feature>/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/<feature>/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/<feature>/data-access.ts
  • 需要复用/去重的查询优先用 cache 包裹React cache
  • 查询函数返回“UI 直接可消费的 DTO”避免页面层再做复杂映射

8.2 写入actions.ts

  • 所有写操作必须通过 Server Actions
  • 每个 action
    • 校验输入Zod 或手写 guard
    • 执行 DB 写入
    • 必须 revalidatePath(以页面为单位)

8.3 Server Action 返回结构(统一反馈协议)

export type ActionState<T = void> = {
  success: boolean
  message?: string
  errors?: Record<string, string[]>
  data?: T
}

约束:

  • errors 必须对齐 zoderror.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 Statenuqs 优先)

  • 列表页筛选/分页/Tab/排序等“可分享状态”必须放 URL
  • 使用 nuqs 做类型安全的 query state 管理

8.6 Data Access 权限边界Security / IDOR 防护)

  • data-access.ts 不是纯 DTO 映射层,必须承担数据归属权校验
  • 允许两种合规方式(二选一,但模块内必须统一):
    • 方式 A强制传参:所有 data-access 函数显式接收 actoruserId/role并在查询条件中约束归属例如 teacherId
    • 方式 B函数内获取data-access 函数首行获取 session/user 并校验 role/归属,再执行查询
  • 禁止把权限校验放在 page.tsx 或 client 组件中作为唯一屏障

9. 数据完整性与 Seed 规则(禁止 Mock

项目默认不使用 Mock 数据。

当某功能缺失实际数据,开发者必须把数据补齐到数据库与种子数据中,而不是在前端临时模拟。

执行规范:

  • 若缺失的是“表结构/字段/关系”:
    • 修改 src/shared/db/schema.tssrc/shared/db/relations.ts(按既有模式)
    • 生成并提交 Drizzle migrationdrizzle/*.sql
  • 若缺失的是“可演示的业务数据”:
    • 更新 scripts/seed.ts,确保 npm run db:seed 可一键生成可用数据
  • 文档同步(必须):
    • 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/<feature>/components 内的复杂业务组件(例如:试卷编辑器、排课表、工作台):
    • 推荐在对应的 docs/design/00*_*.md 增加“用法示例 + 关键 props + 截图”
    • 若团队资源允许,可引入 Storybook 作为可视化组件目录(不作为硬性门禁)

13. Performance Essentials必须遵守

  • 图片:
    • 强制使用 next/image 替代 <img>SVG 或已明确无需优化的极小图标除外)
    • 头像等外部域名资源必须配置并明确缓存策略
  • 字体:
    • 强制使用 next/font 管理字体加载
    • 禁止在 CSS 中 @import 外部字体 URL避免 CLS 与阻塞渲染)
  • 依赖:
    • 禁止引入重型动画库作为默认方案;复杂动效需按需加载并解释收益
    • 大体积 Client 组件必须拆分与动态加载,并通过 Suspense 提供 skeleton fallback

14. 参考实现(从现有代码学习的路径)