从 0 拆解一个 Next.js 笔记系统:npx、App Router、RSC 与组件规划全记录

从 0 拆解一个 Next.js 笔记系统:npx、App Router、RSC 与组件规划全记录

这是一份「带着问题学」的实战笔记。动手写代码前,先把三个最基础的疑问搞清楚,后面每个知识点都能落回它们身上。

一、三个课前疑问,先把地基打牢

① 什么是 npx?

一句话:npx 是 npm 自带的包执行工具,让你不全局安装就能直接运行一个 node 包。

它解决了一个很烦人的场景:很多命令行工具(比如脚手架 create-next-app)你只用一次,犯不着 npm install -g 装到全局污染环境。用 npx 就能「即用即下,用完即删」。

它的查找顺序是:本地 node_modules/.bin → 全局 → 远程临时下载

perl 复制代码
npx create-next-app my-app   # 临时下载并执行,不污染全局
npx eslint .                 # 优先跑本地装的 eslint

笔记里写的 npx = npm i -g create-next-app + create-next-app,本质就是这个意思:npx 帮你把「先全局装、再执行」这两步合并成一步,更便捷。

② 什么是 create-next-app?

一句话:create-next-app 是 React 全栈开发脚手架,用来一键生成一个能直接跑的 Next.js 项目。

运行 npx create-next-app@latest 后,它会帮你选 TypeScript、ESLint、Tailwind、App Router 等配置,然后生成目录结构、装好依赖、配好基础文件。

它背后承载的是 Next.js 的几个核心能力(下面都会用到):

缩写 全称 一句话理解
SSR Server-Side Rendering 服务器端渲染,HTML 在服务器上拼好再发给浏览器,首屏快
SEO Search Engine Optimization 因为返回的是完整 HTML,搜索引擎爬得到内容,利于收录
RSC React Server Component React 组件直接在服务器 上运行,能直接 await 拿数据
hydration 水合 服务器发来静态 HTML,浏览器再「浇」上 JS 让它变可交互

注意 use client 这行标记:加了它组件才在浏览器端跑;默认不加就是 Server Component,在服务器端跑。这是理解后面 async 的关键。

③ 页面函数为什么要写 async

先看这段代码:

javascript 复制代码
// app/page.js
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>
  )
}

为什么函数前面有 async

因为 Page 是一个 Server Component(RSC),它在服务器端运行 。加上 async 后,这个组件函数返回一个 Promise,Next.js 会等 Promise 解析完再渲染------也就是等数据就绪再出结果。

这样我们就能在组件里直接 await 拉数据

javascript 复制代码
export default async function Page() {
  const notes = await getAllNotes()  // 直接在服务端拿数据
  return <div>{notes.map(...)}</div>
}

而不用回到传统的客户端套路(useEffect + useState 去请求):

scss 复制代码
function Page() {
  const [data, setData] = useState(null)
  useEffect(() => { fetch('/api').then(r => setData(r)) }, [])
  // 有 loading 闪烁、首屏更慢、代码更啰嗦
}

一句话总结:async 让服务端组件能直接 await 后端数据,避免用 useEffect 的客户端写法。 组件注释里那句「为了 await 先取后端数据」正是这个意思。


二、项目需求:一个支持 Markdown 的笔记系统

  • 功能:笔记的 CRUD(增删改查),支持 Markdown 格式。
  • 存储与展示的分离 :数据库里存的是 Markdown 原文 ,页面上展示的是渲染后的 HTML (这里用 marked 这个库把 markdown 转成 html)。
  • 界面 :两栏布局------左侧笔记列表,右侧笔记内容

这是典型的「存原格式、展示渲染结果」设计,能保证数据源干净、可复用。


三、路由设计:App Router 的「文件即路由」

Next.js App Router 的核心思想是 目录结构 = 路由结构,是 RESTful 风格的映射:

bash 复制代码
/            → app/page.js           首页(右侧空状态)
/add  (POST) → 新增一条笔记
/note/[id]   → app/note/[id]/page.js 笔记详情(动态路由)
/edit/[id]   → 修改某条笔记
  • page.js 代表「这个路径有一个可访问的页面」。
  • [id]动态路由段 ,方括号里的 id 会作为参数传给页面组件,用来访问 /note/123/note/456 等任意 id。

路由结构图:

