从零读懂一个 Next.js 全栈笔记应用

从零读懂一个 Next.js 全栈笔记应用

这篇文章基于一个 next-blog 笔记项目,逐文件讲解它的技术背景、目录结构、路由设计、组件拆解和数据服务。


一、技术背景:为什么用 Next.js

README 里开篇就点明了这个项目为什么选 Next.js 和它背后的关键概念。

1. npx 的作用

lua 复制代码
npx 是 npm 自带工具,可直接运行 node 包,无需全局安装依赖
npx = npm i -g create-next-app + create-next-app
  • npx 是 npm 自带的工具,可以直接运行 node 包,不需要全局安装依赖
  • npx create-next-app 就能快速搭建项目,等价于「先全局装 create-next-app,再运行它」。
  • 好处是便捷,适合快速试用、测试电脑能不能跑项目。

2. 为什么是 Next.js(React 全栈脚手架)

Next.js 是一个 React 全栈开发脚手架,README 里列出了几个关键概念:

概念 含义
SSR 服务器端渲染(Server-Side Rendering)
SEO 搜索引擎优化(Search Engine Optimization)
RSC React Server Component(React 服务端组件)
  • SSR 让页面在服务器渲染,利于 SEO。
  • use client 用来标记客户端组件,配合 hydration(水合)机制。

这些概念在后面的代码里会反复出现------比如组件里是否写 "use client",决定了它是服务端组件还是客户端组件。


二、项目需求:一个 markdown 笔记系统

README 明确了这个项目要做什么:

  • 做一套笔记系统 ,支持笔记的 CRUD(增删改查),支持 markdown 格式
  • 关键设计:数据库里存的是 markdown 文本,页面显示的是 HTML ,中间用 marked 库做转换。

具体功能点拆解如下:

  1. 界面分两列 :左侧笔记列表,右侧笔记内容(对应 /page.js)。
  2. 点击 new 增加一条笔记,增加后左侧列表同步更新。
  3. 编辑功能,可以删除一条笔记,左侧同步更新。
  4. 编辑当前笔记,支持 markdown。
  5. 搜索功能

路由上也对应了这些需求(App Router 文件即路由):

bash 复制代码
/add          POST 新增
/note/[id]    动态路由,page.js 为笔记详情
/note/[id]/edit   修改
/edit         page 新增一条

三、目录结构

项目采用 Next.js 的约定式目录:

diff 复制代码
- app          页面主目录
    page.js    首页
    layout.js  布局
    [id]       动态路由
- components   组件
- lib          数据库操作、常用函数
- public       静态资源(static server)

核心约定:数据业务逻辑放在 lib 目录 ,组件放在 components,页面放在 app


四、配置 alias:@ 直达根目录

app/notes/[id]/page.js 里引入 lib/redis.js 时,如果用相对路径要写 ../../../lib/redis.js,很麻烦。于是配置了 alias 短链接:

