Next.js - 01 - 路由、特殊文件与渲染机制
2026/7/9 9:15:13 网站建设 项目流程

路由、特殊文件与渲染机制

npm 命令对照

命令实际执行用途
npm run devnext dev开发模式:热更新、详细报错
npm run buildnext build先构建生产版本
npm startnext start生产模式:跑已经 build 好的代码

使用场景

  • 日常写代码、调试→ 用npm run dev
  • 模拟线上环境→ 先npm run build,再npm start

路由与文件

① layout vs page

  • layout.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 + return
SEO / 监控爬虫、日志能识别 404会被当成正常 200 页面

核心:404 是路由/资源层面的结论,不是普通 JSX 的一种样式。

关键点:notFound() 在哪里触发执行,会替换掉 xxx.tsx,改渲染最近的 not-found.tsx

和 throw Error 的区别:

  • notFound() → 业务上「找不到」→ not-found.tsx
  • throw 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.tsx

Next 的规则:特殊文件只影响「自己这一层 + 子层」,不会向上影响父层。

Next.js 渲染逻辑

场景对照

场景谁渲染渲染几次
首次打开 / F5 刷新服务端先出 HTML,Client Component 再在浏览器 hydrate(注水)Server Component:本次请求在服务端执行 1 次(不在浏览器跑);Client Component:服务端预渲染 1 次 + 客户端 hydrate 1 次
<Link>/ router.push 跳转不整页刷新;向服务端要新页面的 RSC Payload,Client Component 局部更新新路由的 Server Component 在服务端再跑一遍
router.refresh()URL 不变,当前页 Server Component 重新在服务端跑再跑 1 次(不是浏览器自己重算)

所以:不是「服务端只渲染一次就完事」,而是每次请求/导航/refresh,Server Component 都会在服务端执行;Client Component 则在服务端预渲染 + 客户端 hydrate/更新。

和传统 React(纯 CSR)的区别

以下以 React 18+ 的createRootAPI 为例(旧版ReactDOM.render已废弃)。

纯 React SPA:

浏览器收到空 HTML → 下载 JS → ReactDOM.createRoot().render() → 整棵树在浏览器里从零构建

你的 Next.js 项目(App Router):

浏览器收到完整 HTML(Server 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.tsx(Suspense 边界),2 秒后服务端渲染完成再替换内容。

② Client Component:必须和服务端 HTML 一致(Hydration)

Client 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] = useState<ThemeMode>(getStoredTheme); // 按钮图标表示「点击后切换到的主题」,不是当前页面的配色: // light → 显示 🌙(提示可切到 dark);dark → 显示 🌞(提示可切到 light)
  • 服务端:getStoredTheme()window,默认light→ 渲染 🌙
  • 客户端:localStorage 存了dark→ 首次 render 为dark→ 渲染 🌞
  • 两边图标不一致 → 控制台可能报 hydration warning

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询