摘要
以Next.js笔记系统为例,深入RSC异步组件直接查询Redis、Server/Client组件边界、BEM命名规范、路径别名及组件驱动开发流程,展示从需求分析到技术方案到组件拆分的全栈落地路径。
一个笔记系统的需求并不复杂:左侧笔记列表,右侧 Markdown 编辑预览,支持 CRUD 和搜索。但用 Next.js 全栈实现它,涉及的技术决策覆盖了 RSC 组件边界、数据层选型、组件拆分策略和项目结构规范------这些正是 Next.js 工程化的核心议题。
技术选型:为什么是 Redis 而不是 MySQL
传统全栈项目的数据层标配是 MySQL。但笔记系统有一个特点:数据量小、结构简单、读写频繁。Redis 作为内存数据库,恰好匹配这种场景。
javascript
import Redis from 'ioredis';
const redis = new Redis(); // 默认连接 localhost:6379
const initialData = {
"1702459181837": '{"title":"sunt aut","content":"quia et suscipit...","updateTime":"2023-12-13T09:19:48.837Z"}',
"1702459182837": '{"title":"qui est","content":"est rerum tempore...","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');
}
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 组件展示了这种模式:
javascript
import { getAllNotes } from "@/lib/redis";
import SidebarNoteList from "@/components/SidebarNoteList2";
export default async function Sidebar() {
const notes = await getAllNotes();
return (
<section className="col sidebar">
<Link href="/">
<img src="/logo.svg" width="22px" height="20px" />
<strong>LLM Notes</strong>
</Link>
<nav>
<SidebarNoteList notes={notes} />
</nav>
</section>
);
}
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 这种深层的相对路径:
json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/components/*": ["components/*"],
"@/lib/*": ["lib/*"]
}
}
}
组件拆分:规范驱动开发
在写任何代码之前,先根据需求设计组件树。这种"规范驱动"的开发方式,让每个组件都有明确的职责边界:
arduino
Sidebar(RSC,数据获取)
├── SidebarSearchField(搜索,未来实现)
└── SidebarNoteList(RSC,列表渲染)
└── SidebarNoteItem(RSC,单个笔记)
└── SidebarNoteItemContent(Client Component,交互)
SidebarNoteList 负责将 Redis 返回的 Hash 对象转为二维数组并渲染列表:
javascript
export default async function SidebarNoteList({ notes }) {
const arr = Object.entries(notes); // Hash → 二维数组
if (arr.length === 0) {
return <div className="notes-empty">No Notes created yet!</div>;
}
return (
<ul className="notes-list">
{arr.map(([noteId, note]) => (
<li key={noteId}>
<SidebarNoteItem noteId={noteId} note={JSON.parse(note)} />
</li>
))}
</ul>
);
}
Object.entries(notes) 将 Redis Hash 的 { id: jsonString } 结构转为 [[id, jsonString], ...],方便 map 遍历。JSON.parse(note) 在服务端执行,将 JSON 字符串反序列化为对象,再传给 SidebarNoteItem。
SidebarNoteItem 进一步拆解 note 对象,提取 title、content 和 updateTime,用 dayjs 格式化日期:
javascript
import dayjs from 'dayjs';
export default function SidebarNoteItem({ noteId, note }) {
const { title, content = '', updateTime } = note;
return (
<SidebarNoteItemContent
id={noteId}
title={note.title}
expandChildren={
<p className="sidebar-note-excerpt">
{content.substring(0, 20) || <i>(No content)</i>}
</p>
}
>
<header className="sidebar-note-header">
<strong>{title}</strong>
<small>{dayjs(updateTime).format('YYYY-MM-DD')}</small>
</header>
</SidebarNoteItemContent>
);
}
content.substring(0, 20) 截取前 20 个字符作为摘要,如果内容为空则显示斜体占位文本。dayjs(updateTime).format('YYYY-MM-DD') 将 ISO 时间戳格式化为可读日期。
RSC 与 Client Component 的边界
SidebarNoteItemContent 是组件树中唯一标记 'use client' 的组件:
javascript
"use client";
export default function SidebarNoteItemContent({
id, title, children, expandChildren,
}) {
return (
<>
{children}
</>
);
}
这个组件目前很简单------它只是一个透传 children 的容器。但它是整个组件树中交互能力的"预留插座"。未来需要添加点击展开、hover 高亮、右键菜单等交互时,只需在这个组件内部添加 useState 和事件处理,而不影响上游的 RSC 组件。
这种设计遵循了一个原则:把 RSC 组件推到最外层,只在需要交互的叶子节点上使用 'use client' 。Sidebar(RSC)→ SidebarNoteList(RSC)→ SidebarNoteItem(RSC)→ SidebarNoteItemContent(Client Component),数据从服务端一路向下流,交互在最底层注入。
两栏布局与根 layout
根 layout.js 定义了全局的两栏结构------左侧 Sidebar,右侧 {children} 内容区:
javascript
import Sidebar from '../components/Sidebar';
export default async function Rootlayout({ children }) {
return (
<html>
<head>
<title>My Blog</title>
<meta name="description" content="一个记录学习路径的博客" />
<meta name="keywords" content="Next.js, blog, notes" />
</head>
<body>
<div className="container">
<div className="main">
<Sidebar />
<section className="col note-viewer">{children}</section>
</div>
</div>
</body>
</html>
);
}
<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 反而是过度设计。