vbnet 复制代码
baseUrl: .
path:
  @/components/*
  @/lib/*

配置后,@ 直接指向根目录,就能写成:

javascript 复制代码
import Sidebar from '@/components/Sidebar'
import { getAllNotes } from '@/lib/redis'

这在我们实际代码里(app/layout.jscomponents/Sidebar.jsSidebarNoteItem.js)都已经用上了。


五、布局:app/layout.js

layout.js 是页面的根布局,代码本身就是一个 async 组件

javascript 复制代码
import './style.css'
import Sidebar from '@/components/Sidebar'

export default async function RootLayout({ children }) {
  return (
    <html>
      <head>
        <title>HHGZ的博客</title>
        <meta name="description" content="..." />
        <meta name="keywords" content="llm,claude,deepseek,rag,langchain" />
      </head>
      <body>
        <div className="container">
          <div className="main">
            <Sidebar/>
            <section className="col note-viewer">{children}</section>
          </div>
        </div>
      </body>
    </html>
  )
}

从这个文件能看到几个知识点:

  1. layout 的层级结构(README 里有梳理):

    css 复制代码
    layout
    └── html
        ├── head
        │   ├── title
        │   └── meta
        └── body
            ├── nav       侧边栏、导航栏
            └── children  page.js
  2. 两列布局 :左侧 <Sidebar/>(笔记列表),右侧 <section className="col note-viewer">{children}</section>(笔记内容),正好对应需求里的「左右两列」。

  3. children 就是子页面{children} 的位置会渲染对应的 page.js


六、首页占位:app/page.js

javascript 复制代码
// RSC 组件 async 异步 为了 await 先去后端数据
export default async function Page() {
  return (
    <div className="note--empty-state">
      <span className="note-text--empty-state">
        Click a note on the left to view something.
      </span>
    </div>
  )
}

注释点出了关键:RSC 组件可以写成 async,因为要 await 先去后端取数据。当前这个首页还没写取数据的逻辑,只是返回一个空状态提示。


七、组件拆解:规范驱动编程 + BEF

1. 规范驱动编程

README 强调了一个重要的工作方法------开发之前不要急着写代码,而是先:

  1. 分析需求
  2. 确定技术方案(next.js)
  3. 拆解任务细节:路由 + 组件
  4. 规划需要哪些组件

其中「组件是工作单元,AI 生成的工作单元」------把大任务拆成一个个小组件,再由 AI 逐个生成。

项目规划出的组件树:

markdown 复制代码
Sidebar
  SidebarSearchField
  EditButton(复用)
  SidebarNoteList
    NoteItem
Note
  NoteEditor   编辑
  NotePreview  负责笔记的预览界面

2. BEF 命名规范

配合原子类 tailwindcss,项目采用 BEF 命名规范(Block / Element / Modifier):

  • Block:块
  • Element :元素,用 _ 连接
  • Modifier :修改器,用 __ 连接

对应到代码里的 class 命名,例如 note--empty-statenote-text--empty-state,都遵循这套约定,方便维护。

3. 「to be continue」注释大法

README 提到,在代码里写注释来规划未来要做的事 (to be continue),有利于团队协作、记忆和维护------把「要做的事情」写在注释里。在 Sidebar.js 里就能看到这样的占位注释:

css 复制代码
<section className="sidebar-menu" role="menubar">
{/*SidebarMenu*/}
</section>

以及解释区块语义的注释:

arduino 复制代码
{/* sidebar
区块 电商网站,商品介绍,评论 图片,售价...
语义是独立的一块内容区域 幻灯片 */}

八、侧边栏组件逐文件拆解

1. Sidebar.js ------ 侧边栏容器

javascript 复制代码
export default async function Sidebar() {
  const notes = await getAllNotes();
  return (
    <section className="col sidebar">
      <Link href="/" className="sidebar-header">
        <img className="logo" src="/logo.svg" width="22px" height="20px" role="presentation" />
        <strong>LLM Notes</strong>
      </Link>
      <section className="sidebar-menu" role="menubar">
        {/*SidebarMenu*/}
      </section>
      <nav>
        <SidebarNoteList notes={notes} />
      </nav>
    </section>
  )
}

知识点:

  • Sidebar 也是 async 组件 ,先用 await getAllNotes() 从后端(redis)取笔记数据。
  • 使用 next/link<Link> 做站内导航,href="/" 回到首页。
  • 图片 role="presentation" 表示纯装饰性图片,不对读屏器暴露语义。
  • <nav> 里渲染 <SidebarNoteList notes={notes} />,把取到的笔记传给子组件。

2. SidebarNoteList.js ------ 笔记列表

javascript 复制代码
export default async function SidebarNoteList({ notes }) {
  const arr = Object.entries(notes); // hash 转成二维数组 方便 map 组件
  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) 把 hash 对象转成 二维数组 [[key, value], ...],方便用 map 遍历生成组件。
  • 数组为空时渲染「No Notes created yet!」空状态。
  • 每条笔记的 value 是从 redis 里拿到的 JSON 字符串 ,所以要 JSON.parse(note) 还原成对象再传给子组件。
  • noteId 作为 key

3. SidebarNoteItem.js ------ 单条笔记

javascript 复制代码
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>
  )
}

