Next.js App Router 父子组件 RSC 拆分实战:从 Redis Hash 到客户端交互的最小化边界

Next.js App Router 父子组件 RSC 拆分实战:从 Redis Hash 到客户端交互的最小化边界

学 Next.js App Router 最容易卡的两个点:① 组件什么时候该加 'use client'?② Redis 的数据怎么变成组件能用的?本文用 next-blog 的 components + lib 真实代码,把 RSC 父子拆分模式和数据转换链路一次讲透。技术栈 Next.js + React + ioredis,运行未验证(部分组件是骨架)。

文章目录

一、整体架构: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

相关推荐
丫头,冲鸭!!!1 小时前
记账网站-post功能
javascript·个人开发
鬼手点金1 小时前
Scrapy + Playwright 完整示例(JS 动态渲染网页)
开发语言·javascript·爬虫·python·scrapy·html·json
逝水无殇2 小时前
HTML 文本格式化详解:掌握 <strong>、<em>、<mark>、<del> 等常用标签
前端·javascript·html
光影少年2 小时前
react离线缓存、图片缓存方案
开发语言·前端·javascript·react native·react.js·缓存·前端框架
我命由我1234510 小时前
CesiumJS 笔记 - 获取容器中心点、Cartesian3 clone 方法、修改 Cartesian3 对象的高度
前端·javascript·css·前端框架·html·html5·js
晚染烟11 小时前
每日八股day17
redis
抱抱宝11 小时前
Agent-study项目教程(03):手写 Mini-ReAct Agent(不依赖框架)
javascript·人工智能·gpt·react.js·prompt·agent
用户9385156350712 小时前
Next.js 笔记系统(二):Redis 数据服务与侧边栏组件拆分实战
javascript·全栈
Heo16 小时前
大厂前端调试不能只会debugger
前端·javascript·面试