启动 Next.js 项目
基础最好在第一天就打好:之后每个规则都必须强行施加到违反它的代码之上。如果有 frontend-starter 模板——请从复制模板开始: 模板中的步骤已经完成并经过验证,本文档仅作为决策解释和审计检查清单。本文档设定了步骤顺序和最小集。如何编写代码本身——请看 fsd-frontend 技能(如果项目中有);授权相关——frontend-auth-session;提交前检查——anti-slop-review。现成配置——references/configs.md,Next + FSD 的结构示例和代码——references/next-structure.md。
规则优先级。 用户的明确指令和项目代理文件(AGENTS.md、CLAUDE.md)优先于本文档。库版本会变化:复制配置前,请对照已安装版本的文档,并且在运行之前不要声称它能工作。
关于 Next.js 的事实——来自包中的文档,而非本文档或记忆。 从 16.2 开始,文档位于 node_modules/next/dist/docs/,项目的 AGENTS.md 会引用它。如果与本文档示例冲突,以已安装版本的文档为准。
标记:MUST — 不可违反;SHOULD — 默认同意,偏离需要理由。
1. 代码之前的决策
表格中的每个决策都要记录在 README(“决策”部分)或 ADR 中,并附上理由。未经记录理由而偏离默认值——不允许。
| 问题 | 默认值 | 何时改变 |
|---|---|---|
| Next.js 还是 SPA | 如果需要至少一项:SSR/SEO、BFF(密钥、HttpOnly-cookie)、路由处理器,则使用 Next.js | 内部登录后应用,无需 SEO 和 BFF → 使用 Vite + React Router:构建和部署更简单 |
| 路由器 | App Router | 仅已有项目中使用 Pages Router |
| Monorepo | 否,单个应用 | 第二个应用或包实际出现 → pnpm workspaces;Nx/Turborepo — 当构建或 CI 开始变慢时 |
| Multi-Zones、微前端 | 否 | 独立团队,独立发布 |
| 服务器数据 | 使用 Server Components 进行初始加载;使用 TanStack Query 进行交互式客户端数据和变更 | 纯客户端应用 → 仅用 TanStack Query |
| 文本与 i18n | 从第一天起就包含翻译模块(next-intl 或类似),即使只有一个语言环境,如果项目遵循 fsd-frontend | “无需 i18n”的明确决策记录在 README 中 |
| 包管理器 | pnpm,版本在 packageManager 中 | — |
| 部署 | 立即决定:Vercel、Node 服务器(output: "standalone")或 Docker | env、缓存、CI 都依赖于此 |
MUST: 不要“为将来”添加技术。Monorepo、第二个路由器、Storybook、状态管理器、MSW(作为“后端类型”)只有在有第一个实际消费者时才出现。
2. 步骤顺序
- 创建项目(
pnpm create next-app@latest):TypeScript、ESLint、Tailwind、App Router。固定packageManager、engines.node(Next 16 需要 Node 20.9+)和.nvmrc。 - 立即删除生成器模板: 起始页面、
public/中的 SVG、模板 README、演示样式。用references/configs.md中的骨架替换 README。留下“以后再用”的模板会一直存活到生产环境。不删除create-next-app的AGENTS.md和CLAUDE.md:<!-- BEGIN:nextjs-agent-rules -->和<!-- END:nextjs-agent-rules -->之间的块将代理指向已安装版本的文档,next dev会自动维护它。自己的项目规则写在这些标记之外。 - 严格 TypeScript(
references/configs.md的第四部分)和typecheck脚本(tsc --noEmit)。 - 在 Next 之上的 FSD 结构(下面的第三部分)。
- Lint 和格式化: ESLint flat config(来自
eslint-config-next)+ 类型感知规则 + 边界 lint + Prettier。Next 16 移除了next lint,并且在next build时不进行 lint——lint 作为独立脚本在 CI 中运行。 - Env 配置(第四部分)。
- 错误模型和 API 客户端 在
shared/api中(第五部分)。 _app层中的提供器: QueryClient、主题、i18n、全局服务(toast、confirm)作为shared中的端口 +_app中的实现。- UI 基础(第六部分)。
- 测试: Vitest + Testing Library + MSW,使用 Playwright 进行 e2e(第七部分)。
- Git hooks: husky + lint-staged(eslint --fix, prettier)+ commitlint。
- CI: 每次 push 和 PR 时运行 lint、typecheck、test、build(第八部分)。
- 默认安全(第九部分)。
- 填写 README,反映实际情况(第十部分),并完成 Done 定义(第十一部分)。
步骤 1–3 作为一个单独的“scaffold”提交:这样在历史中就能看出生成器做了什么,以及你做了什么。
3. 在 Next 路由之上的 FSD
根据 FSD 官方指南针对 App Router:Next 的服务文件夹 app/ 在项目根目录,FSD 层在 src/ 中,而 app 和 pages 层加上 _ 前缀,以避免与 Next 的服务文件夹混淆。
app/ Next 路由:仅限路由文件
├── layout.tsx 根布局 → 来自 src/_app 的 provider
├── (auth)/sign-in/page.tsx export { SignInPage as default } from "@/_pages/sign-in"
└── api/auth/login/route.ts 路由处理器 (BFF),薄层
proxy.ts Next 16+(之前是 middleware.ts),与 app/ 同级
src/
├── _app/ app 层:提供器、全局样式、服务实现
├── _pages/ pages 层
├── widgets/ (如果项目使用)
├── features/
├── entities/
└── shared/{ui,api,lib,config}
MUST:
app/中的文件要薄:从src/_pages重新导出页面、generateMetadata、解析params/searchParams(在 Next 15+ 中是Promise,需要await)并通过 props 传递。文件中没有逻辑。- 路由处理器和 Server Actions 要薄:解析并验证输入 → 调用
src中的函数 → 返回响应。每一个都是公共入口:自己检查授权,而不是依赖于proxy.ts。 - 不要创建
src/app和src/pages:Next 会在根目录和src中查找服务文件夹,同名文件夹很容易变成第二个路由器。 - 边界 lint 了解前缀:
src/_app、src/_pages/*模式,而不是src/app、src/pages/*(配置见fsd-frontend、references/conventions.md,第十一部分)。 "use client"尽可能放在树的下方。页面默认为服务器端,客户端组件是具有交互性的叶子节点。- 包含密钥的模块以
import "server-only"开头,包含window的模块以import "client-only"开头。
详细说明和代码:references/next-structure.md。
4. 环境配置
MUST:
- 变量在两个模块中读取:
shared/config/env.server.ts(import "server-only")和shared/config/env.client.ts。schema 使用zod,导入时进行验证:配置错误在启动时而不是在第一个请求时失败。在其余代码中禁止使用process.env(lint 规则)。 NEXT_PUBLIC_*在构建时 嵌入到 bundle 中,并且仅当直接访问process.env.NEXT_PUBLIC_X时:不使用解构和process.env[name]。无法为一个构建产物构建具有不同公共值的多个环境——此类值在服务器端读取并向下传递。- 密钥只能使用不带
NEXT_PUBLIC_前缀的变量。 .env.example包含每个变量,带有注释和安全值;.env*在.gitignore中,.env.example除外。CI 中的构建使用.env.example中的值。
5. 错误模型和 API 客户端
MUST:
- 错误在
shared/api边界处规范化为带有kind的AppError(模型见fsd-frontend、references/conventions.md,第一部分)。在shared/api之外,没有人解析Response、状态码或错误文本。 - 在
fetch之上构建一个request()(或生成的客户端,同样包装):传递signal,将网络错误转换为network,HTTP 状态转换为kind,对响应体进行 schema 验证。示例见references/next-structure.md。 - 错误类型由代码决定:状态码、
extensions.code(GraphQL 对错误也返回 200)、响应体中的code。如果后端只返回文本——在一个 mapper 中根据文本进行映射,并附上原因注释和测试。 - 后端响应在边界处映射为领域类型;DTO 不会流入 UI。
- 用户数据不会在用户之间缓存:对于依赖会话的响应,不使用
"use cache"/unstable_cache。 - 如果存在 schema(OpenAPI、GraphQL),则根据 schema 生成客户端。schema 或生成的代码位于存储库中:CI 不应访问实时后端。
SHOULD: 传输层之上的接口(AuthApi + factory)——仅当已经有第二个消费者时(fsd-frontend、references/state-and-data.md,2.1)。不要引入 type: "mock" 的配置值,它会抛出“未实现”。
6. UI 基础
- 设计令牌——
:root中的 CSS 变量(以及暗色主题),通过@theme连接到 Tailwind v4。组件中只使用令牌;原始 HEX 和任意值(text-[#3a3a3a])——作为一次性例外,并附上注释。 - 类必须存在。 从 shadcn/ui 和示例中复制的组件带有自己的令牌(
bg-card、text-muted-foreground)。要么引入这些令牌,要么替换这些类——否则组件会默默丢失样式。 shared/ui原语基于无头库(Radix、React Aria)以实现可访问性;变体使用cva,类拼接使用cn(clsx+tailwind-merge)。- 字体使用
next/font,图片使用next/image并指定尺寸,images.remotePatterns显式列出。 - Storybook——仅当 UI 套件被多个应用或团队重用时才引入,不要太早。
7. 测试
- Vitest + Testing Library + jsdom + MSW,配置
onUnhandledFrame: "error"(MSW 3;在 MSW 2 中为onUnhandledRequest)。配置见references/configs.md。 - Vitest 无法渲染
asyncServer Components。将逻辑提取到函数中并进行单元测试,页面本身通过 e2e 测试。 - Playwright 用于 3–10 个关键路径。在 CI 中,e2e 针对可预测的后端(mock 服务器、带有测试数据库的测试环境)运行,而不是针对通用环境,因为通用环境中测试会更改其他数据。
- 第一个测试在第零天编写:它验证测试基础设施正常工作,并作为样本。
8. CI
MUST: 每次推送到主分支和每个 PR:lint、typecheck、test、build。GitHub Actions 模板见 references/configs.md。对于公共存储库,Actions 免费;对于私有存储库,有月度分钟配额。
- CI 中的构建使用
.env.example,无需密钥。 - e2e 作为单独的工作,当存在隔离的后端时执行;在此之前,在 README 中明确标注为什么 CI 中没有 e2e。
- README 中的 CI 徽章与 workflow 同时出现。
9. 默认安全
poweredByHeader: false;通过next.config.ts中的headers()设置标头:X-Content-Type-Options: nosniff、Referrer-Policy: strict-origin-when-cross-origin、X-Frame-Options: DENY(或 CSP 中的frame-ancestors)。带 nonce 的 CSP 是单独的任务,但解决方案(“有/没有”以及原因)会立即记录。- Server Actions 和路由处理器是公共端点:使用 schema 验证输入,并在每个端点中检查权限。
dangerouslySetInnerHTML——仅与 sanitizer 一起使用,并附上原因注释。- 不记录个人数据和令牌。
- 授权——按照
frontend-auth-session。
10. README
骨架见 references/configs.md。一条规则:README 中只包含此提交中代码的真实情况。 每个部分在与描述代码相同的提交中更新。计划放在单独的“下一步”部分,并注明尚未完成。
11. 启动时的 Done 定义
- 生成器模板已删除;README 是实际填充的骨架。
- 已固定
packageManager、engines.node、.nvmrc。 - TS strict;
typecheck、lint、test、build在本地通过(已运行,非假设)。 - 带
_app/_pages前缀的 FSD 布局;边界 lint 能捕获向上导入。 - 环境变量通过带 schema 的两个模块定义;
process.env在其之外被 lint 禁止;.env.example完整。 -
shared/api中的AppError和request()已编写测试(网络状态、HTTP 状态、错误 schema)。 -
_app中的提供器;服务器上 QueryClient 不是每个用户共享的。 - 设计令牌作为 CSS 变量;所有使用的类都存在。
- 已配置 Vitest + MSW + Playwright,各有一个有效的测试。
- Git hooks 和 commitlint 正常工作。
- CI 为绿色。
- 安全标头和
poweredByHeader: false。 - 第 1 部分的决策已记录。
12. 当怀疑时
- 在“现在添加”和“需要时添加”之间——选择需要时添加。
- 在“手动配置”和“保留 create-next-app 生成的内容”之间——保留,直到有理由更改,并在更改时记录原因。
- 在“单个应用”和“monorepo”之间——选择单个应用。
- 在“凭记忆的配置”和“来自当前版本文档的配置”之间——选择来自文档的配置。