手把手教你用 Next.js 14 + Redis 从零搭建一个全栈 Markdown 笔记系统

技术栈: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 的特性做一个全站热搜榜

相关推荐
WIN赢1 小时前
【抽象思想-从复杂中抽离简单、收敛的口子】
java·前端·javascript
martindelophy1 小时前
Codex Chrome 插件 + Timeline Studio:构建可编辑的 AI 视频剪辑 Agent 工作流
前端·人工智能·chrome
whyutianict_vv2 小时前
从 Web 前端到 HarmonyOS ArkTS:一次 AI 鸿蒙全栈智能体开发的迁移实录
前端·人工智能·harmonyos
qziovv2 小时前
前端转flutter——项目架构、初始化
前端·flutter
Ali885202 小时前
Python字符串方法速查表大全
前端·python
前端_刘师兄3 小时前
FAE工程师学习路线-进程
前端
执子念的飞鱼3 小时前
浏览器直接预览 Pages、Numbers、Keynote:iWork 格式真正难在哪
前端·javascript
alloc3 小时前
从延迟聚合到可解释诊断:MetricKit 的原理与工程化实践
前端