知识点:

  • 解构笔记字段:titlecontent(默认空字符串 '')、updateTime
  • 摘要:用 content.substring(0, 20) 截取前 20 个字符,没有内容则显示 <i>(No content)</i>
  • 时间格式化:用 dayjs(updateTime).format('YYYY-MM-DD')(dayjs 是引入的日期库)。
  • 这里体现了组件复用/抽取 的思路:把列表项拆分成了 SidebarNoteItem + SidebarNoteItemContent 两层。

4. SidebarNoteItemContent.js ------ 客户端组件

javascript 复制代码
"use client";
import { useState, useEffect } from 'react';
export default function SidebarNoteItemContent({ id, title, children, expandChildren }) {
  return (
    <>
      {children}
    </>
  )
}

知识点:

  • 顶部 "use client" 标记这是一个客户端组件(区别于前面默认的服务端组件)。
  • 引入了 useStateuseEffect 两个 React Hook(虽然当前代码里还没用到)。
  • 组件接收 idtitlechildrenexpandChildren 四个 props。当前实现只渲染了 {children}expandChildren(展开后的摘要)目前还没被渲染出来------这是一个「留有扩展空间」的占位设计,正好呼应 README 里的「to be continue 注释大法」。

5. SidebarNoteList2.js ------ 对照:内联写法

项目里还有一个 SidebarNoteList2.js,它没有把单条笔记抽成组件,而是直接在 map 里内联渲染:

javascript 复制代码
arr.map(([noteId, note]) => {
  const { title, updateTime } = JSON.parse(note);
  return (
    <li key={noteId}>
      <header className="sidebar-note-header">
        <strong>{title}</strong>
        <small>{dayjs(updateTime).format('YYYY-MM-DD HH:mm:ss')}</small>
      </header>
    </li>
  )
})

对比 SidebarNoteList.js,两者的差异正好说明「组件抽取 vs 内联」的区别:

SidebarNoteList.js SidebarNoteList2.js
单条结构 抽成 SidebarNoteItem 组件 直接内联在 map 里
摘要字段 content 摘要 没有摘要,只有标题
时间格式 YYYY-MM-DD YYYY-MM-DD HH:mm:ss

抽组件的版本更利于复用和后续扩展,内联版本更简单直接。


九、数据服务:lib/redis.js(Redis)

1. 为什么选 Redis

README 介绍,这个项目的数据服务选择了 Redis

  • Redis 是一个 NOSQL 内存数据库 ,默认 6379 端口
  • 没有数据表 ,不是关系型数据库,不用 SQL 驱动 ,数据存在内存里。
  • 用法「有点像 localStorage」,直接 key: value 开搞。

它「高级的地方」在于:对不同类型的值有优化的存储方式和对应的方法

数据类型 方法
字符串 get / set
哈希 hget / hset

典型用途:缓存、计数器、榜单

2. Redis + MySQL 的缓存场景

README 用「掘金首页文章列表」举了个很直观的例子:

  • 首页文章列表「几分钟之内是不变的」。
  • 第一个用户 来访问时,查 MySQL 数据库拿到 posts 列表,以 key: value 形式存进 Redis。
  • 下一个用户再来,直接从 Redis 读取,不再查 MySQL。

这样解决了「数据可读写的 I/O 瓶颈」,用 Redis 做缓存挡在 MySQL 前面。

3. lib/redis.js 代码

javascript 复制代码
// node redis 客户端,驱动
import Redis from 'ioredis';
const redis = new Redis(); // 默认 NOSQL

// hash key 字符串ID,值 note 的序列化字符串
const initialData = {
  "1702459181837": '{"title":"sunt aut",...}',
  "1702459182837": '{"title":"qui est",...}',
  "1702459188837": '{"title":"ea molestias",...}'
}

export async function getAllNotes() {
  // hash 数据类型
  const data = await redis.hgetall('notes');
  if (Object.keys(data).length == 0) {
    await redis.hmset("notes", initialData);
  }
  return await redis.hgetall('notes');
}

