Next.js App Router 实战:笔记博客的数据到底在哪儿跑?

用 React 写博客时,最容易遇到的困惑不是页面怎么拆,而是:数据到底该在哪儿取?组件又该在哪儿运行?
以前的 SPA 通常是浏览器先下载 JavaScript,再请求接口、渲染页面。换到 Next.js App Router 后,page.tsx 默认就在服务端运行,组件既可以直接读取 Redis,又能把一小块交互交给浏览器。第一次接触时,很多人会把这几件事混在一起。
这篇文章以一个笔记博客为例,讲清楚 App Router 的文件路由、根 Layout、动态路由,以及一条笔记数据从 Redis 到浏览器的完整路径。
一、先纠正一个误解:Next.js 不只是为 SEO 而生
传统 SPA 的常见首屏流程是:
text
HTML -> 下载 JS -> 请求数据 -> 浏览器渲染内容
这会让首屏内容依赖 JavaScript 和客户端请求。对内容型网站来说,服务端先提供可见内容,通常更利于首屏体验和搜索收录。
但不要把它说成"SPA 一定是空壳,爬虫一定看不到"。现代爬虫能执行部分 JavaScript,SPA 也能做 SEO。Next.js 真正提供的是一套选择:路由、服务端组件、缓存、流式渲染和接口能力可以放进同一个工程里。
二、文件即路由:博客目录就是 URL 地图
笔记博客可以这样组织:
text
app/
├── layout.tsx # 所有页面的外壳
├── page.tsx # /
├── node/[id]/page.tsx # /node/123
├── node/edit/page.tsx # /node/edit
├── node/edit/[id]/page.tsx# /node/edit/123
└── api/notes/route.ts # /api/notes
目录名对应 URL 片段,page.tsx 对应一个可访问页面。[id] 是动态路由段,会匹配任意一个笔记 ID。
不需要再维护一份 <Routes> 配置表。访问 /node/123 时,Next.js 根据目录定位页面,再把沿途的 Layout 组合起来。

