
如果你正在用 Next.js 做全栈项目或者想搞懂 RSC、App Router 和 Redis 在真实业务里怎么配合这篇文章就是为你准备的。我们会从零开始一步步拆解一个支持 Markdown 的笔记系统重点不是“能跑”而是“为什么这么设计”。一、为什么又是 Next.js先说清楚一个工具npx。很多同学以为npx是安装命令其实它是 npm 自带的“包执行器”。说白了就是你不需要全局安装create-next-app直接npx create-next-app就能跑起来用一次下一次省得污染全局环境。而create-next-app这个脚手架帮你搞定了 React 全栈开发要用的几乎一切SSR服务器端渲染首屏 HTML 直接给到浏览器SEO 友好。RSCReact Server Component组件默认在服务端渲染数据获取不用useEffect直接async组件里await数据库。App Router文件即路由目录结构就是 API 设计。你会发现Next.js 把“数据流”和“渲染流”统一了服务端拿数据 → 服务端渲染 → 客户端水合hydration整个过程非常丝滑。技术选型不是追新而是看它能不能让数据流更简单。RSC 到底好在哪里数据在转换过程中变成了什么很多同学对 RSC 的理解还停留在“服务端渲染”其实它比传统 SSR 更进了一步。RSC 的核心优势可以总结为三点减少客户端 JS 体积服务端组件及其依赖不会打包到客户端比如你在服务端组件里用了ioredis、dayjs这些库客户端完全不需要下载它们的代码。直接访问后端资源服务端组件可以安全地读写数据库、调用内部 API不需要暴露接口给浏览器。自动代码分割Next.js 会根据路由和服务端/客户端边界自动拆包首屏加载更快。那数据在 RSC 的渲染过程中究竟变成了什么当你在服务端组件里await数据库拿到笔记数据后这个数据并不会直接变成 HTML 字符串。Next.js 会把它序列化成一种特殊的RSC Payload类似 JSON 的流式格式这个 payload 包含了组件树的结构、props、以及客户端组件的引用信息。浏览器收到这个 payload 后React 会在客户端进行hydration水合把静态的结构和客户端交互逻辑结合起来最终生成可交互的 DOM。简单理解就是数据库对象 → JS 对象 → RSC Payload → 客户端 React 元素 → 真实 DOM这也是为什么服务端组件里可以直接console.log(notes)在终端看到数据但页面上却能渲染出列表的原因。二、需求拆解两栏笔记系统我们要做的是一个极简但完整的笔记应用左侧笔记列表右侧笔记内容两栏布局。点击“New”新增笔记左侧列表同步更新。支持编辑、删除笔记。笔记内容支持 Markdown 格式。支持搜索功能。看起来功能不多但涉及路由设计、组件拆分、数据持久化、客户端交互等多个维度非常适合作为 Next.js 全栈练手项目。路由设计Next.js App Router 的文件即路由让 API 设计和页面路由天然对应功能路由类型首页空状态/页面笔记详情/note/[id]页面新增笔记/edit页面/交互编辑笔记/edit/[id]页面/交互删除/新增 API/api/...接口未来扩展这里我们把“新增”和“编辑”都放在/edit下用动态路由区分是否带[id]逻辑更内聚。组件规划先规划组件再写代码——AI 时代组件就是你的工作单元。我们根据 UI 结构把页面拆成这些组件Sidebar侧边栏容器负责拉取笔记列表。SidebarSearchField搜索框预留。SidebarNoteList笔记列表服务端组件SEO 友好。SidebarNoteItem单个笔记项展示标题、时间、摘要。SidebarNoteItemContent笔记项的客户端交互部分高亮、点击等。NoteEditor编辑区客户端组件。NotePreview预览区Markdown 渲染。这样的拆分让每个组件职责单一后面加交互或改 UI 都不至于牵一发动全身。三、项目骨架目录与别名先看目录结构├── app/ │ ├── layout.js # 根布局 │ ├── page.js # 首页 │ ├── note/ │ │ └── [id]/ │ │ └── page.js # 笔记详情 │ └── edit/ │ ├── page.js # 新增笔记 │ └── [id]/ │ └── page.js # 编辑笔记 ├── components/ # 通用组件 ├── lib/ # 数据逻辑 ├── public/ # 静态资源 └── jsconfig.json # 别名配置在app/note/[id]/page.js里如果我们要引入lib/redis.js相对路径会写成import { getAllNotes } from ../../../lib/redis;三层../看起来就头疼。我们在jsconfig.json里配置别名{ compilerOptions: { baseUrl: ., paths: { /*: [./*] } } }之后就可以这样写import { getAllNotes } from /lib/redis;短路径导入少一点../../../多一点开发幸福感。四、BEM 命名与布局样式方面我们采用BEM 命名规范 Tailwind 原子类混合的思路。BEM 的核心是Block块独立的功能单元如note。Element元素块内部的组成部分用__连接如note__title。Modifier修饰器状态或变体用--连接如note--empty。虽然现在我们没有用 Tailwind但 BEM 的结构化命名对维护非常友好。根布局app/layout.js长这样import ./style.css; import Sidebar from /components/Sidebar; export default async function RootLayout({ children }) { return ( html head titleLLM Notes/title meta namedescription content一位大模型工程师的笔记系统 / meta namekeywords contentllm,deepseek,rag,langchain / /head body div classNamecontainer div classNamemain Sidebar / section classNamecol note-viewer{children}/section /div /div /body /html ); }这里用了语义化标签section、nav并在head里放 SEO 相关的 meta。Next.js App Router 的layout.js天然支持这种结构每个页面都会作为children渲染到右侧内容区。layout.js 里的 SEO 友好写法在app/layout.js中head标签里的内容对 SEO 至关重要。你需要写清楚这几个关键点title页面标题要准确描述当前页面的核心内容。比如“LLM Notes - 大模型工程师的笔记系统”而不是笼统的“首页”。meta namedescription页面描述搜索引擎会展示在搜索结果里写得好能直接提升点击率。建议包含 2-3 个核心关键词长度控制在 150 字符左右。meta namekeywords虽然现在 Google 已不把它作为排名因子但其他搜索引擎或内部系统可能还在用写了也不亏。如果想更规范还可以加link relcanonical告诉搜索引擎哪个 URL 是规范版本避免重复内容。Open Graph 标签og:title、og:description、og:image分享到社交媒体时展示更友好。robots 标签控制页面是否被索引一般默认即可。Next.js 也提供了更优雅的Metadata API你可以在layout.js或page.js里导出一个metadata对象Next.js 会自动生成对应的head标签。但用原生head也完全可以关键是内容要准确、有针对性而不是复制粘贴模板。五、数据层为什么选 Redis很多人觉得 Redis 只是缓存其实它做轻量级数据存储也很香。我们这个笔记系统数据结构很简单每篇笔记是一个 JSON 对象{ title, content, updateTime }需要一个 key 来标识每篇笔记这不就是 Redis 的哈希Hash吗redis key: notes field: 1702459181837 value: {title:sunt aut,content:...,updateTime:...}Redis 特点内存数据库读写飞快适合高频访问。哈希结构hgetall/hset直接操作字段不用 SQL。零配置启动即用不需要建表、建索引。Redis 不是银弹但它能让数据库少受点罪。实际场景中掘金首页文章列表几分钟不变第一个用户请求 MySQL后续用户直接读 Redis数据库压力骤降。Redis 哈希数据结构解析Redis 的哈希Hash是一种键值对的集合特别适合存储对象。你可以把它理解成一个微型的“字典”外层有一个 Redis key比如notes这个 key 对应的值不是简单的字符串而是一组 field-value 对。在这个笔记系统里我们这样设计外层 keynotes代表“所有笔记”这个集合。field笔记 ID时间戳字符串用来唯一标识一篇笔记。value笔记内容的 JSON 字符串比如{title:sunt aut,content:...,updateTime:...}。这样hset(notes, 1702459181837, jsonString)就相当于给notes这个哈希表添加或更新了一个字段。hgetall(notes)会返回一个 JS 对象形如{ 1702459181837: {title:sunt aut,content:...,updateTime:...}, 1702459182837: {title:qui est,content:...,updateTime:...} }拿到这个对象后我们用Object.entries()把它转成二维数组方便用map遍历渲染列表。哈希相比字符串类型的优势在于你可以单独读取、更新或删除某个字段而不需要把整个对象取出来再序列化。这对于笔记列表这种“整体读取、单独更新”的场景非常合适。我们在lib/redis.js里封装数据操作import Redis from ioredis; const redis new Redis(); const initialData { 1702459181837: {title:sunt aut,content:quia et suscipit suscipit recusandae,updateTime:2023-12-13T09:19:48.837Z}, 1702459182837: {title:qui est,content:est rerum tempore vitae sequi sint,updateTime:2023-12-13T09:19:48.837Z}, 1702459188837: {title:ea molestias,content:et iusto sed quo iure,updateTime:2023-12-13T09:19:48.837Z} }; export async function getAllNotes() { const data await redis.hgetall(notes); if (Object.keys(data).length 0) { await redis.hset(notes, initialData); } return await redis.hgetall(notes); }这里hset可以一次设置多个字段hgetall返回一个对象key 就是笔记 IDvalue 是 JSON 字符串。所有数据逻辑都放在lib/目录下组件里只负责调用干净利落。六、核心组件实现1. Sidebar服务端组件直接拿数据components/Sidebar.jsimport Link from next/link; import { getAllNotes } from /lib/redis; import SidebarNoteList from ./SidebarNoteList; export default async function Sidebar() { const notes await getAllNotes(); return ( section classNamecol sidebar Link href/ classNamesidebar-header img classNamelogo src/logo.svg width22px height20px rolepresentation / strongLLM Notes/strong /Link section classNamesidebar-menu rolemenubar {/* SideSearchField 未来实现 */} /section nav SidebarNoteList notes{notes} / /nav /section ); }注意Sidebar是async 组件它可以直接await getAllNotes()。这就是 RSC 的魅力数据获取和组件渲染在服务端一气呵成不需要客户端 API 请求。2. SidebarNoteList把哈希转成列表components/SidebarNoteList.jsimport SidebarNoteItem from ./SidebarNoteItem; export default async function SidebarNoteList({ notes }) { const arr Object.entries(notes); // 哈希转二维数组 if (arr.length 0) { return div classNamenotes-emptyNo Notes created yet!/div; } return ( ul classNamenote-list {arr.map(([noteId, note]) ( li key{noteId} SidebarNoteItem noteId{noteId} note{JSON.parse(note)} / /li ))} /ul ); }Redis 返回的notes是对象Object.entries转成数组方便map。为什么不能直接写一个SidebarNoteList还要拆出SidebarNoteItem很多同学可能会想列表渲染就这么点逻辑直接在一个组件里map出来不就行了拆分的原因主要有三点职责单一SidebarNoteList只负责“获取数据 遍历渲染”而单个笔记项如何展示标题、时间、摘要、交互状态是SidebarNoteItem的职责。如果以后要修改笔记项的样式或加点击效果不需要动列表容器的逻辑。服务端/客户端边界SidebarNoteList是服务端组件里面包含了数据获取和转换。但如果把笔记项的交互逻辑比如高亮选中、展开收起也写在同一个组件里就必须把整个组件标记为use client这样整个列表都会变成客户端组件失去服务端渲染的优势。拆出SidebarNoteItemContent作为客户端组件后只有这一小块需要水合其余部分仍然在服务端静态输出。可复用性和可测试性单个笔记项可能在别的场景复用比如搜索结果列表拆分后可以单独测试每个小组件而不是面对一个几百行的庞然大物。3. SidebarNoteItem展示笔记摘要components/SidebarNoteItem.jsimport dayjs from dayjs; import SidebarNoteItemContent from ./SidebarNoteItemContent; export default function SidebarNoteItem({ noteId, note }) { const { title, content , updateTime } note; return ( SidebarNoteItemContent id{noteId} title{title} expandChildren{ p classNamesidebar-note-excerpt {content.substring(0, 20) || i(No content)/i} /p } header classNamesidebar-note-header strong{title}/strong small{dayjs(updateTime).format(YYYY-MM-DD)}/small /header /SidebarNoteItemContent ); }这里我们用dayjs格式化时间并截取内容前 20 个字符作为摘要。4. SidebarNoteItemContent客户端交互的入口components/SidebarNoteItemContent.jsuse client; import { useState, useEffect } from react; export default function SidebarNoteItemContent({ id, title, children, expandChildren }) { return ( {children} / ); }虽然目前它只渲染children但它是整个列表项中唯一一个标记use client的组件。未来要做高亮选中、展开/收起、点击跳转等交互都可以在这里加状态而不影响父级的服务端渲染。服务端组件负责拿数据客户端组件负责搞交互这就是 Next.js App Router 的最佳实践。七、Markdown 预览与编辑思路笔记内容在数据库里存的是 Markdown 字符串展示时需要转成 HTML。常用方案是markedDOMPurify或者直接用react-markdown组件。服务端可以用marked把 Markdown 转成 HTML 字符串再通过dangerouslySetInnerHTML渲染注意 XSS 过滤。客户端直接用react-markdown在NotePreview组件里渲染更安全也支持自定义样式。编辑区NoteEditor自然是客户端组件用useState控制输入提交时调用 API 或 Server Action 保存到 Redis。八、总结与可优化项这个笔记系统麻雀虽小但五脏俱全RSC App Router服务端组件拿数据客户端组件做交互。Redis 哈希存储轻量、快速、零配置。BEM 命名 组件拆分代码可维护性拉满。别名配置告别相对路径地狱。当然还有很多可以优化的地方搜索功能目前是预留可以在SidebarSearchField里做客户端过滤或服务端搜索接口。持久化Redis 数据在内存里重启会丢可以配合 MySQL 做持久化。API Route新增/编辑/删除都可以用 Next.js 的 Route Handlersapp/api/实现 RPC 风格接口。部署Vercel 一键部署Redis 可以用 Upstash 免费额度。好的架构不是一开始就完美而是每一步都清晰可演进。希望这篇文章能帮你把 Next.js 和 Redis 的配合摸清楚。如果你也在做类似的全栈小项目不妨动手试试踩坑的过程才是真正的成长。