Next.js App Router 踩坑笔记
技术

Next.js App Router 踩坑笔记

2026 年 9 月 5 日约 2 分钟Next.js · React · 服务端组件

服务端组件、水合报错、图片优化、metadata 生成——把这次重写站点时踩到的坑整理成一份清单。

这次重写站点把 Pages Router 换成了 App Router,过程中遇到的坑比想象中多。整理一份清单,也算给以后的自己省点时间。

1. 默认就是服务端组件

app/ 目录下的组件默认在服务端渲染,不能直接用 useState。需要交互的文件必须在第一行写:

"use client";

我的习惯是:页面骨架(page.tsx)保持服务端,只把需要交互的叶子组件抽成客户端组件。文章列表、数据统计这类纯展示内容完全不需要进入客户端包。

2. 水合报错几乎都来自「时间」

在服务端渲染时输出当前时间、随机数、window 宽度,客户端再渲染一次就对不上了。三种解法:

  1. 把这类内容放进 useEffect,首屏渲染占位;
  2. 给根节点加 suppressHydrationWarning;
  3. 干脆让父级页面 export const dynamic = "force-dynamic"。

我这次用的是第 1 种,还顺手避开了「运行天数」在静态导出时被冻结的问题。

3. 读文件必须留在服务端

博客文章用 Markdown 存放,读取时用 node:fs:

import fs from "node:fs";
import path from "node:path";

const dir = path.join(process.cwd(), "content", "posts");
export const slugs = fs.readdirSync(dir).map((f) => f.replace(/\.md$/, ""));

注意:一旦某个客户端组件间接 import 了这个文件,构建就会因为 fs 无法打包而失败。所以我把 lib/posts.ts 只提供给服务端组件使用。

4. metadata 要跟着内容走

文章详情页用 generateMetadata 把标题、摘要、封面写进 <head>,分享到社交平台时才有正确的预览卡片:

export async function generateMetadata({ params }: Props) {
  const { slug } = await params;
  const post = getPostBySlug(slug);
  return { title: post?.title, description: post?.excerpt };
}

注意 Next 15 之后 params 是 Promise,需要 await,这一点和早期文档不一致,很容易写错。

5. 构建期报错优先看 eslint 之外的输出

有一次 next build 失败,报错行号指向一个完全无关的组件。实际原因是我在客户端组件里用了 path。这类报错要往「模块边界」的方向想,而不是盯着那一行代码。

整理完这份清单,最大的感受是:App Router 的心智负担主要在边界上——服务端与客户端的边界、静态与动态的边界。想清楚边界,剩下的写法都很直接。

评论(2)

演示模式下评论只保存在你的浏览器里

  • 路路过的小猫2026 年 9 月 19 日

    写得很清楚,尤其是关于边界那一节,帮我解决了困扰半天的报错。

  • LLuna2026 年 9 月 19 日

    玻璃质感那段有被治愈到,回去就改一下我的站点配色。

播放列表(4)

反方向的钟

反方向的钟