三、根 Layout:共享 UI 的插槽
根布局负责整站都需要的结构,例如全局样式、侧边栏和页面容器:
tsx
// app/layout.tsx
import "./style.css";
import Sidebar from "@/components/sidebar";
import type { ReactNode } from "react";
export default function RootLayout({ children }: { children: ReactNode }) {
return <html lang="zh-CN"><body><Sidebar />{children}</body></html>;
}
children 是当前路由页面的插槽。访问首页时它是 app/page.tsx;访问 /node/123 时它是 app/node/[id]/page.tsx。
如果你用过 React Router,可以把它类比成 Layout 里的 <Outlet />。区别是 App Router 用文件层级表达嵌套关系,不用手动配置父子路由。
元数据别手写在 <head> 里
App Router 更推荐使用 metadata API,由框架合并和输出 <head> 内容:
tsx
// app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "小明的技术笔记",
description: "LLM、RAG 与工程实践笔记",
};
title 和 description 是 SEO 的基础,但 SEO 还依赖真实内容、语义化 HTML、稳定 URL 和内部链接,不能只靠几个 meta 标签。
四、Server Component:把读数据留在服务器
App Router 的组件默认是 Server Component。它可以直接导入数据层,在服务端读取 Redis:
tsx
// components/sidebar.tsx
import { getAllNotes } from "@/lib/redis";
import { SidebarNoteList } from "./sidebar-note-list";
export default async function Sidebar() {
const notes = await getAllNotes();
return <SidebarNoteList notes={notes} />;
}
首次请求时,Next.js 会在服务端执行组件,生成 HTML 和 RSC 数据给浏览器。浏览器能先展示笔记列表,也不用为了这部分列表额外下载组件逻辑。
这里有两个边界要记住:
- Server Component 可以访问数据库、Redis、文件系统和只放在服务端的密钥。
- 它不能绑定
onClick,也不能使用useState、useEffect等客户端 Hook。
"服务端取数"不代表永远没有加载等待。慢查询照样会拖慢响应;可以用 loading.tsx 或 Suspense 提供加载反馈,并通过缓存和并行请求改善速度。
五、列表渲染:Object.entries 为什么不能换成 values
假设 Redis Hash 返回的是 { noteId: noteJson },列表既要 ID 又要内容,就应该使用 Object.entries:
tsx
const entries = Object.entries(notes);
return entries.map(([id, noteJson]) => {
const note = JSON.parse(noteJson);
return <li key={id}>{note.title}</li>;
});
它的返回值是 [key, value] 对:
ts
Object.entries({ a: 1 }); // [["a", 1]]
Object.values({ a: 1 }); // [1]
如果改用 Object.values(),却仍然写 ([id, noteJson]) 解构,字符串会被误当成数组拆开,后续 JSON.parse 就会出错。
实际项目还应在数据层完成 JSON 解析和类型校验。让组件接收 Note[],比把 Redis 的原始字符串格式泄漏到 UI 层更容易维护。
六、动态路由:[id] 把 URL 参数交给页面
访问 /node/1702459181837 时,[id] 就是这段 ID。在当前 Next.js 的动态 API 中,params 是异步值:
tsx
// app/node/[id]/page.tsx
export default async function NotePage({ params }: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return <article>正在查看笔记:{id}</article>;
}
较早版本的 Next.js 可以直接读取 params.id。跟着旧教程写时,要先确认项目使用的 Next.js 版本;新版本中 await params 才是更稳妥的写法。
拿到 ID 后,服务端页面可以调用 getNote(id)。查不到数据时,使用 notFound() 返回 404,比自己渲染一个普通提示更符合路由语义。
七、Client Component:只把交互这一小块送到浏览器
需要编辑笔记、输入标题或点击保存时,再创建一个客户端组件:
tsx
"use client";
import { useState } from "react";
export function NoteTitle({ initialTitle }: { initialTitle: string }) {
const [title, setTitle] = useState(initialTitle);
return <input value={title} onChange={(event) => setTitle(event.target.value)} />;
}
"use client" 不是"这个组件只在浏览器生成 HTML",而是在模块边界上声明:它需要客户端 JavaScript 来水合和处理交互。初始页面仍可能由服务端预渲染。
最实用的组合是:Server Page 获取初始笔记,Client Component 只负责输入、点击和局部状态。 不要因为页面里有一个按钮,就把整页标记为 "use client"。
八、保存笔记时,数据会怎么走?
编辑器提交后,可以调用 Route Handler:
ts
// app/api/notes/route.ts
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const { title, content } = await request.json();
const note = await createNote({ title, content });
return NextResponse.json(note, { status: 201 });
}
这条链路是:客户端编辑器 fetch('/api/notes') -> Route Handler 校验请求 -> Redis 或数据库持久化 -> 返回 JSON -> 客户端更新局部状态或 router.refresh()。
Route Handler 只是 HTTP 入口,并不会自动帮你完成鉴权、参数校验、限流和持久化。特别是不要用模块级数组临时存笔记,开发热更新、多实例部署后数据都会不可靠。
九、面试时怎么一句话讲清楚?
App Router 是 Next.js 基于文件系统的路由方案。组件默认是 Server Component,适合在服务端获取数据并减少客户端 JavaScript;需要状态、事件或浏览器 API 的局部组件用
"use client"标记。页面通过page.tsx、共享结构通过layout.tsx组织,动态路由通过[id]接收参数,Route Handler 则在同一项目中提供 HTTP 接口。
总结
| 概念 | 在笔记博客里负责什么 |
|---|---|
page.tsx |
一个可访问页面,例如某篇笔记详情 |
layout.tsx |
共享的侧边栏、样式和页面骨架 |
| Server Component | 从 Redis 读取笔记并生成初始内容 |
| Client Component | 编辑输入、点击保存等交互 |
[id] |
从 URL 取出笔记 ID |
| Route Handler | 接收创建、修改笔记的 HTTP 请求 |
metadata |
管理标题、描述等页面元数据 |
App Router 的重点不是"所有东西都放到服务端",而是让数据读取、页面渲染和浏览器交互各自待在合适的位置。弄清这条数据流后,Next.js 就不再是一堆新文件名,而是一种更清晰的 React 工程分层方式。