技术栈:Next.js 14 (App Router) + Redis + TailwindCSS,涵盖 RSC、SSR、动态路由、CRUD 及搜索功能。
作为一名大模型方向的工程师,日常学习和研究中会产生海量的论文笔记、Prompt 调试记录和模型对比数据。一直以来,我都在寻找一个既能快速记录 Markdown 格式笔记,又能保证 SEO 友好的轻量级方案。
在经过一番调研后,我决定"自己动手,丰衣足食"。我利用 Next.js 14 最新的 App Router 特性,结合 Redis 内存数据库,开发了这套极简的全栈笔记系统。本文将还原完整的开发思路,不仅有核心代码展示,更重要的是分享"规范驱动开发"的工程化流程。
一、技术选型与背景(为什么要这么选?)
在动笔之前,我们先明确技术选型的依据:
- Next.js (App Router): 原生支持 RSC(React Server Component)和 SSR(Server Side Rendering)。这对于笔记系统来说至关重要------首屏加载快,且天然对 SEO 友好(笔记内容可以被搜索引擎抓取)。
- Redis: 这是一款基于内存的 NoSQL(非关系型数据库),采用 Key-Value 存储。为什么不用 MySQL?因为笔记数据的结构相对简单(标题、内容、时间戳),且读写频繁。Redis 极高的读写速度使其非常适合作为主要数据源。此外,利用 Redis 的哈希(Hash)结构存储笔记,利用其过期时间特性,甚至可以轻松实现"热点笔记缓存" 。
- npx 与 create-next-app: 使用
npx create-next-app@latest next-blog启动项目。npx是 NPM 自带的工具,无需全局安装依赖即可执行包,非常便捷。
二、规范驱动开发:先别急着写代码!
在许多 AI 辅助编程或团队协作场景中, "规范驱动开发" 能显著提升效率。在写代码前,我们需要先规划组件树和目录结构。
1. 需求分析
- 核心功能 :笔记的增删改查(CRUD),支持 Markdown 格式(数据库中存 Markdown 原文,页面渲染为 HTML)。
- 界面布局 :经典的左侧列表 + 右侧内容两栏布局。
- 交互反馈:点击 "New" 新增笔记,左侧列表实时更新;支持搜索功能。
2. 组件拆解(工作单元划分)
我们将界面拆解为独立的组件,每一个组件都是一个独立的工作单元:
-
Sidebar:侧边栏容器。
-
SidebarSearchField:搜索框(预留)。
-
SidebarNoteList:笔记列表容器(Server Component,负责数据获取)。
-
SidebarNoteItem:单个笔记项(Client Component,负责交互,如高亮、点击跳转)。
- SidebarNoteItemContent:内容展示(负责编辑和预览的切换)。
-
-
3. 目录结构规划
Next.js 14 的 App Router 遵循"文件即路由"(File-based Routing)的约定,目录结构设计如下:
text
python
next-blog/
├── app/
│ ├── api/ # 后端 API 路由(RPC 风格)
│ │ └── notes/
│ │ └── [id]/ # 处理动态路由请求
│ ├── note/
│ │ ├── [id]/ # 动态路由:/note/1
│ │ │ └── page.js # 笔记详情页
│ │ └── edit/
│ │ └── [id]/ # 编辑页
│ ├── layout.js # 根布局(包含全局 SEO 设置)
│ ├── page.js # 首页(默认展示空状态或第一篇笔记)
│ └── style.css # 全局样式
├── components/ # 存放所有 UI 组件
├── lib/ # 工具库与数据服务层
│ └── redis.js # Redis 数据库操作封装
└── public/ # 静态资源(图片、字体等)
配置路径别名:
为了避免 ../../../../lib/redis 这种地狱式引用,我们可以在 jsconfig.json 中配置别名,让 @ 直接指向根目录:
json
json
// jsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/components/*": ["components/*"],
"@/lib/*": ["lib/*"]
}
}
}
三、数据服务层(Service Layer):连接 Redis
我们将所有数据库操作封装在 lib/redis.js 中。Redis 的哈希结构非常契合我们的笔记存储模型。
设计思路: 使用 notes 作为主键(Key),存储所有笔记的哈希表。每个笔记的 ID 作为哈希表的 Field,Value 则是一个 JSON 字符串(包含标题、内容、更新时间)。
javascript
javascript
// lib/redis.js
import Redis from 'ioredis';
// 初始化 Redis 连接 (默认端口 6379)
const redis = new Redis();
// 初始化测试数据
const initialData = {
"1": JSON.stringify({
title: "LLM 入门心得",
content: "大模型不仅仅是调参...",
updateTime: Date.now()
}),
"2": JSON.stringify({
title: "Prompt Engineering 指南",
content: "Few-shot, Chain-of-thought...",
updateTime: Date.now()
})
};
// 获取所有笔记
export async function getAllNotes() {
// 使用 hgetall 获取哈希中的所有字段
const data = await redis.hgetall('notes');
// 如果数据库为空,初始化种子数据
if (Object.keys(data).length === 0) {
await redis.hset('notes', initialData);
return initialData;
}
return data;
}
// 获取单个笔记
export async function getNote(id) {
const note = await redis.hget('notes', id);
return JSON.parse(note);
}
// 新增/更新笔记
export async function addOrUpdateNote(id, title, content) {
const note = JSON.stringify({ title, content, updateTime: Date.now() });
await redis.hset('notes', id, note);
return note;
}
// 删除笔记
export async function deleteNote(id) {
await redis.hdel('notes', id);
}
四、布局与 SEO(搜索引擎优化)优化
Next.js 的 layout.js 是设置全局元数据的最佳场所。这里我们注入语义化的 HTML 结构,并预留了 SEO 相关的 Meta 标签。
jsx
javascript
// app/layout.js
import './style.css';
import Sidebar from '@/components/Sidebar';
export const metadata = {
title: '大模型工程师的博客',
description: '深入探讨 LLM、RAG、LangChain 等前沿技术',
keywords: 'LLM, Claude, DeepSeek, RAG, LangChain',
};
export default async function RootLayout({ children }) {
return (
<html>
<body>
<div className="container">
<div className="main">
{/* 左侧导航栏 - Server Component */}
<Sidebar />
{/* 右侧内容区 - children 来自 page.js */}
<section className="col note-viewer">
{children}
</section>
</div>
</div>
</body>
</html>
);
}
五、核心交互:列表渲染与"水合(Hydration)"策略
在 Next.js 中,我们需要巧妙区分 Server Component 和 Client Component。
SidebarNoteList 负责拉取数据,但它是一个 Server Component(默认),此时我们可以直接使用 await 获取数据,且无需担心暴露数据库连接信息。
jsx
javascript
// components/SidebarNoteList.jsx
import { getAllNotes } from '@/lib/redis';
import SidebarNoteItem from './SidebarNoteItem';
export default async function SidebarNoteList() {
const notes = await getAllNotes();
const arr = Object.entries(notes);
if (arr.length === 0) {
return <div className="notes-empty">No Notes created yet!</div>;
}
return (
<ul className="notes-list">
{arr.map(([noteId, note]) => {
const parsedNote = JSON.parse(note);
return (
<li key={noteId}>
{/* 将数据传递给客户端组件 */}
<SidebarNoteItem noteId={noteId} note={parsedNote} />
</li>
);
})}
</ul>
);
}
SidebarNoteItem 必须是一个 Client Component(加上 'use client'),因为它需要绑定点击事件、响应键盘操作等。
这里需要注意一个小坑:CSR(客户端渲染)时的 hydration(水合) 。如果 Server 返回的 HTML 结构与 Client 首次渲染的结构不匹配,就会报错。因此,我们可以在客户端组件中通过 useEffect 或状态管理来确保一致性。
jsx
javascript
// components/SidebarNoteItem.jsx
'use client';
import dayjs from 'dayjs';
import Link from 'next/link';
import SidebarNoteItemContent from './SidebarNoteItemContent';
export default function SidebarNoteItem({ noteId, note }) {
const { title, content, updateTime } = note;
return (
<Link href={`/note/${noteId}`}>
<SidebarNoteItemContent
id={noteId}
title={title}
expandChildren={
<p className="sidebar-note-excerpt">
{content?.substring(0, 20) || 'No content'}
</p>
}
>
<header className="sidebar-note-header">
<strong>{title}</strong>
<small>{dayjs(updateTime).format('YYYY-MM-DD HH:mm:ss')}</small>
</header>
</SidebarNoteItemContent>
</Link>
);
}
六、动态路由与 API 规范
Next.js 的 App Router 下,/app/note/[id]/page.js 即对应 /note/1 的详情页。
jsx
javascript
// app/note/[id]/page.js
import { getNote } from '@/lib/redis';
export default async function NotePage({ params }) {
const { id } = params;
const note = await getNote(id);
return (
<div className="note-viewer-content">
<h1>{note.title}</h1>
<div className="markdown-body">
{/* 这里可以配合 remark 库将 Markdown 转 HTML */}
{note.content}
</div>
</div>
);
}
对于修改操作,我们采用 API Routes 来实现 RESTful 风格的接口(或 RPC 风格)。例如 /api/notes/[id]/route.js 处理 DELETE 请求。
七、开发心得与注释大法
在本次开发过程中,我深刻体会到 "注释不仅是写给现在的自己,更是写给未来的队友(或是 AI)" 。
-
规划未来(TODO 注释): 比如
// TODO: 实现搜索功能 SideSearchField,这不仅能提醒自己,还能让团队明确边界。 -
BEM 命名与 CSS 管理: 项目中我采用了 BEM(Block, Element, Modifier)命名规范配合 TailwindCSS,有效避免了样式冲突。
.sidebar-note-header__title清晰表明了层级。
-
Redis 的数据结构选择: 为什么用哈希而不是字符串?哈希允许我们单独更新某个字段(比如只改标题),而不用重写整个对象,性能更优。
八、结语
通过这次实战,我们不仅实现了一个功能完整的笔记系统,更重要的是熟悉了 Next.js 14 的 RSC(React 服务端组件) 、路由约定 以及 Redis 在实际业务中的使用模式。
这套架构的可扩展性极强------未来我们可以轻松接入 LangChain 实现"AI 自动总结笔记",或者利用 Redis 的特性做一个全站热搜榜。