css 复制代码
graph TD
    ROOT["app/ 根路径 /"] --> L["layout.js 根布局(包裹所有页面)"]
    ROOT --> P["page.js 首页 /"]
    ROOT --> N["note/[id]/page.js 详情 /note/:id"]
    ROOT --> A["/add (POST) 新增"]
    ROOT --> E["edit/[id]/page.js 修改 /edit/:id"]

四、组件规划:规范驱动编程

笔记里强调一个重要的开发姿势:开发之前不要急着动手写代码

先做三件事:分析需求 → 定技术方案(Next.js)→ 拆任务细节(路由 + 组件) 。这种「规范驱动编程」把组件当成工作单元,规划好再写(也方便交给 AI 逐块生成)。

规划出的组件树:

css 复制代码
graph TD
    RootLayout["RootLayout 根布局"] --> Sidebar["Sidebar 左侧栏"]
    RootLayout --> Page["Page 右侧内容"]
    Sidebar --> SSF["SidebarSearchField 搜索框"]
    Sidebar --> EB["EditButton 编辑按钮(复用)"]
    Sidebar --> SNL["SidebarNoteList 笔记列表"]
    SNL --> NI["NoteItem 单条笔记"]
    Page --> Note["Note 笔记容器"]
    Note --> NE["NoteEditor 编辑"]
    Note --> NP["NotePreview 预览"]

要点:

  • EditButton 被标注为「复用」------搜索、列表等多处都会用到同一个编辑按钮,抽成独立组件避免重复。
  • Note 再拆成 NoteEditor(编辑)和 NotePreview(预览) ------读和写是两个职责,分开组件各自维护。

五、目录结构:四个目录各司其职

bash 复制代码
next-blog/
├── app/          # 页面主目录(路由)
│   ├── page.js
│   ├── layout.js
│   └── note/[id]/page.js
├── components/   # 组件
├── lib/          # 数据库操作、常用函数
├── public/       # 静态资源(static server,直接托管,如 logo.svg)
  • app 决定路由和页面
  • components可复用组件
  • lib数据访问逻辑(比如后面要写的 redis 操作)。
  • public静态资源,直接由静态服务器托管,不经过编译。

六、配置 alias:告别 ../../../ 地狱

动态路由页面(app/note/[id]/page.js)要引入 lib/redis.js,如果用相对路径会写成:

javascript 复制代码
import { getAllNotes } from '../../../lib/redis.js'  // 丑,还容易数错层级

通过 jsconfig.json 配置路径别名 后,就能用 @ 直接指向项目根目录:

json 复制代码
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/components/*": ["components/*"],
      "@/lib/*": ["lib/*"]
    }
  }
}

于是引入变成清爽的短链接:

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

baseUrl: "." 指定相对基准是项目根目录,paths@/lib/* 映射到 lib/*@ 直接来到根目录,路径清晰且不会因为目录层级变动而改代码。


七、布局 layout 与 SEO metadata

layout.js 是根布局,定义了整个页面的「骨架」:

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

export const metadata = {
  title: 'djz的大模型工程师博客',
  description: '这是一位未来大模型工程师的笔记......',
  keywords: ['llm', 'claude', 'deepseek', 'rag', 'langchain'],
};

export default function RootLayout({ children }) {
  return (
    <html lang="zh-CN">
      <body>
        <div className="container">
          <div className="main">
            <Sidebar />
            <section className="col note-viewer">{children}</section>
          </div>
        </div>
      </body>
    </html>
  );
}

嵌套结构对应笔记里画的:

css 复制代码
graph TD
    html --> head["head (title / meta)"]
    html --> body
    body --> container["div.container"]
    container --> main["div.main"]
    main --> Sidebar["Sidebar 侧边栏"]
    main --> viewer["section.col.note-viewer"]
    viewer --> children["children = page.js 当前页面"]

三个要点:

  1. layout.js 包裹 children{children} 就是当前路由的 page.js。切到 /note/1 时,children 就变成 note/[id]/page.js 的内容,而 Sidebar 保持不动------这正是 App Router 嵌套布局的核心价值:共享部分不重渲染
  2. metadata 导出对象是 SEO 的体现 :Next.js 会把这些字段渲染成 <head> 里的 <title><meta name="description"><meta name="keywords">。因为是服务端渲染,搜索引擎能直接爬到完整 HTML,利于收录。
  3. <html lang="zh-CN"> 声明了页面语言,对无障碍和 SEO 都有帮助。

八、其余代码逐文件解析

1. components/Sidebar.js ------ 左侧栏

javascript 复制代码
import React from 'react';
import Link from 'next/link';

export default async function Sidebar() {
  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">
          {/* SidebarSearchField 未来干 */}
        </section>
      </section>
    </>
  );
}
  • Link 来自 next/link ,是 Next.js 内置的路由跳转组件,href="/" 点击回到首页。它比原生 <a> 好在对内部路由做客户端跳转,不整页刷新。
  • role="presentation" 是 ARIA 无障碍属性,告诉读屏器这个 logo 图片只是装饰、没有语义,可忽略------所以标题 LLM Notes 才是真正有意义的内容。
  • role="menubar" 声明这是一块菜单栏容器,未来搜索框 SidebarSearchField 会放在里面(注释「未来干」就是预留的占位)。
  • 类名 sidebarsidebar-headersidebar-menu 都遵循 BEM 命名(见下一节)。

