路由、特殊文件与渲染机制npm 命令对照命令实际执行用途npm run devnext dev开发模式热更新、详细报错npm run buildnext build先构建生产版本npm startnext start生产模式跑已经 build 好的代码使用场景日常写代码、调试→ 用npm run dev模拟线上环境→ 先npm run build再npm start路由与文件① layout vs pagelayout.tsx在路由切换时通常不重新挂载导航栏放这里就不会闪page.tsx会随 URL 换掉② 文件夹 ≈ URL文件夹路径对应 URL 路径例外(main)这种路由组——括号里的名字不出现在 URL 里③ Server Component默认作为 Server Component在服务端渲染 HTML如果页面只是静态展示标题、段落、链接没有useState、点击事件、浏览器 API → 保持 Server Component 即可④ 常见文件夹写法写法含义例子about/固定路由只匹配/about[slug]/动态路由匹配/posts/hello、/posts/abc等(main)/路由组不出现在 URL 里只用来组织 layout(main)/about的 URL 仍是/about等特殊文件① 为何用 notFound()不直接return PageNotFound /notFound()return PageNotFound /HTTP 状态生产环境通常返回 404开发环境也会渲染 not-found 页面但状态码行为可能不同200仍是「正常页面」语义「这个资源不存在」「这是一个普通页面碰巧长得像 404」UI 来源渲染最近的 not-found.tsx你自己 import 的组件多处复用page.tsx、Server Action、数据层都能调 notFound()不用每个文件 import每个地方都要 import returnSEO / 监控爬虫、日志能识别 404会被当成正常 200 页面核心404 是路由/资源层面的结论不是普通 JSX 的一种样式。关键点notFound() 在哪里触发执行会替换掉 xxx.tsx改渲染最近的 not-found.tsx和 throw Error 的区别notFound() → 业务上「找不到」→ not-found.tsxthrow Error → 程序出 bug → error.tsx② loading / error / not-found 放的位置不一样差别在于作用范围segment 树app/(main)/posts/ ├── loading.tsx ← 覆盖 posts 下所有子路由 ├── error.tsx ├── not-found.tsx ├── page.tsx ← 若将来有 /posts 列表页 └── [slug]/ ├── loading.tsx ← 只覆盖 /posts/:slug 详情 ├── error.tsx ├── not-found.tsx └── page.tsxNext 的规则特殊文件只影响「自己这一层 子层」不会向上影响父层。Next.js 渲染逻辑场景对照场景谁渲染渲染几次首次打开 / F5 刷新服务端先出 HTMLClient Component 再在浏览器 hydrate注水Server Component本次请求在服务端执行 1 次不在浏览器跑Client Component服务端预渲染 1 次 客户端 hydrate 1 次Link/ router.push 跳转不整页刷新向服务端要新页面的 RSC PayloadClient Component 局部更新新路由的 Server Component 在服务端再跑一遍router.refresh()URL 不变当前页 Server Component 重新在服务端跑再跑 1 次不是浏览器自己重算所以不是「服务端只渲染一次就完事」而是每次请求/导航/refreshServer Component 都会在服务端执行Client Component 则在服务端预渲染 客户端 hydrate/更新。和传统 React纯 CSR的区别以下以 React 18 的createRootAPI 为例旧版ReactDOM.render已废弃。纯 React SPA浏览器收到空 HTML → 下载 JS → ReactDOM.createRoot().render() → 整棵树在浏览器里从零构建你的 Next.js 项目App Router浏览器收到完整 HTMLServer Component 已填好内容 → 下载 JS → 只有带 use client 的组件做 hydrate挂事件、恢复状态 → Server Component 的 JS 不会发到浏览器执行「客户端渲染逻辑一致吗」—— 分两种组件① Server Component客户端根本不跑同样逻辑像posts/[slug]/page.tsx里的await params、2 秒延迟、notFound()// src/app/(main)/posts/[slug]/page.tsx async function page({ params }: Props) { const { slug } await params; // ... await new Promise((resolve) setTimeout(resolve, 2000)); return (/* ... */); }React 惯例组件名首字母大写如Page项目中此处写的是page。这些只在服务端执行。浏览器收到的只是 HTML 序列化的组件树RSC Payload不会在客户端再跑一遍这段 async 逻辑。首次访问时用户会先看到 loading.tsxSuspense 边界2 秒后服务端渲染完成再替换内容。② Client Component必须和服务端 HTML 一致HydrationClient Component 会跑两次服务端生成初始 HTML为了 SEO、首屏快客户端React 对比 DOM挂上交互hydrate两边输出的 HTML 必须一致否则报 hydration mismatch。你项目里 ThemeToggle 就是一个典型风险点// src/components/theme/index.tsx function getStoredTheme(): ThemeMode { if (typeof window undefined) { return light; // 服务端永远是 light } const cachedTheme localStorage.getItem(cachedThemeKey); return cachedTheme dark ? dark : light; // 客户端可能读到 dark } const [theme, setTheme] useStateThemeMode(getStoredTheme); // 按钮图标表示「点击后切换到的主题」不是当前页面的配色 // light → 显示 提示可切到 darkdark → 显示 提示可切到 light服务端getStoredTheme()无window默认light→ 渲染 客户端localStorage 存了dark→ 首次 render 为dark→ 渲染 两边图标不一致 → 控制台可能报 hydration warning