Next.js App Router 父子组件 RSC 拆分实战:从 Redis Hash 到客户端交互的最小化边界
学 Next.js App Router 最容易卡的两个点:① 组件什么时候该加
'use client'?② Redis 的数据怎么变成组件能用的?本文用 next-blog 的 components + lib 真实代码,把 RSC 父子拆分模式和数据转换链路一次讲透。技术栈 Next.js + React + ioredis,运行未验证(部分组件是骨架)。
文章目录
- [Next.js App Router 父子组件 RSC 拆分实战:从 Redis Hash 到客户端交互的最小化边界](#Next.js App Router 父子组件 RSC 拆分实战:从 Redis Hash 到客户端交互的最小化边界)
-
- [一、整体架构:4 层组件调用链](#一、整体架构:4 层组件调用链)
- 二、数据层:lib/redis.js
- [三、RSC 父子组件拆分](#三、RSC 父子组件拆分)
-
- 拆分思想(注释点明)
- [RSC vs 客户端组件对比](#RSC vs 客户端组件对比)
- 各组件代码解读
-
- [Sidebar.js(RSC 根)](#Sidebar.js(RSC 根))
- [SidebarNoteList.js(RSC 父)](#SidebarNoteList.js(RSC 父))
- SidebarNoteItemContent.js(客户端组件叶子)
- 四、数据转换工具函数
- [五、children 与自定义 prop](#五、children 与自定义 prop)
- 六、完整数据转换链路
- 七、收藏资产:速查表
- 总结与下一步
一、整体架构:4 层组件调用链
next-blog 的 components + lib 形成一条清晰的调用链:
text
lib/redis.js ← 数据层:连 Redis 取数据
↓
components/Sidebar.js ← RSC 根:async 取数据
↓
SidebarNoteList.js ← RSC 父:渲染列表 HTML
↓
SidebarNoteItem.js ← 中间层:组合 props
↓
SidebarNoteItemContent.js ← 客户端组件:处理交互
关键点 :前三层都是 RSC(服务端组件),只有叶子组件加 'use client'------这就是 App Router 的「最小化客户端边界」哲学。
二、数据层:lib/redis.js
连接 Redis
js
import Redis from 'ioredis';
const redis = new Redis(); // 默认 localhost:6379
new Redis() 不传参走默认值,连本机 6379 端口。
种子数据设计
js
const initialData = {
"1702459181837": '{"title":"sunt aut","content":"quia et...","updateTime":"2023-12-13T09:19:48.837Z"}',
"1702459182837": '{"title":"qui est","content":"est rerum...","updateTime":"2023-12-13T09:19:48.837Z"}',
"1702459188837": '{"title":"ea molestias","content":"et iusto...","updateTime":"2023-12-13T09:19:48.837Z"}'
}
设计思路:
- key 是时间戳字符串(笔记 ID)
- value 是 JSON 字符串(笔记对象
JSON.stringify()后的结果) - Redis 只能存字符串,对象必须序列化
懒加载初始化模式
lib/redis.js(file:///c:/Users/38335/Desktop/workspace/jzh_ai/fe/fullstack/nextjs/next-blog/lib/redis.js) 的 getAllNotes:
js
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 后第一次访问自动恢复。
为什么 hgetall 返回 JS 对象
text
Redis 服务端 网络 Node.js
──────────── ◄TCP► ────────
Hash 数据结构 RESP 文本 JS 对象
(数组+链表) *6\r\n$13... {id: json, ...}
↓
ioredis 自动解析
两道转换:① Redis 序列化成 RESP 文本 ② ioredis 反序列化成 JS 对象。进程间不能共享内存,必须序列化传输。
关键结论 :redis.hgetall() 返回的是 JS 对象,不是 Redis 的 hash 表。hash 表留在 Redis 服务端内存里。
三、RSC 父子组件拆分
拆分思想(注释点明)
SidebarNoteList.js(file:///c:/Users/38335/Desktop/workspace/jzh_ai/fe/fullstack/nextjs/next-blog/components/SidebarNoteList.js) 的注释:
js
// SidebarNoteList(RSC SEO ) -> SidebarNoteItem (交互 CSR)
// 核心思想 :父组件 RSC 取数据 + 渲染 HTML 给搜索引擎看;子组件客户端组件处理用户交互。 最小化客户端组件范围 。
核心判断:
- 父 RSC:取数据 + 渲染 HTML → SEO + 首屏快
- 子客户端组件:处理交互(点击/展开/state)
- 最小化客户端边界 :只有真正需要交互的叶子组件才加
'use client'
RSC vs 客户端组件对比
| RSC(默认) | 客户端组件 | |
|---|---|---|
| 标记 | 不写 | 文件顶部 'use client' |
| 在哪跑 | 服务器 | 浏览器 |
| async/await | ✅ | ❌ |
| useState/useEffect | ❌ | ✅ |
| onClick | ❌ | ✅ |
| SEO | ✅ | ❌ |
| JS 打包 | 不发浏览器 | 发浏览器 |
各组件代码解读
Sidebar.js(RSC 根)
js
export default async function Sidebar() {
const notes = await getAllNotes(); // async 取数据
return (
<section className="col sidebar">
...
<SidebarNoteList notes={notes} /> // 传 JS 对象给子
</section>
)
}
为什么是 RSC :要 await getAllNotes() 取数据,必须 async。组件渲染的 HTML 给搜索引擎抓取,SEO 友好。
SidebarNoteList.js(RSC 父)
js
export default async function SidebarNoteList({ notes }) {
return (
<ul className="notes-list">
{notes.map(([noteId, note]) => { // 遍历数组
return (
<li key={noteId}>
<SidebarNoteItem noteId={noteId} note={note} />
</li>
)
})}
</ul>
)
}
为什么是 RSC:渲染列表 HTML 给搜索引擎看,本身不需要交互。
SidebarNoteItemContent.js(客户端组件叶子)
js
"use client"; // ← 标记客户端组件
import { useState, useEffect } from 'react';
export default function SidebarNoteItemContent({
id, title, children, expandChildren
}) {
return <>{children}</> // 目前是骨架
}
为什么是客户端组件 :未来要加点击展开交互(useState 控制 expandChildren 显隐),所以提前标 'use client'。目前 useState/useEffect 已引入但未实际使用。
四、数据转换工具函数
Object.entries:对象转二维数组
SidebarNoteList2.js(file:///c:/Users/38335/Desktop/workspace/jzh_ai/fe/fullstack/nextjs/next-blog/components/SidebarNoteList2.js):
js
const arr = Object.entries(notes);
// { "id1": "json1", "id2": "json2" }
// → [["id1", "json1"], ["id2", "json2"]]
为什么用 :JS 对象不能直接 .map(),转成 [key, value] 二维数组才能遍历。
JSON.parse:字符串解析成对象
js
const { title, updateTime } = JSON.parse(note);
// '{"title":"sunt aut",...}' → { title: "sunt aut", ... }
为什么用:Redis 存的是 JSON 字符串,组件要用字段,必须解析成对象再解构。
dayjs:轻量日期格式化
js
dayjs(updateTime).format('YYYY-MM-DD HH:mm:ss')
// '2023-12-13T09:19:48.837Z' → '2023-12-13 09:19:48'
为什么用 dayjs 不用 moment:API 几乎兼容,但 dayjs 体积小很多(2KB vs 70KB+),大厂优先 dayjs。
五、children 与自定义 prop
SidebarNoteItem.js(file:///c:/Users/38335/Desktop/workspace/jzh_ai/fe/fullstack/nextjs/next-blog/components/SidebarNoteItem.js) 的精妙设计:
js
<SidebarNoteItemContent
id={noteId}
title={note.title}
expandChildren={<p>{content.substring(0, 20)}</p>} // 自定义 prop
>
<header> // children
<strong>{title}</strong>
<small>{dayjs(updateTime).format('YYYY-MM-DD')}</small>
</header>
</SidebarNoteItemContent>
两个 prop 的区别:
| children | expandChildren | |
|---|---|---|
| 是什么 | React 内置 prop | 自定义 prop |
| 怎么传 | 组件标签之间的内容 | 显式写在属性里 |
| 用途 | 默认显示的内容 | 展开后显示的内容 |
设计意图:把笔记拆成「头部」(children,默认显示)和「展开内容」(expandChildren,点击后显示),客户端组件可以用 useState 控制 expandChildren 显隐,实现点击展开效果。
六、完整数据转换链路
text
Redis 服务端: Hash 数据类型 (field → value)
↓ redis.hgetall() + ioredis 反序列化
Node.js: JS 对象 { id: jsonString }
↓ Object.entries()
二维数组: [[id, jsonString], ...]
↓ JSON.parse(note)
JS 对象: { title, content, updateTime }
↓ 解构 + dayjs 格式化
组件 props: title, content, formattedTime
↓ 渲染
浏览器: HTML
每一步都在变数据形态:Redis Hash → JS 对象 → 数组 → 解析后的对象 → 组件 props。理解这条链路就掌握了 App Router 数据流的核心。
七、收藏资产:速查表
RSC 拆分原则
text
能用 RSC 就用 RSC(取数据+SEO+首屏快)
只有需要以下功能才加 'use client':
- useState / useReducer(状态)
- useEffect(副作用)
- onClick / onChange(事件)
- window / localStorage(浏览器 API)
判断口诀:纯展示用 RSC,要交互才用客户端组件
数据转换工具
text
Object.entries(obj) → 对象转二维数组,让对象能 map
JSON.parse(str) → JSON 字符串转对象
JSON.stringify(obj) → 对象转 JSON 字符串(存 Redis 时用)
dayjs(t).format(fmt) → 格式化时间
组件调用链速记
text
Sidebar (RSC 取数据)
→ SidebarNoteList (RSC 渲染列表)
→ SidebarNoteItem (中间层组合)
→ SidebarNoteItemContent (客户端组件交互)
总结与下一步
App Router 的拆分哲学是「最小化客户端边界」------父 RSC 取数据+渲染 HTML 给搜索引擎看,只有真正需要交互的叶子组件才加 'use client'。Redis Hash 通过 ioredis 自动转成 JS 对象,中间用 Object.entries 转数组、JSON.parse 解析字符串、dayjs 格式化时间,每一步都是数据形态转换。
可迁移的判断 :组件该不该加 'use client',就看它要不要交互------要就加,不要就留 RSC。这个判断适用于任何 App Router 项目。
下一步:
- 给 SidebarNoteItemContent.js 加真实交互(useState 控制 expandChildren 显隐)
- 修复 SidebarNoteList.js 和 SidebarNoteList2.js 的不一致
- 研究客户端组件边界效应('use client' 后子组件自动是客户端组件)
运行未验证:本文基于 next-blog 项目静态代码分析,未实际
npm run dev。生产环境请以 Next.js 官方文档为准。
标签:Next.js, App Router, React Server Component, RSC 拆分, Redis