Warming up the lab…
Warming up the lab…
Next.js 官方定义的顶层结构如下:
my-app/
├── app/ # App Router — 所有路由
├── public/ # 静态资源
├── src/ # 可选的源码目录
├── next.config.ts # 配置文件(TS 而非 .js)
├── instrumentation.ts # OpenTelemetry 监控
├── proxy.ts # 请求代理(Next.js 16 取代 middleware.ts)
└── .env* # 环境变量
Next.js 16 重要变化:middleware.ts 已被 proxy.ts 取代,运行在 Node.js runtime,用于明确网络边界。
结合官方约定与社区成熟实践,推荐如下结构:
my-app/
├── app/ ← 只放路由文件,作"薄编排层"
│ ├── (auth)/ ← Route Group:分组不影响 URL
│ │ ├── login/page.tsx
│ │ ├── register/page.tsx
│ │ └── layout.tsx
│ ├── (dashboard)/
│ │ ├── dashboard/page.tsx
│ │ └── layout.tsx
│ ├── api/ ← Route Handler(按需使用)
│ │ └── webhooks/stripe/route.ts
│ ├── layout.tsx ← 根布局
│ ├── page.tsx
│ ├── loading.tsx ← 全局加载
│ ├── error.tsx ← 错误边界
│ ├── global-error.tsx ← 根布局错误兜底
│ └── not-found.tsx ← 404
│
├── components/
│ ├── ui/ ← 原子组件(Button/Input/Modal)
│ └── features/ ← 领域组件(auth/billing/dashboard)
│
├── lib/ ← 应用核心(不可反向依赖 components/app)
│ ├── actions/ ← Server Actions("use server")
│ ├── db/queries/ ← 数据库查询(可复用)
│ ├── validations/ ← Zod schema
│ ├── auth/ ← 认证配置
│ └── utils/ ← 纯工具函数
│
├── hooks/ ← 客户端 hooks(隐含 "use client" 语义)
├── stores/ ← 客户端状态(Zustand/Jotai)
├── types/ ← 全局类型
├── agents/ ← AI 集成(prompts/tools/workflows)
└── proxy.ts ← Next.js 16 中间件
核心依赖规则:依赖图单向流动 app → components → lib,lib/ 不得导入 components/ 或 app/。hooks/ 与 stores/ 是显式的客户端目录,保持服务端/客户端边界在文件系统层面可见。
路由文件定位:/app 目录是神圣的,只放 page.tsx、layout.tsx、loading.tsx、error.tsx、route.ts、not-found.tsx,绝不放业务逻辑或可复用组件,路由文件只做薄编排。
Button.tsx、LoginForm.tsx)useAuth.ts、format-date.ts),团队内保持一致auth.ts、user.ts、billing.ts),文件顶部一次性声明 "use server" 即可标记全文件导出函数features/customer 而非 customers;customer-list 而非 customers-list(组件本身是一件事物)@/ 前缀(@/components/ui/button),create-next-app 默认配置,需在 tsconfig.json 的 paths 中声明这是 App Router 时代最重要的架构决策:
| 判定条件 | 组件类型 |
|---|---|
使用浏览器 API(window/document/localStorage) | 必须加 "use client" |
使用 React hooks(useState/useEffect) | 必须加 "use client" |
有事件处理(onClick/onChange) | 必须加 "use client" |
| 仅渲染静态标记、接收 props | 保持 Server Component(默认) |
| 需要访问数据库、文件系统、机密 | Server Component |
最佳实践:把 "use client" 边界尽量往叶子节点推。页面布局、Header、数据网格结构、静态内容全部留在服务端,只有筛选下拉、搜索框、图表 tooltip 等真正交互的部分才标记客户端组件。 | |
| 三大反模式: |
"use client" — 每个客户端组件及其导入都会进 JS bundle"use client" 壳包住整棵组件树 — 等于完全放弃了 RSC 的性能优势Server Actions 是官方推荐的变更(mutation)模式,替代大量 API Route。生产级标准模式:
// lib/actions/user.ts
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { db } from "@/lib/db/prisma";
import { getCurrentUser } from "@/lib/auth/session";
const UpdateProfileSchema = z.object({
name: z.string().min(2).max(100),
bio: z.string().max(500).optional(),
});
export type UpdateProfileState = {
errors?: { name?: string[]; bio?: string[] };
message?: string;
success?: boolean;
};
export async function updateProfile(
prevState: UpdateProfileState,
formData: FormData
): Promise<UpdateProfileState> {
const user = await getCurrentUser();
if (!user) redirect("/login");
const validated = UpdateProfileSchema.safeParse({
name: formData.get("name"),
bio: formData.get("bio"),
});
if (!validated.success) {
return {
errors: validated.error.flatten().fieldErrors,
message: "Validation failed.",
};
}
await db.user.update({ where: { id: user.id }, data: validated.data });
revalidatePath("/settings/profile");
return { success: true, message: "Profile updated." };
}此模式要点:Zod 在服务端校验先于任何数据库操作,错误状态通过 useActionState(React 19 替代 useFormState)流回表单,revalidatePath 使缓存失效避免读到旧数据。
Server Component 直接 async/await,不需要 useEffect 或 API Route:
// app/(dashboard)/dashboard/page.tsx
import { cache } from "react";
// React cache() 去重:多个组件调用同一请求只发一次
const getUser = cache(async (id: string) => {
return db.user.findUniqueOrThrow({ where: { id } });
});
export default async function DashboardPage() {
const [user, metrics] = await Promise.all([
getCurrentUser(),
getMetrics({ days: 30 }),
]);
// ...
}关键规则:
fetch 默认不再缓存(Next.js 15 起变更)use cache 显式缓存,或用 <Suspense> 包裹流式渲染
客户端状态 vs 服务端状态:Zustand/Jotai 只用于 UI 状态(弹窗开关、tab 切换),服务端数据留在 RSC 或用 React Query 做客户端轮询/实时订阅,两者混用会产生脏数据 bug。Next.js 16 引入 Cache Components,缓存从隐式转为完全显式 opt-in,experimental.ppr 标志已移除并演进为 use cache 编程模型:
// next.config.ts
const nextConfig: NextConfig = {
cacheComponents: true, // 启用 Cache Components
};// 数据级缓存 — 尽量贴近数据获取处,而非 page 级
import { cacheLife, cacheTag } from "next/cache";
async function getProducts() {
"use cache";
cacheLife("hours"); // 或 cacheLife({ stale: 60, revalidate: 300, expire: 3600 })
cacheTag("products"); // 域级标签,便于精确失效
return db.product.findMany();
}最佳实践:
use cache 放在数据获取函数或缓存型子组件,不要放整个 page<Suspense> 包裹posts、post:42),而非耦合到组件边界revalidateTag() 现在需要第二个 cacheLife 参数(推荐 'max')实现 stale-while-revalidate;updateTag() 用于 Server Actions 中读己之写语义;refresh() 只刷新未缓存数据Next.js 使用错误边界机制,须区分两种文件:
// app/error.tsx — 段级错误边界(必须是 Client Component)
"use client";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}// app/global-error.tsx — 根 layout/template 错误兜底
"use client";
export default function GlobalError({
error,
retry,
}: {
error: Error & { digest?: string };
retry: () => void;
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => retry()}>Try again</button>
</body>
</html>
);
}关键区别:
error.tsx 不捕获同段 layout.tsx 抛出的错误(因为边界嵌套在 layout 内)app/global-error.tsx,且需自带 <html>/<body> 标签global-error.tsx 不支持 metadata 导出,用 React <title> 组件替代
配套约定:loading.tsx 做段级 Suspense 骨架、not-found.tsx 处理 404、route.ts 只用于 webhook/外部 API/流式响应等场景。静态页面直接导出对象,动态内容用 generateMetadata:
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Your Page Title",
description: "Your page description",
robots: { index: true, follow: true },
};
// 动态路由
type Props = { params: Promise<{ id: string }> };
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const { id } = await params; // Next.js 15+ params 是 Promise
const product = await getProduct(id);
return { title: product.name };
}注意:Next.js 16 中 params、searchParams、cookies()、headers() 全部为异步,必须 await。generateMetadata 中的 canonical URL 需绝对地址,相对 URL 会失效。
NEXT_PUBLIC_ 前缀变量会打进客户端 bundle,只能放非敏感配置(分析 ID、公开 API URL)@t3-oss/env-nextjs + Zod 在启动时校验所有环境变量.env.local、.env.production 加入 .gitignore,提交 .env.example 作为文档生产级项目标准 tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"isolatedModules": true,
"jsx": "preserve",
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./src/*"] }
}
}关键决策:
moduleResolution: "bundler" 是 Turbopack 时代的正确值(不要用 node/node16)noUncheckedIndexedAccess: true 强制处理数组/对象访问可能返回 undefined 的情况,能抓住 strict: true 漏掉的一整类运行时错误Next.js 16 重要变化:next lint 命令已移除,next build 不再运行 lint,需直接使用 ESLint CLI 或 Biome;@next/eslint-plugin-next 默认改为 ESLint Flat Config 格式。
推荐工具链:
| 工具 | 职责 |
|---|---|
| Prettier | 格式化(消除分号/缩进之争) |
ESLint(含 eslint-config-next) | 代码质量规则 + React/Next 专项规则 |
| Husky | Git hooks 管理 |
| lint-staged | 只对暂存文件跑 linter |
| Commitlint | 强制 Conventional Commits 格式 |
| eslint.config.mjs(Flat Config): |
import { FlatCompat } from "@eslint/eslintrc";
const compat = new FlatCompat({ baseDirectory: __dirname });
export default [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
rules: {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "warn",
},
},
];Conventional Commits 格式(commitlint 默认配置):
feat: add user profile page # 新功能
fix: resolve hydration mismatch # 修复
docs: update README # 文档
refactor: extract useAuth hook # 重构
chore: bump dependencies # 杂务
.husky/pre-commit(配合 lint-staged):
{
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,css,md}": ["prettier --write"]
}.husky/commit-msg:
npx --no -- commitlint --edit $1@prisma/client@6.x):TypeScript 集成最佳、文档完善、开发效率优先lib/db/queries/types/ 目录,避免重复定义z.infer 派生类型保证单一来源satisfies 操作符约束对象字面量类型TypeScript 生态统一采用 TSDoc 标准(@microsoft/tsdoc),让不同工具能无歧义地解析文档注释:
/**
* 返回两个数的算术平均值。
*
* @remarks
* 本方法属于 {@link Statistics} 子系统。
*
* @param x - 第一个输入数
* @param y - 第二个输入数
* @returns x 与 y 的算术平均值
*
* @beta
*/
export function getAverage(x: number, y: number): number {
return (x + y) / 2.0;
}TypeScript 支持的核心 JSDoc/TSDoc 标签:
| 类别 | 标签 |
|---|---|
| 类型 | @type @param @returns @template @satisfies @import |
| 类 | @public @private @readonly @override @extends |
| 文档(TS/JS 通用) | @deprecated @see {@link} |
| 其他 | @enum @author |
/**
* 用户资料卡片组件
*
* Server Component:仅在服务端渲染,不发送任何 JS 到客户端。
* 通过 props 接收已序列化的用户数据,组件内部不做数据获取。
*
* @component
* @example
* ```tsx
* <UserProfileCard user={user} showEmail />
* ```
*/
interface UserProfileCardProps {
/** 用户对象(从 Server Component 传入,已剥离敏感字段) */
user: Pick<User, "id" | "name" | "avatarUrl">;
/** 是否展示邮箱,默认 false */
showEmail?: boolean;
}
export function UserProfileCard({ user, showEmail = false }: UserProfileCardProps) {
// ...
}@remarks 写长解释,@param 后跟短描述并用 - 分隔@deprecated 标记废弃 API 并说明替代方案,IDE 会显示删除线{@link} 建立关联,如 {@link getUserById},支持 IDE 跳转@type/@param),配合 checkJs 可获得接近 TS 的类型检查// TODO: / // FIXME: / // HACK: 带上下文)必须有注释结合 Next.js 官方 Production Checklist 与上述实践,上线前核查:
<Suspense> 流式 + PPRuse cache 显式声明,配合 cacheTag/cacheLife 精确控制next/image(images.qualities 默认改为 [75],minimumCacheTTL 默认 4 小时)next/font 自动优化error.tsx,根配 global-error.tsx,404 配 not-found.tsxinstrumentation.ts 集成 OpenTelemetry| 项目 | 要求 |
|---|---|
| Node.js | ≥ 20.9.0(LTS),18 已不支持 |
| TypeScript | ≥ 5.1.0 |
| 浏览器目标 | Chrome/Edge/Firefox 111+,Safari 16.4+ |
| 默认构建器 | Turbopack(可 next build --webpack 退回) |
| React | 19.2(View Transitions、useEffectEvent、<Activity/>) |
| 中间件 | proxy.ts(middleware.ts 已弃用) |
以上规范综合自 Next.js 官方文档(2026 年 8 月更新)与 2026 年主流社区生产实践。核心思想可以浓缩为一句话:App Router 时代,用文件系统表达架构边界(server/client、路由/组件/逻辑),用显式声明("use client"/"use server"/"use cache")表达执行意图,用类型系统与工具链守住质量底线。