Skip to content

第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的核心优势

  1. 零Bundle Size:Server Component的代码不会打包到客户端
  2. 直接访问后端资源:可以直接读取数据库、文件系统、环境变量
  3. 自动代码分割:每个Client Component自动成为代码分割点
  4. 更好的性能:减少客户端JavaScript,加快页面加载
  5. 更好的安全性:敏感逻辑(API密钥、数据库查询)只在服务端运行

对比表格

特性传统React组件Server ComponentClient 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的代码不会打包的原因是:

  1. 组件在服务端执行,返回的是渲染结果(React Element树)
  2. 渲染结果通过Flight协议序列化
  3. 客户端接收的是序列化后的数据,不是组件代码
  4. 客户端只需要重建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>
  );
}

组件边界规则

  1. Server Component可以导入Server Component
  2. Server Component可以导入Client Component
  3. Client Component只能导入Client Component
  4. 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>
  );
}

组件边界图解

为什么有这些限制?

  1. Client Component不能导入Server Component:因为Client Component在客户端运行,而Server Component的代码不会发送到客户端
  2. 可以通过props传递:Server Component可以先渲染,然后将结果作为props传递给Client Component
  3. "use client"是边界标记:它告诉打包工具这个模块及其依赖需要打包到客户端

9.2 Flight协议概述

Flight协议是React Server Components的通信协议,负责将服务端渲染的组件树序列化并传输到客户端。

9.2.1 什么是Flight协议

Flight协议是React团队设计的一种轻量级序列化协议,用于在服务端和客户端之间传输React组件树。

为什么需要Flight协议?

传统的JSON序列化无法满足RSC的需求:

  1. 无法表示React Element:React Element包含$$typeof等Symbol,JSON无法序列化
  2. 无法表示模块引用:Client Component需要引用客户端模块
  3. 无法表示Promise:异步组件需要表示pending状态
  4. 无法表示循环引用:组件树可能有循环引用
  5. 无法流式传输:JSON需要完整对象才能解析

Flight协议解决了这些问题,提供了一种专门为React设计的序列化格式。

Flight协议的特点

  1. 行格式协议:每行是一个独立的数据块(Chunk)
  2. 流式传输:可以边生成边发送,无需等待完整数据
  3. 支持引用:通过ID引用其他Chunk,避免重复
  4. 支持模块引用:可以引用客户端模块
  5. 支持Promise:可以表示异步数据的pending/resolved状态
  6. 紧凑高效:比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. 看到@1,知道这是一个模块引用
  2. 查找id为1的Chunk,获取模块信息
  3. 动态导入./src/AddToCart.js
  4. 用导入的组件替换@1

对比表格

特性JSONFlight协议
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;
}

参数说明

  1. model:要序列化的React组件树(通常是根组件)
  2. webpackMap:客户端模块映射表(Client Manifest)
    • 包含所有Client Component的模块信息
    • 由打包工具(Webpack/Vite)生成
  3. 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对象的关键属性

  1. 状态管理

    • status:渲染状态(OPENING/CLOSING/CLOSED)
    • fatalError:致命错误
    • destination:输出目标(ReadableStream的controller)
  2. Chunk管理

    • nextChunkId:下一个Chunk的ID
    • pendingChunks:待处理的Chunk数量
    • completedImportChunks:完成的Import Chunk队列
    • completedRegularChunks:完成的普通Chunk队列
    • completedErrorChunks:完成的Error Chunk队列
  3. 缓存与映射

    • cache:组件缓存
    • writtenSymbols:已写入的Symbol映射
    • writtenClientReferences:已写入的Client Reference映射
    • writtenServerReferences:已写入的Server Reference映射
    • writtenObjects:已写入的对象映射(WeakMap)
  4. 配置

    • 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?

识别机制

  1. 默认是Server Component:在服务端环境中,所有组件默认是Server Component
  2. "use client"标记Client Component:文件顶部的"use client"指令标记该模块为Client Component
  3. 打包工具处理: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

