Appearance
第9章 React Server Components(Flight协议)
本章将深入React 19的革命性特性——React Server Components(RSC),理解Flight协议的设计、服务端组件的渲染机制、客户端组件引用、Flight数据格式和Chunk类型。这是理解React全栈架构的核心章节。
在React 19之前,所有React组件都运行在客户端,即使使用SSR,组件代码也需要打包发送到浏览器。这导致了两个问题:一是JavaScript包体积庞大,二是无法直接访问服务端资源(数据库、文件系统等)。React Server Components(RSC)彻底改变了这一局面,允许组件只在服务端运行,零Bundle Size,直接访问后端资源。
为什么需要RSC?Flight协议是如何工作的?服务端组件如何渲染?客户端组件如何引用?Flight数据格式是什么样的?Chunk类型有哪些?
本章将逐一解答这些问题,带你深入理解React Server Components的设计与实现。
9.1 RSC设计理念
React Server Components是React 19最重要的特性,它重新定义了React组件的运行边界。
9.1.1 为什么需要RSC
在传统的React应用中,所有组件都在客户端运行,这带来了一些根本性的限制。
传统React的限制
jsx
// 传统React组件 - 运行在客户端
function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
// 问题1:需要通过API获取数据
fetch('/api/products')
.then(res => res.json())
.then(setProducts);
}, []);
return (
<div>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// 问题2:ProductCard组件代码必须发送到客户端
// 问题3:如果使用了大型库(如markdown解析器),也会增加包体积RSC的解决方案
jsx
// Server Component - 运行在服务端
async function ProductList() {
// 优势1:直接访问数据库,无需API
const products = await db.query('SELECT * FROM products');
// 优势2:可以使用任何服务端库,不增加客户端包体积
const marked = require('marked');
return (
<div>
{products.map(product => (
// 优势3:ProductCard如果也是Server Component,
// 它的代码不会发送到客户端
<ProductCard key={product.id} product={product} />
))}
</div>
);
}RSC的核心优势
- 零Bundle Size:Server Component的代码不会打包到客户端
- 直接访问后端资源:可以直接读取数据库、文件系统、环境变量
- 自动代码分割:每个Client Component自动成为代码分割点
- 更好的性能:减少客户端JavaScript,加快页面加载
- 更好的安全性:敏感逻辑(API密钥、数据库查询)只在服务端运行
对比表格
| 特性 | 传统React组件 | Server Component | Client Component |
|---|---|---|---|
| 运行位置 | 客户端 | 服务端 | 客户端 |
| 包体积 | 计入 | 不计入 | 计入 |
| 数据获取 | 通过API | 直接访问数据库 | 通过API |
| 状态管理 | 支持useState | 不支持 | 支持useState |
| 生命周期 | 支持useEffect | 不支持 | 支持useEffect |
| 异步组件 | 不支持 | 支持async/await | 不支持 |
| 服务端库 | 不可用 | 可用 | 不可用 |
9.1.2 零Bundle Size的组件
Server Component最大的特点是"零Bundle Size"——它的代码完全不会发送到客户端。
示例:Markdown渲染器
jsx
// ServerMarkdown.js - Server Component
import marked from 'marked'; // 这个库不会打包到客户端!
async function ServerMarkdown({ filePath }) {
// 直接读取服务器文件系统
const content = await fs.readFile(filePath, 'utf-8');
// 使用marked库解析(marked约50KB,但不会发送到客户端)
const html = marked(content);
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}传统方案的对比
jsx
// 传统方案 - 所有代码都在客户端
import marked from 'marked'; // 50KB打包到客户端
function ClientMarkdown({ content }) {
const html = marked(content); // 在客户端解析
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
// 包体积:50KB(marked) + 组件代码jsx
// RSC方案 - 只发送渲染结果
// ServerMarkdown.js(不会打包)
import marked from 'marked'; // 只在服务端使用
async function ServerMarkdown({ filePath }) {
const content = await fs.readFile(filePath, 'utf-8');
const html = marked(content);
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
// 包体积:0KB(只发送HTML结果)零Bundle Size的实现原理
Server Component的代码不会打包的原因是:
- 组件在服务端执行,返回的是渲染结果(React Element树)
- 渲染结果通过Flight协议序列化
- 客户端接收的是序列化后的数据,不是组件代码
- 客户端只需要重建React Element树,不需要执行组件函数
9.1.3 服务端与客户端的边界
在RSC架构中,组件分为两类:Server Component和Client Component。理解它们的边界至关重要。
组件类型标记
jsx
// Server Component(默认)
// 文件:app/ProductList.js
async function ProductList() {
const products = await db.query('SELECT * FROM products');
return <div>{/* ... */}</div>;
}
// Client Component(需要"use client"指令)
// 文件:app/AddToCart.js
'use client'; // 标记为Client Component
import { useState } from 'react';
function AddToCart({ productId }) {
const [count, setCount] = useState(1);
return (
<button onClick={() => setCount(count + 1)}>
Add {count} to cart
</button>
);
}组件边界规则
- Server Component可以导入Server Component
- Server Component可以导入Client Component
- Client Component只能导入Client Component
- Client Component不能导入Server Component(但可以通过props接收)
正确的组合方式
jsx
// ✅ 正确:Server Component渲染Client Component
// ProductPage.js - Server Component
import AddToCart from './AddToCart'; // Client Component
async function ProductPage({ id }) {
const product = await db.getProduct(id);
return (
<div>
<h1>{product.name}</h1>
<AddToCart productId={id} /> {/* 可以渲染Client Component */}
</div>
);
}jsx
// ❌ 错误:Client Component不能直接导入Server Component
// AddToCart.js - Client Component
'use client';
import ProductDetails from './ProductDetails'; // Server Component
function AddToCart() {
return (
<div>
<ProductDetails /> {/* 错误! */}
</div>
);
}jsx
// ✅ 正确:通过props传递Server Component
// AddToCart.js - Client Component
'use client';
function AddToCart({ children }) {
return (
<div>
{children} {/* 可以接收Server Component作为children */}
</div>
);
}
// ProductPage.js - Server Component
async function ProductPage({ id }) {
const product = await db.getProduct(id);
return (
<AddToCart>
<ProductDetails product={product} /> {/* Server Component */}
</AddToCart>
);
}组件边界图解
为什么有这些限制?
- Client Component不能导入Server Component:因为Client Component在客户端运行,而Server Component的代码不会发送到客户端
- 可以通过props传递:Server Component可以先渲染,然后将结果作为props传递给Client Component
- "use client"是边界标记:它告诉打包工具这个模块及其依赖需要打包到客户端
9.2 Flight协议概述
Flight协议是React Server Components的通信协议,负责将服务端渲染的组件树序列化并传输到客户端。
9.2.1 什么是Flight协议
Flight协议是React团队设计的一种轻量级序列化协议,用于在服务端和客户端之间传输React组件树。
为什么需要Flight协议?
传统的JSON序列化无法满足RSC的需求:
- 无法表示React Element:React Element包含$$typeof等Symbol,JSON无法序列化
- 无法表示模块引用:Client Component需要引用客户端模块
- 无法表示Promise:异步组件需要表示pending状态
- 无法表示循环引用:组件树可能有循环引用
- 无法流式传输:JSON需要完整对象才能解析
Flight协议解决了这些问题,提供了一种专门为React设计的序列化格式。
Flight协议的特点
- 行格式协议:每行是一个独立的数据块(Chunk)
- 流式传输:可以边生成边发送,无需等待完整数据
- 支持引用:通过ID引用其他Chunk,避免重复
- 支持模块引用:可以引用客户端模块
- 支持Promise:可以表示异步数据的pending/resolved状态
- 紧凑高效:比JSON更紧凑,解析更快
9.2.2 协议格式详解
Flight协议是基于文本的行格式协议,每行代表一个Chunk。
基本格式
<id>:<type>:<data>\n- id:Chunk的唯一标识符(数字或字符串)
- type:Chunk类型标识符(单个字符)
- data:Chunk的数据(JSON格式)
示例:简单的Flight数据
0:["$","div",null,{"children":"Hello World"}]解析:
- id: 0
- type: 无(默认为Model Chunk)
- data:
["$","div",null,{"children":"Hello World"}]"$": 表示这是一个React Element"div": 元素类型null: key{"children":"Hello World"}: props
示例:包含Client Component引用
1:I{"id":"./src/AddToCart.js","chunks":["client1"],"name":"default"}
2:["$","div",null,{"children":[["$","h1",null,{"children":"Product"}],["$","@1",null,{"productId":123}]]}]解析:
- 行1:定义Client Component引用
- id: 1
- type: I (Import/Module Reference)
- data: 模块信息(文件路径、chunk、导出名)
- 行2:使用引用
"@1": 引用id为1的Chunk(Client Component)
9.2.3 与JSON的区别
让我们通过对比来理解Flight协议的优势。
JSON序列化的问题
jsx
// Server Component
function ProductList() {
return (
<div>
<h1>Products</h1>
<AddToCart productId={123} /> {/* Client Component */}
</div>
);
}
// 尝试用JSON序列化(不可行)
const json = JSON.stringify({
type: 'div',
props: {
children: [
{ type: 'h1', props: { children: 'Products' } },
// 问题:如何表示AddToCart这个Client Component?
// 不能发送组件代码,也不能只发送数据
{ type: AddToCart, props: { productId: 123 } } // ❌ 函数无法序列化
]
}
});Flight协议的解决方案
1:I{"id":"./src/AddToCart.js","chunks":["client1"],"name":"default"}
2:["$","div",null,{"children":[["$","h1",null,{"children":"Products"}],["$","@1",null,{"productId":123}]]}]客户端解析时:
- 看到
@1,知道这是一个模块引用 - 查找id为1的Chunk,获取模块信息
- 动态导入
./src/AddToCart.js - 用导入的组件替换
@1
对比表格
| 特性 | JSON | Flight协议 |
|---|---|---|
| React Element | ❌ 无法表示Symbol | ✅ 使用"$"标记 |
| 模块引用 | ❌ 无法表示 | ✅ 使用"@id"引用 |
| Promise | ❌ 无法表示 | ✅ 支持pending/resolved |
| 循环引用 | ❌ 报错 | ✅ 通过引用解决 |
| 流式传输 | ❌ 需要完整对象 | ✅ 逐行解析 |
| 大小 | 较大 | 更紧凑 |
9.3 ReactFlightServer源码分析
ReactFlightServer是服务端Flight协议的实现,负责将React组件树序列化为Flight格式。
9.3.1 renderToReadableStream入口
renderToReadableStream是Flight服务端的主要入口函数。
javascript
// 文件:packages/react-server/src/ReactFlightServer.js
// 行号:1850-1900(React 19.3.0)
export function renderToReadableStream(
model: ReactClientValue,
webpackMap: ClientManifest,
options?: Options,
): ReadableStream {
// 1. 创建Request对象
const request = createRequest(
model,
webpackMap,
options ? options.onError : undefined,
options ? options.context : undefined,
options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined,
);
// 2. 开始渲染
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
} else {
const listener = () => {
abort(request, (signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
// 3. 创建ReadableStream
const stream = new ReadableStream({
type: 'bytes',
start(controller): ?Promise<void> {
request.destination = controller;
startWork(request);
},
});
return stream;
}参数说明
- model:要序列化的React组件树(通常是根组件)
- webpackMap:客户端模块映射表(Client Manifest)
- 包含所有Client Component的模块信息
- 由打包工具(Webpack/Vite)生成
- options:配置选项
- onError:错误处理回调
- context:服务端上下文
- identifierPrefix:ID前缀
- onPostpone:延迟渲染回调
- signal:AbortSignal,用于取消渲染
返回值
返回一个Web Streams API的ReadableStream,可以:
- 通过Response发送给客户端
- 管道到其他流
- 逐块读取数据
9.3.2 Request对象创建
Request对象是Flight渲染的核心状态容器。
javascript
// 文件:packages/react-server/src/ReactFlightServer.js
// 行号:650-750(React 19.3.0,简化版)
function createRequest(
model: ReactClientValue,
bundlerConfig: ClientManifest,
onError: void | ((error: mixed) => ?string),
context?: Array<[string, ServerContextJSONValue]>,
identifierPrefix?: string,
onPostpone: void | ((reason: string) => void),
): Request {
// 1. 创建Chunk缓冲区
const chunks: Array<Chunk> = [];
const abortSet: Set<Task> = new Set();
// 2. 创建Request对象
const request: Request = {
status: OPENING,
flushScheduled: false,
fatalError: null,
destination: null,
bundlerConfig,
cache: new Map(),
nextChunkId: 0,
pendingChunks: 0,
hints: createHints(),
abortableTasks: abortSet,
pingedTasks: [],
completedImportChunks: [],
completedHintChunks: [],
completedRegularChunks: [],
completedErrorChunks: [],
writtenSymbols: new Map(),
writtenClientReferences: new Map(),
writtenServerReferences: new Map(),
writtenObjects: new WeakMap(),
identifierPrefix: identifierPrefix || '',
identifierCount: 1,
onError: onError === undefined ? defaultOnError : onError,
onPostpone: onPostpone === undefined ? defaultOnPostpone : onPostpone,
toJSON: function(key: string, value: ReactClientValue): ReactJSONValue {
return resolveModelToJSON(request, this, key, value);
},
};
// 3. 初始化上下文
if (context) {
for (let i = 0; i < context.length; i++) {
const [name, value] = context[i];
// 设置服务端上下文
}
}
// 4. 创建根Task
const rootTask = createTask(
request,
model,
null,
false,
abortSet,
);
request.pingedTasks.push(rootTask);
return request;
}Request对象的关键属性
状态管理
- status:渲染状态(OPENING/CLOSING/CLOSED)
- fatalError:致命错误
- destination:输出目标(ReadableStream的controller)
Chunk管理
- nextChunkId:下一个Chunk的ID
- pendingChunks:待处理的Chunk数量
- completedImportChunks:完成的Import Chunk队列
- completedRegularChunks:完成的普通Chunk队列
- completedErrorChunks:完成的Error Chunk队列
缓存与映射
- cache:组件缓存
- writtenSymbols:已写入的Symbol映射
- writtenClientReferences:已写入的Client Reference映射
- writtenServerReferences:已写入的Server Reference映射
- writtenObjects:已写入的对象映射(WeakMap)
配置
- bundlerConfig:打包工具配置(Client Manifest)
- identifierPrefix:ID前缀
- onError/onPostpone:回调函数
9.3.3 组件树的序列化
组件树的序列化是Flight的核心流程,通过递归遍历组件树,将每个节点转换为Flight格式。
序列化流程
javascript
// 文件:packages/react-server/src/ReactFlightServer.js
// 简化版
function resolveModelToJSON(
request: Request,
parent: {+[key: string | number]: ReactClientValue} | $ReadOnlyArray<ReactClientValue>,
key: string,
value: ReactClientValue,
): ReactJSONValue {
// 1. 处理特殊值
if (value === null) {
return null;
}
if (typeof value === 'object') {
// 2. 检查是否已序列化(避免循环引用)
const existingReference = request.writtenObjects.get(value);
if (existingReference !== undefined) {
// 返回引用
return serializeByValueID(existingReference);
}
// 3. 处理React Element
if (isValidElement(value)) {
return serializeReactElement(request, parent, key, value);
}
// 4. 处理Client Reference
if (isClientReference(value)) {
return serializeClientReference(request, parent, key, value);
}
// 5. 处理Promise
if (typeof value.then === 'function') {
return serializeThenable(request, value);
}
// 6. 处理普通对象/数组
return value;
}
// 7. 处理基本类型
if (typeof value === 'string') {
return escapeStringValue(value);
}
if (typeof value === 'number' || typeof value === 'boolean') {
return value;
}
if (typeof value === 'undefined') {
return serializeUndefined();
}
// 8. 处理函数(Server Action)
if (typeof value === 'function') {
return serializeServerReference(request, parent, key, value);
}
// 9. 处理Symbol
if (typeof value === 'symbol') {
return serializeSymbol(request, value);
}
return undefined;
}序列化流程图
9.4 服务端组件渲染
服务端组件的渲染过程与客户端组件有本质区别,它直接执行组件函数并序列化结果。
9.4.1 Server Component的识别
React如何区分Server Component和Client Component?
识别机制
- 默认是Server Component:在服务端环境中,所有组件默认是Server Component
- "use client"标记Client Component:文件顶部的"use client"指令标记该模块为Client Component
- 打包工具处理:Webpack/Vite插件会:
- 扫描所有文件,找到"use client"指令
- 为Client Component生成模块引用
- 生成Client Manifest(模块映射表)
Client Reference的数据结构
javascript
// 文件:packages/react-server/src/ReactFlightServer.js
type ClientReference<T> = {
$$typeof: symbol, // REACT_CLIENT_REFERENCE_TYPE
$$id: string, // 模块ID(文件路径)
$$async: boolean, // 是否异步模块
};
// 示例
const AddToCartReference = {
$$typeof: Symbol.for('react.client.reference'),
$$id: './src/AddToCart.js#default',
$$async: false,
};isClientReference检查
javascript
// 文件:packages/react-server/src/ReactFlightServer.js
function isClientReference(reference: mixed): boolean {
return (
typeof reference === 'object' &&
reference !== null &&
reference.$$typeof === REACT_CLIENT_REFERENCE_TYPE
);
}9.4.2 组件执行与数据获取
Server Component可以直接执行异步操作,这是它的核心优势之一。
同步Server Component
jsx
// ProductList.js - Server Component
function ProductList({ category }) {
// 注意:这里不能使用异步操作
// 需要在外部获取数据后传入
return (
<div>
<h1>{category}</h1>
{/* ... */}
</div>
);
}异步Server Component(React 19新特性)
jsx
// ProductList.js - Async Server Component
async function ProductList({ category }) {
// ✅ 可以直接使用async/await
const products = await db.query(
'SELECT * FROM products WHERE category = ?',
[category]
);
// ✅ 可以并行获取多个数据源
const [reviews, inventory] = await Promise.all([
fetch(`/api/reviews?category=${category}`).then(r => r.json()),
db.query('SELECT * FROM inventory WHERE category = ?', [category]),
]);
return (
<div>
<h1>{category}</h1>
{products.map(product => (
<ProductCard
key={product.id}
product={product}
reviews={reviews[product.id]}
stock={inventory[product.id]}
/>
))}
</div>
);
}async组件的执行流程
javascript
// 文件:packages/react-server/src/ReactFlightServer.js
// 简化版
function renderElement(
request: Request,
task: Task,
type: any,
props: Object,
): void {
// 1. 执行组件函数
const result = type(props);
// 2. 检查是否返回Promise
if (typeof result === 'object' && typeof result.then === 'function') {
// 3. 处理async组件
result.then(
(resolvedResult) => {
// Promise resolved,继续渲染
renderModel(request, task, resolvedResult);
},
(error) => {
// Promise rejected,报告错误
reportError(request, error);
}
);
} else {
// 4. 同步组件,直接渲染结果
renderModel(request, task, result);
}
}9.4.3 async组件的处理
async组件是React 19的重要特性,它允许组件函数返回Promise。
Thenable的序列化
javascript
// 文件:packages/react-server/src/ReactFlightServer.js
// 简化版
function serializeThenable(
request: Request,
thenable: Thenable<any>,
): string {
// 1. 创建新的Chunk ID
const newTask = createTask(
request,
null,
null,
false,
request.abortableTasks,
);
const ping = newTask.ping;
// 2. 监听Promise状态
thenable.then(
(value) => {
// Promise resolved
newTask.model = value;
ping();
},
(error) => {
// Promise rejected
newTask.status = ERRORED;
newTask.model = error;
ping();
}
);
// 3. 返回引用
return serializeByValueID(newTask.id);
}Promise的Flight表示
// 初始状态:Promise pending
1:["$","@2",null,{"children":"Loading..."}]
// Promise resolved后,发送新的Chunk
2:["$","div",null,{"children":"Loaded content"}]9.4.4 示例:async Server Component
让我们通过一个完整的例子理解async Server Component的工作流程。
服务端代码
jsx
// app/products/[category]/page.js
async function ProductPage({ params }) {
// 1. 并行获取多个数据源
const [products, categories, featured] = await Promise.all([
db.query('SELECT * FROM products WHERE category = ?', [params.category]),
db.query('SELECT * FROM categories'),
fetch('https://api.example.com/featured').then(r => r.json()),
]);
return (
<div>
<Sidebar categories={categories} />
<main>
<h1>{params.category}</h1>
<FeaturedBanner items={featured} />
<ProductGrid products={products} />
</main>
</div>
);
}
// Sidebar.js - Server Component
function Sidebar({ categories }) {
return (
<aside>
{categories.map(cat => (
<a key={cat.id} href={`/products/${cat.slug}`}>
{cat.name}
</a>
))}
</aside>
);
}
// ProductGrid.js - Server Component
function ProductGrid({ products }) {
return (
<div className="grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// ProductCard.js - 包含Client Component
function ProductCard({ product }) {
return (
<div>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
{/* Client Component用于交互 */}
<AddToCartButton productId={product.id} />
</div>
);
}生成的Flight数据
1:I{"id":"./src/AddToCartButton.js","chunks":["client1"],"name":"default"}
2:["$","div",null,{"children":[["$","aside",null,{"children":[["$","a","cat1",{"href":"/products/electronics","children":"Electronics"}],["$","a","cat2",{"href":"/products/books","children":"Books"}]]}],["$","main",null,{"children":[["$","h1",null,{"children":"Electronics"}],["$","div","featured",{"children":"..."}],["$","div","grid",{"className":"grid","children":[["$","div","p1",{"children":[["$","img",null,{"src":"/img/laptop.jpg","alt":"Laptop"}],["$","h3",null,{"children":"Laptop"}],["$","p",null,{"children":"$999"}],["$","@1",null,{"productId":1}]]}]]}]]}]]}]执行时序图
9.5 客户端组件引用
Client Component在Flight协议中以模块引用的形式传输,客户端动态加载这些模块。
9.5.1 Client Reference的创建
当Server Component渲染Client Component时,不会执行Client Component的代码,而是创建一个引用。
Client Reference的序列化
javascript
// 文件:packages/react-server/src/ReactFlightServer.js
// 简化版
function serializeClientReference(
request: Request,
parent: {+[key: string | number]: ReactClientValue} | $ReadOnlyArray<ReactClientValue>,
key: string,
clientReference: ClientReference<any>,
): string {
// 1. 获取模块ID
const moduleId = clientReference.$$id;
// 2. 检查是否已写入
const existingId = request.writtenClientReferences.get(clientReference);
if (existingId !== undefined) {
// 已写入,返回引用
return serializeByValueID(existingId);
}
// 3. 创建新的Chunk ID
const importId = request.nextChunkId++;
request.pendingChunks++;
// 4. 记录已写入
request.writtenClientReferences.set(clientReference, importId);
// 5. 从bundlerConfig获取模块信息
const moduleMetadata = resolveClientReferenceMetadata(
request.bundlerConfig,
clientReference,
);
// 6. 写入Import Chunk
emitImportChunk(request, importId, moduleMetadata);
// 7. 返回引用
return serializeByValueID(importId);
}emitImportChunk:输出Import Chunk
javascript
// 文件:packages/react-server/src/ReactFlightServer.js
function emitImportChunk(
request: Request,
id: number,
moduleMetadata: ModuleMetadata,
): void {
// 构造Import Chunk
const json = stringify({
id: moduleMetadata.id, // 模块路径
chunks: moduleMetadata.chunks, // 依赖的chunk
name: moduleMetadata.name, // 导出名(default/named)
});
// 格式:<id>:I<json>\n
const row = id.toString(16) + ':I' + json + '\n';
// 添加到completedImportChunks队列
request.completedImportChunks.push(stringToChunk(row));
}9.5.2 模块引用的序列化
模块引用包含了客户端加载模块所需的所有信息。
ModuleMetadata数据结构
javascript
type ModuleMetadata = {
id: string, // 模块ID(文件路径)
chunks: Array<string>, // 依赖的webpack chunk
name: string, // 导出名
async?: boolean, // 是否异步模块
};
// 示例
const metadata = {
id: './src/components/AddToCart.js',
chunks: ['client1', 'client-common'],
name: 'default',
async: false,
};Import Chunk示例
5:I{"id":"./src/AddToCart.js","chunks":["client1","client-common"],"name":"default"}解析:
- id: 5(Chunk ID)
- type: I(Import Chunk)
- data: 模块元数据
- id: 模块路径
- chunks: 需要加载的webpack chunk
- name: 导出名(default表示默认导出)
9.5.3 Webpack插件的作用
Webpack插件负责生成Client Manifest,这是Flight协议的关键基础设施。
Client Manifest的生成
javascript
// Webpack插件(简化版)
class ReactFlightWebpackPlugin {
apply(compiler) {
compiler.hooks.compilation.tap('ReactFlightWebpackPlugin', (compilation) => {
// 1. 扫描所有模块
compilation.hooks.afterOptimizeModules.tap('ReactFlightWebpackPlugin', (modules) => {
const clientManifest = {};
for (const module of modules) {
// 2. 检查是否有"use client"指令
if (module.resource && hasUseClientDirective(module)) {
// 3. 记录模块信息
const moduleId = getModuleId(module);
const chunks = getChunksForModule(module, compilation);
clientManifest[moduleId] = {
id: moduleId,
chunks: chunks.map(c => c.id),
name: 'default', // 或解析具体的导出名
};
}
}
// 4. 输出Client Manifest文件
compilation.assets['react-client-manifest.json'] = {
source: () => JSON.stringify(clientManifest),
size: () => JSON.stringify(clientManifest).length,
};
});
});
}
}Client Manifest示例
json
{
"./src/components/AddToCart.js": {
"id": "./src/components/AddToCart.js",
"chunks": ["client1"],
"name": "default"
},
"./src/components/SearchBar.js": {
"id": "./src/components/SearchBar.js",
"chunks": ["client2", "client-common"],
"name": "default"
},
"./src/components/Modal.js": {
"id": "./src/components/Modal.js",
"chunks": ["client3"],
"name": "Modal"
}
}使用Client Manifest
javascript
// 服务端使用
import clientManifest from './dist/react-client-manifest.json';
const stream = renderToReadableStream(
<App />,
clientManifest, // 传入Client Manifest
);9.6 Flight数据格式
Flight协议使用行格式,每行是一个独立的Chunk,支持流式传输和解析。
9.6.1 行格式协议
每行的基本格式:<id>:<type><data>\n
格式说明
id:Chunk的唯一标识符
- 十六进制数字(如:0, 1, a, ff)
- 或字符串(如:S1, E2)
type:单字符类型标识符
- 无类型标识:Model Chunk(默认)
I:Import Chunk(模块引用)E:Error Chunk(错误)T:Hint Chunk(提示,如preload)D:Debug Chunk(调试信息)
data:JSON格式的数据
- 必须是有效的JSON
- 可以包含特殊标记(如
$、@)
9.6.2 类型标识符
Flight协议使用特殊标记来表示不同类型的值。
React Element标记:$
0:["$","div",null,{"children":"Hello"}]"$": 表示这是一个React Element"div": 元素类型null: key{"children":"Hello"}: props
引用标记:@
1:I{"id":"./Button.js","chunks":["client1"],"name":"default"}
2:["$","@1",null,{"onClick":"..."}]"@1": 引用id为1的Chunk- 客户端解析时会用实际的模块替换
@1
Symbol标记:S
S1:"react.suspense"
3:["$","$S1",null,{"fallback":"Loading...","children":"..."}]S1: Symbol的ID"react.suspense": Symbol的描述"$S1": 引用Symbol
Undefined标记:$undefined
0:{"value":"$undefined"}- JSON无法表示undefined
- 使用特殊字符串
"$undefined"
9.6.3 引用与内联
Flight协议支持两种数据表示方式:引用和内联。
内联方式
0:["$","div",null,{"children":[["$","h1",null,{"children":"Title"}],["$","p",null,{"children":"Content"}]]}]- 所有数据都在一行中
- 适合小型、简单的结构
- 无法复用
引用方式
1:["$","h1",null,{"children":"Title"}]
2:["$","p",null,{"children":"Content"}]
0:["$","div",null,{"children":["@1","@2"]}]- 子元素单独定义为Chunk
- 通过
@id引用 - 可以复用(多处引用同一个Chunk)
- 支持循环引用
9.6.4 示例:Flight数据解析
让我们通过一个完整的例子理解Flight数据格式。
React组件树
jsx
function App() {
return (
<div>
<Header />
<main>
<ProductList />
<AddToCartButton productId={123} />
</main>
</div>
);
}
function Header() {
return <header><h1>My Store</h1></header>;
}
function ProductList() {
return <div>Products...</div>;
}
// Client Component
'use client';
function AddToCartButton({ productId }) {
return <button>Add to Cart</button>;
}生成的Flight数据
1:I{"id":"./src/AddToCartButton.js","chunks":["client1"],"name":"default"}
2:["$","header",null,{"children":["$","h1",null,{"children":"My Store"}]}]
3:["$","div",null,{"children":"Products..."}]
4:["$","@1",null,{"productId":123}]
5:["$","main",null,{"children":["@3","@4"]}]
0:["$","div",null,{"children":["@2","@5"]}]逐行解析
行1:Import Chunk
- 定义Client Component引用
- id=1, 模块路径=
./src/AddToCartButton.js
行2:Header组件的渲染结果
- id=2, React Element:
<header><h1>My Store</h1></header>
- id=2, React Element:
行3:ProductList组件的渲染结果
- id=3, React Element:
<div>Products...</div>
- id=3, React Element:
行4:AddToCartButton的引用
- id=4, 引用
@1(Client Component) - props:
{productId: 123}
- id=4, 引用
行5:main元素
- id=5, 包含
@3和@4两个子元素
- id=5, 包含
行0:根元素
- id=0, 包含
@2和@5两个子元素
- id=0, 包含
数据流图
9.7 Chunk类型详解
Flight协议定义了多种Chunk类型,每种类型有特定的用途和格式。
9.7.1 Module Chunk
Module Chunk(也叫Model Chunk)是最常见的Chunk类型,表示普通的数据或React Element。
格式
<id>:<json>\n注意:没有类型标识符,直接是JSON数据。
示例
// React Element
0:["$","div",null,{"className":"container","children":"Hello"}]
// 普通对象
1:{"name":"John","age":30}
// 数组
2:[1,2,3,4,5]
// 字符串
3:"Hello World"
// 数字
4:42
// 布尔值
5:true
// null
6:nullReact Element的格式
["$", type, key, props]"$": 固定标记,表示React Elementtype: 元素类型(字符串或引用)key: React key(可以是null)props: 属性对象
9.7.2 Symbol Chunk
Symbol Chunk用于表示JavaScript Symbol,因为Symbol无法直接序列化为JSON。
格式
S<id>:"<description>"\n示例
S1:"react.element"
S2:"react.suspense"
S3:"react.fragment"使用Symbol
// 定义Symbol
S1:"react.suspense"
// 使用Symbol作为type
0:["$","$S1",null,{"fallback":"Loading...","children":"..."}]"$S1": 引用id为S1的Symbol- 客户端会用实际的Symbol替换
常见的React Symbol
javascript
// 文件:packages/shared/ReactSymbols.js
export const REACT_ELEMENT_TYPE = Symbol.for('react.element');
export const REACT_FRAGMENT_TYPE = Symbol.for('react.fragment');
export const REACT_SUSPENSE_TYPE = Symbol.for('react.suspense');
export const REACT_SUSPENSE_LIST_TYPE = Symbol.for('react.suspense_list');
export const REACT_LAZY_TYPE = Symbol.for('react.lazy');9.7.3 Model Chunk
Model Chunk是Module Chunk的别名,表示普通的数据模型。
用途
- 表示React Element
- 表示普通JavaScript对象
- 表示数组、字符串、数字等基本类型
- 表示引用(通过
@id)
复杂示例
// 嵌套对象
0:{"user":{"name":"John","profile":{"age":30,"city":"NYC"}},"posts":["@1","@2"]}
// 引用的posts
1:{"id":1,"title":"Post 1"}
2:{"id":2,"title":"Post 2"}9.7.4 Error Chunk
Error Chunk用于表示渲染过程中发生的错误。
格式
<id>:E<json>\n示例
E1:{"message":"Failed to fetch data","stack":"Error: Failed to fetch data\n at fetchData (server.js:10:15)"}Error Chunk的处理
javascript
// 文件:packages/react-server/src/ReactFlightServer.js
// 简化版
function emitErrorChunk(
request: Request,
id: number,
error: mixed,
): void {
const digest = logRecoverableError(request, error);
// 构造Error Chunk
const errorInfo = {
digest: digest,
message: __DEV__ ? error.message : undefined,
stack: __DEV__ ? error.stack : undefined,
};
const json = stringify(errorInfo);
const row = id.toString(16) + ':E' + json + '\n';
request.completedErrorChunks.push(stringToChunk(row));
}客户端处理Error Chunk
javascript
// 客户端解析到Error Chunk时
function resolveErrorChunk(chunk, errorInfo) {
// 创建Error对象
const error = new Error(errorInfo.message || 'An error occurred');
error.digest = errorInfo.digest;
error.stack = errorInfo.stack;
// 标记Chunk为错误状态
chunk.status = ERRORED;
chunk.reason = error;
// 触发错误边界
triggerErrorBoundary(chunk);
}Chunk类型总结表
| 类型 | 标识符 | 格式 | 用途 |
|---|---|---|---|
| Model Chunk | 无 | <id>:<json> | 普通数据、React Element |
| Import Chunk | I | <id>:I<json> | Client Component引用 |
| Symbol Chunk | S | S<id>:"<desc>" | JavaScript Symbol |
| Error Chunk | E | <id>:E<json> | 错误信息 |
| Hint Chunk | T | <id>:T<json> | 资源提示(preload等) |
| Debug Chunk | D | <id>:D<json> | 调试信息 |
9.8 综合案例:手写一个简易Flight序列化器
让我们通过实现一个简化版的Flight序列化器,深入理解Flight协议的工作原理。
目标
实现一个简单的Flight序列化器,支持:
- 序列化React Element
- 处理Client Component引用
- 支持对象引用(避免重复)
- 生成行格式的Flight数据
实现代码
javascript
// mini-flight-serializer.js
class FlightSerializer {
constructor(clientManifest = {}) {
this.clientManifest = clientManifest;
this.nextId = 0;
this.chunks = [];
this.writtenObjects = new WeakMap();
this.writtenClientRefs = new Map();
}
// 主序列化函数
serialize(value) {
const id = this.nextId++;
const json = this.serializeValue(value);
this.chunks.push(`${id}:${json}`);
return this.chunks.join('\n') + '\n';
}
// 序列化值
serializeValue(value) {
// 1. null
if (value === null) {
return 'null';
}
// 2. 基本类型
if (typeof value !== 'object' && typeof value !== 'function') {
return JSON.stringify(value);
}
// 3. 检查是否已序列化(避免重复)
const existingId = this.writtenObjects.get(value);
if (existingId !== undefined) {
return JSON.stringify(`@${existingId}`);
}
// 4. React Element
if (this.isReactElement(value)) {
return this.serializeElement(value);
}
// 5. Client Reference
if (this.isClientReference(value)) {
return this.serializeClientRef(value);
}
// 6. 数组
if (Array.isArray(value)) {
return this.serializeArray(value);
}
// 7. 普通对象
return this.serializeObject(value);
}
// 检查是否是React Element
isReactElement(value) {
return (
typeof value === 'object' &&
value !== null &&
value.$$typeof === Symbol.for('react.element')
);
}
// 序列化React Element
serializeElement(element) {
const { type, key, props } = element;
// 记录已序列化
const id = this.nextId - 1;
this.writtenObjects.set(element, id);
// 序列化type
let serializedType;
if (typeof type === 'string') {
// 原生元素:div, span等
serializedType = JSON.stringify(type);
} else if (this.isClientReference(type)) {
// Client Component
serializedType = this.serializeClientRef(type);
} else {
// 其他(不应该出现在Flight中)
throw new Error('Unsupported element type');
}
// 序列化props
const serializedProps = this.serializeObject(props);
// 格式:["$", type, key, props]
return `["$",${serializedType},${JSON.stringify(key)},${serializedProps}]`;
}
// 检查是否是Client Reference
isClientReference(value) {
return (
typeof value === 'object' &&
value !== null &&
value.$$typeof === Symbol.for('react.client.reference')
);
}
// 序列化Client Reference
serializeClientRef(ref) {
// 检查是否已写入
const existingId = this.writtenClientRefs.get(ref);
if (existingId !== undefined) {
return JSON.stringify(`@${existingId}`);
}
// 创建Import Chunk
const importId = this.nextId++;
this.writtenClientRefs.set(ref, importId);
// 从manifest获取模块信息
const moduleInfo = this.clientManifest[ref.$$id] || {
id: ref.$$id,
chunks: [],
name: 'default',
};
// 输出Import Chunk
const importJson = JSON.stringify(moduleInfo);
this.chunks.push(`${importId}:I${importJson}`);
// 返回引用
return JSON.stringify(`@${importId}`);
}
// 序列化数组
serializeArray(arr) {
const id = this.nextId - 1;
this.writtenObjects.set(arr, id);
const items = arr.map(item => this.serializeValue(item));
return `[${items.join(',')}]`;
}
// 序列化对象
serializeObject(obj) {
const id = this.nextId - 1;
this.writtenObjects.set(obj, id);
const pairs = [];
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = this.serializeValue(obj[key]);
pairs.push(`${JSON.stringify(key)}:${value}`);
}
}
return `{${pairs.join(',')}}`;
}
}
// 导出
module.exports = { FlightSerializer };使用示例
javascript
// example.js
const { FlightSerializer } = require('./mini-flight-serializer');
// 1. 定义Client Manifest
const clientManifest = {
'./AddToCart.js': {
id: './AddToCart.js',
chunks: ['client1'],
name: 'default',
},
};
// 2. 创建序列化器
const serializer = new FlightSerializer(clientManifest);
// 3. 创建Client Reference
const AddToCart = {
$$typeof: Symbol.for('react.client.reference'),
$$id: './AddToCart.js',
};
// 4. 创建React Element树
const element = {
$$typeof: Symbol.for('react.element'),
type: 'div',
key: null,
props: {
className: 'container',
children: [
{
$$typeof: Symbol.for('react.element'),
type: 'h1',
key: null,
props: { children: 'My Store' },
},
{
$$typeof: Symbol.for('react.element'),
type: AddToCart, // Client Component
key: null,
props: { productId: 123 },
},
],
},
};
// 5. 序列化
const flightData = serializer.serialize(element);
console.log(flightData);输出结果
1:I{"id":"./AddToCart.js","chunks":["client1"],"name":"default"}
2:["$","h1",null,{"children":"My Store"}]
3:["$","@1",null,{"productId":123}]
0:["$","div",null,{"className":"container","children":["@2","@3"]}]测试用例
javascript
// test.js
const { FlightSerializer } = require('./mini-flight-serializer');
// 测试1:简单元素
function test1() {
const serializer = new FlightSerializer();
const element = {
$$typeof: Symbol.for('react.element'),
type: 'div',
key: null,
props: { children: 'Hello World' },
};
const result = serializer.serialize(element);
console.log('Test 1 - Simple Element:');
console.log(result);
// 期望:0:["$","div",null,{"children":"Hello World"}]
}
// 测试2:嵌套元素
function test2() {
const serializer = new FlightSerializer();
const element = {
$$typeof: Symbol.for('react.element'),
type: 'div',
key: null,
props: {
children: [
{
$$typeof: Symbol.for('react.element'),
type: 'h1',
key: null,
props: { children: 'Title' },
},
{
$$typeof: Symbol.for('react.element'),
type: 'p',
key: null,
props: { children: 'Content' },
},
],
},
};
const result = serializer.serialize(element);
console.log('\nTest 2 - Nested Elements:');
console.log(result);
}
// 测试3:Client Component
function test3() {
const clientManifest = {
'./Button.js': {
id: './Button.js',
chunks: ['client1'],
name: 'default',
},
};
const serializer = new FlightSerializer(clientManifest);
const Button = {
$$typeof: Symbol.for('react.client.reference'),
$$id: './Button.js',
};
const element = {
$$typeof: Symbol.for('react.element'),
type: 'div',
key: null,
props: {
children: {
$$typeof: Symbol.for('react.element'),
type: Button,
key: null,
props: { onClick: 'handleClick' },
},
},
};
const result = serializer.serialize(element);
console.log('\nTest 3 - Client Component:');
console.log(result);
}
// 运行测试
test1();
test2();
test3();运行结果
Test 1 - Simple Element:
0:["$","div",null,{"children":"Hello World"}]
Test 2 - Nested Elements:
1:["$","h1",null,{"children":"Title"}]
2:["$","p",null,{"children":"Content"}]
0:["$","div",null,{"children":["@1","@2"]}]
Test 3 - Client Component:
1:I{"id":"./Button.js","chunks":["client1"],"name":"default"}
2:["$","@1",null,{"onClick":"handleClick"}]
0:["$","div",null,{"children":"@2"}]扩展练习
添加Error Chunk支持
- 实现
serializeError方法 - 处理渲染过程中的错误
- 实现
添加Promise支持
- 实现
serializeThenable方法 - 支持async组件
- 实现
添加Symbol支持
- 实现
serializeSymbol方法 - 处理React内置Symbol
- 实现
优化性能
- 使用流式输出,避免一次性生成所有Chunk
- 实现Chunk优先级队列
本章小结
本章深入讲解了React Server Components和Flight协议的核心原理:
RSC设计理念
- 零Bundle Size:Server Component代码不发送到客户端
- 直接访问后端资源:数据库、文件系统等
- 服务端与客户端的边界:通过"use client"标记
Flight协议
- 行格式协议:每行一个Chunk,支持流式传输
- 特殊标记:
$表示React Element,@表示引用 - 多种Chunk类型:Model、Import、Symbol、Error等
ReactFlightServer
- renderToReadableStream:服务端入口
- Request对象:渲染状态容器
- 组件树序列化:递归遍历并转换为Flight格式
服务端组件渲染
- Client Reference识别:通过$$typeof标记
- async组件支持:直接使用async/await
- 数据获取:并行获取多个数据源
客户端组件引用
- 模块引用序列化:Import Chunk
- Client Manifest:打包工具生成的模块映射表
- 动态加载:客户端根据引用加载模块
Flight数据格式
- 行格式:
<id>:<type><data>\n - 引用机制:避免重复和支持循环引用
- 类型标识符:I、E、S等
- 行格式:
综合案例
- 手写Flight序列化器
- 理解序列化流程
- 掌握Flight协议细节
思考题
- 为什么Flight协议使用行格式而不是完整的JSON?
- Client Component为什么不能直接导入Server Component?
- async Server Component如何处理Promise的pending状态?
- Flight协议如何处理循环引用?
- 如果一个Server Component渲染了1000个子组件,Flight数据会有多大?如何优化?
下一章预告
第10章将讲解客户端如何解析Flight数据,包括ReactFlightClient的实现、Chunk解析流程、组件树重建、Suspense与流式更新等内容。