知识点:

  • ioredis 作为 node 的 redis 客户端(驱动)。
  • new Redis() 连接默认配置的 Redis 实例。
  • 数据用 hash 类型 存:外层 key 是 notes,里面的 field 是「字符串 ID」,value 是「note 的序列化字符串(JSON)」。
  • initialData 是初始化用的三条种子笔记。
  • getAllNotes 逻辑:先 hgetall('notes') 取所有笔记;如果为空,就 hmset("notes", initialData) 写入初始数据;最后返回全部笔记。

这个函数正是前面 Sidebar.jsawait getAllNotes() 调用的数据来源,也印证了「lib 目录放 next.js 的数据业务逻辑」。

4. 关于接口:RPC

README 末尾还提到 /app/api/route.js ------ 接口的 RPC 远程调用(这个文件当前项目里还没实现,属于待补充的部分)。


十、总结

项目总结

这是一个基于 Next.js + Redismarkdown 笔记应用,整体架构清晰:

  • 前端展示 :Next.js 的 App Router,采用「文件即路由」,layout.js 负责左右两列布局,page.js 负责各页面。
  • 组件体系 :用「规范驱动编程」先规划再实现,按 BEF 命名规范拆分出 SidebarSidebarNoteListSidebarNoteItemSidebarNoteItemContent 的组件树;通过 "use client" 区分客户端/服务端组件。
  • 数据层 :用 ioredis 连接 Redis(NOSQL 内存数据库),用 hash 类型存储笔记(field 为 ID、value 为 JSON 字符串),lib/redis.js 统一封装数据访问。
  • 路径优化 :通过 alias 让 @/ 直达根目录,避免冗长的相对路径。

知识点总结

  1. npx :npm 自带工具,无需全局安装即可运行 node 包,npx create-next-app 快速脚手架。
  2. Next.js 核心概念 :SSR(服务端渲染)、SEO、RSC(服务端组件)、use client 客户端组件、hydration 水合。
  3. App Router 文件即路由/add POST、/note/[id] 动态路由、/edit 等。
  4. async 组件 :RSC 组件写成 async,可以 await 先去后端取数据。
  5. 规范驱动编程:开发前先分析需求、拆任务(路由 + 组件),组件是工作单元。
  6. BEF 命名规范 :Block / Element(_)/ Modifier(__),配合 tailwindcss 原子类。
  7. Object.entries :把 hash 对象转成二维数组,方便 map 渲染列表。
  8. JSON.parse:redis 存的是字符串,取出来要先 parse 成对象。
  9. dayjs :日期格式化库,format('YYYY-MM-DD') 等。
  10. ioredis + Redis :NOSQL 内存数据库(6379 端口),hash 类型用 hgetall / hmset,可做缓存、计数器、榜单,常用于 Redis + MySQL 的缓存场景。
  11. alias 配置@/components/*@/lib/*@ 直达根目录。
  12. 「to be continue」注释大法:用注释规划待做事项,利于协作与维护。

这个项目麻雀虽小、五脏俱全:从脚手架、路由、组件拆解,到数据服务和缓存设计,正好是一条完整的 Next.js 全栈开发学习路径。

相关推荐
Maxkim1 小时前
DeepSeek Harness 源码深度分析:像 VS Code 一样插件化的 Agent 框架
前端·架构
喜欢睡觉1 小时前
从零看懂一个 Next.js 笔记应用
前端
cindershade1 小时前
从零实现画布「双击创建节点」:一个 VueFlow 项目的交互全记录
前端
YHL1 小时前
🚀 SSE 服务器发送事件与 BFF 层实战
前端·后端
今日无bug1 小时前
HTML5 Canvas:从画图到游戏开发
前端·canvas
cindershade1 小时前
用 Web Workers 优化前端重计算任务:避免主线程卡顿的实战方案
前端
爱丶不疚1 小时前
搞不清 CommonJS 与 ESM,你是否也有这些疑问🤔
前端·node.js
YIAN1 小时前
http无状态?State来展示!从基础路由到鉴权守卫,吃透 SPA 前端路由核心
前端·react.js
__zRainy__1 小时前
Node系列 · Node基础:全局变量与全局对象
开发语言·前端·javascript