格式说明

  1. id:Chunk的唯一标识符

    • 十六进制数字(如:0, 1, a, ff)
    • 或字符串(如:S1, E2)
  2. type:单字符类型标识符

    • 无类型标识:Model Chunk(默认)
    • I:Import Chunk(模块引用)
    • E:Error Chunk(错误)
    • T:Hint Chunk(提示,如preload)
    • D:Debug Chunk(调试信息)
  3. 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. 行1:Import Chunk

    • 定义Client Component引用
    • id=1, 模块路径=./src/AddToCartButton.js
  2. 行2:Header组件的渲染结果

    • id=2, React Element: <header><h1>My Store</h1></header>
  3. 行3:ProductList组件的渲染结果

    • id=3, React Element: <div>Products...</div>
  4. 行4:AddToCartButton的引用

    • id=4, 引用@1(Client Component)
    • props: {productId: 123}
  5. 行5:main元素

    • id=5, 包含@3@4两个子元素
  6. 行0:根元素

    • id=0, 包含@2@5两个子元素

数据流图


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:null

React Element的格式

["$", type, key, props]
  • "$": 固定标记,表示React Element
  • type: 元素类型(字符串或引用)
  • 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的别名,表示普通的数据模型。

用途

  1. 表示React Element
  2. 表示普通JavaScript对象
  3. 表示数组、字符串、数字等基本类型
  4. 表示引用(通过@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 ChunkI<id>:I<json>Client Component引用
Symbol ChunkSS<id>:"<desc>"JavaScript Symbol
Error ChunkE<id>:E<json>错误信息
Hint ChunkT<id>:T<json>资源提示(preload等)
Debug ChunkD<id>:D<json>调试信息

9.8 综合案例:手写一个简易Flight序列化器

让我们通过实现一个简化版的Flight序列化器,深入理解Flight协议的工作原理。

目标

实现一个简单的Flight序列化器,支持:

  1. 序列化React Element
  2. 处理Client Component引用
  3. 支持对象引用(避免重复)
  4. 生成行格式的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"}]

扩展练习

  1. 添加Error Chunk支持

    • 实现serializeError方法
    • 处理渲染过程中的错误
  2. 添加Promise支持

    • 实现serializeThenable方法
    • 支持async组件
  3. 添加Symbol支持

    • 实现serializeSymbol方法
    • 处理React内置Symbol
  4. 优化性能

    • 使用流式输出,避免一次性生成所有Chunk
    • 实现Chunk优先级队列

本章小结

本章深入讲解了React Server Components和Flight协议的核心原理:

  1. RSC设计理念

    • 零Bundle Size:Server Component代码不发送到客户端
    • 直接访问后端资源:数据库、文件系统等
    • 服务端与客户端的边界:通过"use client"标记
  2. Flight协议

    • 行格式协议:每行一个Chunk,支持流式传输
    • 特殊标记:$表示React Element,@表示引用
    • 多种Chunk类型:Model、Import、Symbol、Error等
  3. ReactFlightServer

    • renderToReadableStream:服务端入口
    • Request对象:渲染状态容器
    • 组件树序列化:递归遍历并转换为Flight格式
  4. 服务端组件渲染

    • Client Reference识别:通过$$typeof标记
    • async组件支持:直接使用async/await
    • 数据获取:并行获取多个数据源
  5. 客户端组件引用

    • 模块引用序列化:Import Chunk
    • Client Manifest:打包工具生成的模块映射表
    • 动态加载:客户端根据引用加载模块
  6. Flight数据格式

    • 行格式:<id>:<type><data>\n
    • 引用机制:避免重复和支持循环引用
    • 类型标识符:I、E、S等
  7. 综合案例

    • 手写Flight序列化器
    • 理解序列化流程
    • 掌握Flight协议细节

思考题

  1. 为什么Flight协议使用行格式而不是完整的JSON?
  2. Client Component为什么不能直接导入Server Component?
  3. async Server Component如何处理Promise的pending状态?
  4. Flight协议如何处理循环引用?
  5. 如果一个Server Component渲染了1000个子组件,Flight数据会有多大?如何优化?

下一章预告

第10章将讲解客户端如何解析Flight数据,包括ReactFlightClient的实现、Chunk解析流程、组件树重建、Suspense与流式更新等内容。