2. lib/redis.js ------ 数据层(当前是桩)

javascript 复制代码
export async function getAllNotes(){
    // 目前是空实现,后续在这里从 Redis/数据库拿笔记列表
}

它是 lib 目录的职责体现------数据操作单独收口 。页面组件只管 await getAllNotes(),具体数据从哪来、怎么查,都封装在这里,将来换成 Redis、数据库都不用改页面。

3. app/note/[id]/page.js ------ 动态路由详情页

javascript 复制代码
// alias
import { getAllNotes } from '@/lib/redis'
  • 这个文件目前只写了一行,但它身上同时演示了两个知识点 :动态路由([id])+ 路径别名(@/lib/redis)。
  • 真正的详情页会通过 params.id 拿到路由里的 id,再去查对应笔记、渲染 markdown。

4. app/page.js ------ 首页空状态(已在前文问题③解析)

类名 note--empty-state 是「块 + 修饰符」的 BEM 写法,表示「笔记这个块处于空状态」。


九、BEM 命名规范:让 CSS 类名可读、可维护

BEM 是国际通用的 CSS 命名规范,把类名拆成三部分:

部分 含义 符号 例子
Block 块(独立模块) notesidebar
Element 元素(块的组成部分) __ sidebar__header
Modifier 修饰器(状态/变体) -- note--empty-state

它解决的是「类名起得乱、看不懂谁属于谁」的问题。看到 note--empty-state 一眼就知道:这是 note 块的一个「空状态」变体。

搭配 原子类 Tailwind CSS 一起用:Tailwind 负责高频的小样式(间距、颜色),BEM 负责大块的结构化命名,两者互补维护。


十、总结:一条完整的认知链路

把整份笔记串起来,其实是一条清晰的链路:

  1. npxcreate-next-app 生成 Next.js 项目(问题①②)。
  2. Next.js 基于 App Router 的「文件即路由」和 RSC 服务端组件 (问题③的 async)。
  3. 动手前先做规范驱动编程 :拆需求 → 定方案 → 规划路由和组件
  4. layout.js 搭骨架、metadata 做 SEO、alias 管路径、BEM 管样式。
  5. 数据层收口在 lib ,页面专注 await 拿数据渲染。

这篇文章对应的是一套「先想清楚、再动手」的 Next.js 全栈开发范式,比单纯记住 API 重要得多。

相关推荐
GitLqr1 小时前
玩转 Flutter 中的 Stack 与 Positioned:解决 UI 重叠问题的实战指南
flutter·面试·全栈
hello93071 小时前
plop代码生成器
前端
windliang1 小时前
Claude Code 源码分析(十二):错误处理与自动恢复:让 Agent 稳定运行
前端·javascript·面试
fatcoder1 小时前
玩转Docker 06 — 容器网络
前端·后端·docker
打呵欠的猫1 小时前
我用 AI 重写了项目的请求层,从 800 行"面条代码"变成 3 层洋葱模型
前端·ai编程
feng尘2 小时前
深入浅出 Java:ThreadLocal 为什么会产生内存泄漏?
后端
默_笙2 小时前
🛬 前端路由的"高级玩法":懒加载、404、鉴权路由,一个都不能少(下篇)
前端·javascript
用户921080262862 小时前
1. Cesium 在 Vue 项目中的简单初始化配置
前端
YIAN2 小时前
从 Hash 底层原理到 React Router v6 实战:我学会了什么?
前端·react.js·vue-router