摘要
以Next.js笔记系统为例,深入RSC异步组件直接查询Redis、Server/Client组件边界、BEM命名规范、路径别名及组件驱动开发流程,展示从需求分析到技术方案到组件拆分的全栈落地路径。
一个笔记系统的需求并不复杂:左侧笔记列表,右侧 Markdown 编辑预览,支持 CRUD 和搜索。但用 Next.js 全栈实现它,涉及的技术决策覆盖了 RSC 组件边界、数据层选型、组件拆分策略和项目结构规范——这些正是 Next.js 工程化的核心议题。
技术选型:为什么是 Redis 而不是 MySQL
传统全栈项目的数据层标配是 MySQL。但笔记系统有一个特点:数据量小、结构简单、读写频繁。Redis 作为内存数据库,恰好匹配这种场景。
1import Redis from 'ioredis'; 2 3const redis = new Redis(); // 默认连接 localhost:6379 4 5const initialData = { 6 "1702459181837": '{"title":"sunt aut","content":"quia et suscipit...","updateTime":"2023-12-13T09:19:48.837Z"}', 7 "1702459182837": '{"title":"qui est","content":"est rerum tempore...","updateTime":"2023-12-13T09:19:48.837Z"}', 8}; 9 10export async function getAllNotes() { 11 const data = await redis.hgetall('notes'); 12 if (Object.keys(data).length === 0) { 13 await redis.hset('notes', initialData); 14 } 15 return await redis.hgetall('notes'); 16} 17
Redis 在这里不是缓存层,而是主数据存储。使用 Hash 数据结构(hgetall / hset),每条笔记是一个 field-value 对——field 是时间戳 ID,value 是 JSON 序列化的笔记对象。hgetall 一次返回所有笔记,hset 以对象形式批量写入初始数据。
Redis 没有表结构、不需要 SQL、不需要 ORM。数据在内存中,读写速度极快。对于笔记这种"小数据量、高频读写"的场景,Redis 比 MySQL 更轻量、更直接。
生产环境中 Redis 通常作为 MySQL 的缓存层——第一个用户请求时查 MySQL 写入 Redis,后续用户直接从 Redis 读取。但在这个笔记系统中,Redis 本身就是数据库,简化了数据层的同时保持了足够的表达能力。
RSC 组件:在服务端直接查询数据库
Next.js App Router 中,组件默认是服务端组件(RSC),可以直接使用 async/await 获取数据。Sidebar 组件展示了这种模式:
1import { getAllNotes } from "@/lib/redis"; 2import SidebarNoteList from "@/components/SidebarNoteList2"; 3 4export default async function Sidebar() { 5 const notes = await getAllNotes(); 6 7 return ( 8 <section className="col sidebar"> 9 <Link href="/"> 10 <img src="/logo.svg" width="22px" height="20px" /> 11 <strong>LLM Notes</strong> 12 </Link> 13 <nav> 14 <SidebarNoteList notes={notes} /> 15 </nav> 16 </section> 17 ); 18} 19
Sidebar 是一个 async 函数组件——这在 React 19 之前的客户端组件中是不可能的。await getAllNotes() 在服务端执行,直接读取 Redis,返回的 notes 数据被注入到 JSX 中,编译为 HTML 发送给浏览器。
整个过程没有 useState、没有 useEffect、没有 fetch、没有 loading 状态。数据获取和渲染在服务端一次完成,浏览器收到的就是包含笔记列表的完整 HTML。这就是 RSC 的核心价值——数据获取与 UI 渲染在同一位置、同一时刻完成。
@/lib/redis 是路径别名。Next.js 通过 jsconfig.json 配置 baseUrl 和 paths,将 @/ 映射到项目根目录,避免了 ../../../lib/redis 这种深层的相对路径:
1{ 2 "compilerOptions": { 3 "baseUrl": ".", 4 "paths": { 5 "@/components/*": ["components/*"], 6 "@/lib/*": ["lib/*"] 7 } 8 } 9} 10
组件拆分:规范驱动开发
在写任何代码之前,先根据需求设计组件树。这种"规范驱动"的开发方式,让每个组件都有明确的职责边界:
1Sidebar(RSC,数据获取) 2├── SidebarSearchField(搜索,未来实现) 3└── SidebarNoteList(RSC,列表渲染) 4 └── SidebarNoteItem(RSC,单个笔记) 5 └── SidebarNoteItemContent(Client Component,交互) 6
SidebarNoteList 负责将 Redis 返回的 Hash 对象转为二维数组并渲染列表:
1export default async function SidebarNoteList({ notes }) { 2 const arr = Object.entries(notes); // Hash → 二维数组 3 if (arr.length === 0) { 4 return <div className="notes-empty">No Notes created yet!</div>; 5 } 6 return ( 7 <ul className="notes-list"> 8 {arr.map(([noteId, note]) => ( 9 <li key={noteId}> 10 <SidebarNoteItem noteId={noteId} note={JSON.parse(note)} /> 11 </li> 12 ))} 13 </ul> 14 ); 15} 16
Object.entries(notes) 将 Redis Hash 的 { id: jsonString } 结构转为 [[id, jsonString], ...],方便 map 遍历。JSON.parse(note) 在服务端执行,将 JSON 字符串反序列化为对象,再传给 SidebarNoteItem。
SidebarNoteItem 进一步拆解 note 对象,提取 title、content 和 updateTime,用 dayjs 格式化日期:
1import dayjs from 'dayjs'; 2 3export default function SidebarNoteItem({ noteId, note }) { 4 const { title, content = '', updateTime } = note; 5 return ( 6 <SidebarNoteItemContent 7 id={noteId} 8 title={note.title} 9 expandChildren={ 10 <p className="sidebar-note-excerpt"> 11 {content.substring(0, 20) || <i>(No content)</i>} 12 </p> 13 } 14 > 15 <header className="sidebar-note-header"> 16 <strong>{title}</strong> 17 <small>{dayjs(updateTime).format('YYYY-MM-DD')}</small> 18 </header> 19 </SidebarNoteItemContent> 20 ); 21} 22
content.substring(0, 20) 截取前 20 个字符作为摘要,如果内容为空则显示斜体占位文本。dayjs(updateTime).format('YYYY-MM-DD') 将 ISO 时间戳格式化为可读日期。
RSC 与 Client Component 的边界
SidebarNoteItemContent 是组件树中唯一标记 'use client' 的组件:
1"use client"; 2 3export default function SidebarNoteItemContent({ 4 id, title, children, expandChildren, 5}) { 6 return ( 7 <> 8 {children} 9 </> 10 ); 11} 12
这个组件目前很简单——它只是一个透传 children 的容器。但它是整个组件树中交互能力的"预留插座"。未来需要添加点击展开、hover 高亮、右键菜单等交互时,只需在这个组件内部添加 useState 和事件处理,而不影响上游的 RSC 组件。
这种设计遵循了一个原则:把 RSC 组件推到最外层,只在需要交互的叶子节点上使用 'use client'。Sidebar(RSC)→ SidebarNoteList(RSC)→ SidebarNoteItem(RSC)→ SidebarNoteItemContent(Client Component),数据从服务端一路向下流,交互在最底层注入。
两栏布局与根 layout
根 layout.js 定义了全局的两栏结构——左侧 Sidebar,右侧 {children} 内容区:
1import Sidebar from '../components/Sidebar'; 2 3export default async function Rootlayout({ children }) { 4 return ( 5 <html> 6 <head> 7 <title>My Blog</title> 8 <meta name="description" content="一个记录学习路径的博客" /> 9 <meta name="keywords" content="Next.js, blog, notes" /> 10 </head> 11 <body> 12 <div className="container"> 13 <div className="main"> 14 <Sidebar /> 15 <section className="col note-viewer">{children}</section> 16 </div> 17 </div> 18 </body> 19 </html> 20 ); 21} 22
<head> 中的 <title> 和 <meta> 标签是 SEO 的基础——告诉搜索引擎页面标题、描述和关键词。<section className="col note-viewer"> 包裹 {children},当用户访问 / 时渲染首页的提示文字,访问 /note/[id] 时渲染笔记详情页。
HTML 语义化标签在这个布局中也有体现:<nav> 用于导航区域,<section> 用于独立内容区块。这种语义化结构不仅对 SEO 友好,也让代码的意图更清晰。
从需求到代码的完整链路
回顾整个项目的开发流程:
- 需求分析:笔记 CRUD + Markdown 渲染 + 搜索
- 技术方案:Next.js App Router + Redis + dayjs
- 组件规划:Sidebar → SidebarNoteList → SidebarNoteItem → SidebarNoteItemContent
- 目录结构:
app/(页面)、components/(组件)、lib/(数据层)、public/(静态资源) - 路径别名:
@/components/*、@/lib/* - 数据层:
lib/redis.js封装getAllNotes等数据操作 - 页面实现:
layout.js(布局)+page.js(页面)+[id]/page.js(动态路由)
每一步都是上一步的自然延伸,而不是边写边想。这种"先设计再实现"的流程,在 AI 辅助开发中尤为重要——清晰的组件规划让 AI 能准确理解每个文件的职责,生成的代码更少返工。
总结
Next.js 笔记系统的架构设计围绕三个核心决策展开:用 RSC 异步组件直接在服务端查询 Redis,消除客户端数据获取的样板代码;用组件树分层将 RSC 推到外层、Client Component 限制在叶子节点,保持交互能力的精确注入;用路径别名和 BEM 命名规范约束项目结构,让代码可维护。
Redis 在这里不是缓存,而是主存储——这种"轻数据库"的选择,反映了 Next.js 全栈开发的一个重要思路:根据数据特征选择存储,而不是被传统三层架构束缚。笔记系统数据量小、结构简单,Redis 的 Hash 类型完全够用,引入 MySQL 反而是过度设计。


