发布日期

第 07 讲|Server / Client Components 的边界

Server / Client Components 边界的实现原理、序列化约束与最佳实践

阶段二:App Router 核心机制 · 第 7 / 40 讲 难度:⭐⭐⭐⭐⭐ · 预计耗时:3.5 小时 配套 fixture:fixtures/lecture-07/

学习目标

学完本讲,你应当能:

  • 默写 'use client' 在 SWC transform → next-flight-loader → flight-client-entry-plugin 三层的产物。
  • 解释 Next.js "双 module graph"(server graph / client graph)的物理含义。
  • 用 webpack layer 这个术语描述每条 import 关系(rscssrapp-pages-browser)。
  • 在 fixture 里能跑通:Context Provider、useState Hook、第三方 client-only 包、props 序列化失败等 5 种典型边界场景。
  • 看到 hydration mismatch / window is not defined / Module not found 等错误时立刻知道是哪一层的问题

1. 前置:React Server Components 的两层 module graph

RSC 不是"在服务端渲染 React"那么简单。它是一种模块系统

              ┌──────────────────────┐
              │   server graph (rsc) │   ← Server Components 在这里
              │   ────────────────   │
- 不可有 useState   │
- 可以 fetch        │
- 可以 import 任何   │
              │     server-only 代码 │
              └─────┬────────────────┘
                    │ 遇到 'use client'
                      (跨边界引用)
              ┌──────────────────────┐
              │  client graph (ssr +              │   app-pages-browser)  │   ← Client Components 在这里
              │  ─────────────────   │
- 可以 useState 等   │
- 不能 import server-only │
- 在 server 与 client 都跑│
              └──────────────────────┘

关键认知

  1. 同一个 import 在两张图里被解析成不同的东西——这就是 React 19 引入的 "exports conditions"。例如 react 在 server graph 用的是 react.shared-subset,在 client graph 用完整版。
  2. 跨图引用必须经过显式边界:用 'use client' 标记某文件,server graph 才能引用它(但只引用元数据,不引用实现)。
  3. server graph 是单向的:你可以 server → client,不能 client → server(除了 Server Actions,那是一种 RPC)。

Next.js 给每条物理 import 起了层名(layer)。这些层定义在:

170:packages/next/src/lib/constants.ts
  reactServerComponents: 'rsc',
  /**
   * Server Side Rendering layer for app (ssr).
   */
  serverSideRendering: 'ssr',
  /**
   * The browser client bundle layer for actions.
   */
  actionBrowser: 'action-browser',
  /**
   * The Node.js bundle layer for the API routes.
   */
  apiNode: 'api-node',
  /**
   * The Edge Lite bundle layer for the API routes.
   */
  apiEdge: 'api-edge',
  /**
   * The layer for the middleware code.
   */
  middleware: 'middleware',
  /**
   * The layer for the instrumentation hooks.
   */
  instrument: 'instrument',
  /**
   * The layer for assets on the edge.
   */
  edgeAsset: 'edge-asset',
  /**
   * The browser client bundle layer for App directory.
   */
  appPagesBrowser: 'app-pages-browser',
  /**
   * The browser client bundle layer for Pages directory.
   */
  pagesDirBrowser: 'pages-dir-browser',

记忆这 5 个最常用的:

Layer谁在跑跑在哪
rscServer Componentsserver (Node / Edge)
ssrClient Components 的"服务器影分身"server (SSR pass)
app-pages-browserClient Componentsbrowser
action-browserServer Actions 客户端引用browser → 转发到 server
middlewaremiddleware.tsEdge sandbox

一个 'use client' 组件实际同时存在于 ssrapp-pages-browser 两层:第一次访问时 server 端 SSR 跑 ssr 版本生成 HTML;客户端 hydrate 时跑 app-pages-browser 版本。

这就是为什么"client component 也会在服务端执行"——它有两份 bundle,server 跑的那份是为了 SSR。

2. 'use client' 在编译期发生了什么

2.1 第一步:SWC parse 找指令

packages/next/crates/next-custom-transforms/src/transforms/react_server_components.rs 里有一个 visitor 专门扫源码顶部的 "use client" / "use server" directive:

crates/next-custom-transforms/src/transforms/react_server_components.rs

它做的事:

  1. 检查指令是否出现在文件最顶部(comments / imports 之前——React 协议要求);
  2. 给模块打标记:isClientEntry / isServerEntry
  3. 注入一些 metadata 注释(如 // __client__:hello.js#default,foo),供后续 webpack loader 读取。

如果你查 source code 里 SWC transform 后的中间产物,会在文件尾看到注释类似:

/* __next_internal_client_entry_do_not_use__ default,Counter cjs */

这是给后续 loader 用的 edge-band 标记——既不会出现在用户代码视野,又能被 loader regex 捕获。

2.2 第二步:next-flight-loader 把 client module "替换"成 proxy

webpack 在加载 'use client' 文件时,会用 next-flight-loader 处理它。在 server graph 中,文件实际被替换为 proxy module

120:packages/next/src/build/webpack/loaders/next-flight-loader/index.ts
export default function transformSource(
  this: LoaderContext<undefined>,
  source: string,
  sourceMap: any
) {
  // Avoid buffer to be consumed
  if (typeof source !== 'string') {
    throw new Error('Expected source to have been transformed to a string.')
  }
  const module = this._module!

  // Assign the RSC meta information to buildInfo.
  // Exclude next internal files which are not marked as client files
  const buildInfo = getModuleBuildInfo(module)
  buildInfo.rsc = getRSCModuleInformation(source, true)
  let prefix = ''
  if (process.env.BUILTIN_FLIGHT_CLIENT_ENTRY_PLUGIN) {
    const rscModuleInformationJson = JSON.stringify(buildInfo.rsc)
    prefix = `/* __rspack_internal_rsc_module_information_do_not_use__ ${rscModuleInformationJson} */\n`
    source = prefix + source
  }
  prefix += `// This file is generated by the Webpack next-flight-loader.\n`

  // Resource key is the unique identifier for the resource. When RSC renders
  // a client module, that key is used to identify that module across all compiler
  // layers.
  //
  // Usually it's the module's file path + the export name (e.g. `foo.js#bar`).
  // But with Barrel Optimizations, one file can be splitted into multiple modules,
  // so when you import `foo.js#bar` and `foo.js#baz`, they are actually different
  // "foo.js" being created by the Barrel Loader (one only exports `bar`, the other
  // only exports `baz`).
  //
  // Because of that, we must add another query param to the resource key to
  // differentiate them.
  let resourceKey: string = this.resourcePath
  if (module.matchResource?.startsWith(BARREL_OPTIMIZATION_PREFIX)) {
    resourceKey = formatBarrelOptimizedResource(
      resourceKey,
      module.matchResource
    )
  }

  // A client boundary.
  if (buildInfo.rsc?.type === RSC_MODULE_TYPES.client) {
    const assumedSourceType = getAssumedSourceType(
      module,
      sourceTypeFromModule(module)
    )

rsc layer,loader 把整个 client 模块替换成调用 createProxy(resourceKey) 的代码——proxy 来自 react-server-dom-webpack

6:packages/next/src/build/webpack/loaders/next-flight-loader/module-proxy.ts
/* eslint-disable import/no-extraneous-dependencies */
import { createClientModuleProxy } from 'react-server-dom-webpack/server'

// Re-assign to make it typed.
export const createProxy: (moduleId: string) => any = createClientModuleProxy

Proxy 的本质:调用任何 export 时返回一个特殊对象 { $$typeof: react.client.reference, $$id: '<resourceKey>#exportName' }。这个对象不是真正的组件实现,而是**"占位符 + ID"**——server 渲染时遇到它就在 RSC payload 写出 $L<id> 引用。

判定函数最终长这样:

48:packages/next/src/lib/client-and-server-references.ts
export function isClientReference(mod: any): boolean {
  const defaultExport = mod?.default || mod
  return defaultExport?.$$typeof === Symbol.for('react.client.reference')
}

2.3 第三步:flight-client-entry-plugin 把 client 模块拉成入口

'use client' 不只是在 server graph 里被代理;它还需要作为单独的入口被打到 client graph,否则浏览器拿到 $L<id> 引用后无从加载真正实现。

flight-client-entry-plugin 做这件事:

src/build/webpack/plugins/flight-client-entry-plugin.ts

它的工作:

  1. 扫所有 page 的 server graph,收集所有 client 边界(client reference id)。
  2. 对每个 client 边界,在 client compiler 里注册一个新的入口(叫 app-pages-browser layer 的入口)。
  3. 把这些边界写进 client-reference-manifest(详见上一讲)。
  4. 同时收集所有 'use server' actions,写进 server-reference-manifest

2.4 整体流程图

你的代码 hello-button.tsx
SWC parse
检测到 'use client' → metadata 注入
   ┌─────────────────────────┬─────────────────────────┐
   ▼ rsc layer               ▼ ssr layer               ▼ app-pages-browser layer
next-flight-loader          原样保留                   原样保留
替换为 createProxy           (SSR 跑这份)             (hydration 跑这份)
    ↓                        ↓                          ↓
server graph 中:           server graph 中:          client bundle 中:
import HelloButton          import HelloButton         作为新的 entry
=> { $$typeof, $$id }       => 真正的 React 组件       (.next/static/chunks/...)
                            (用于 SSR 渲染初始 HTML)
                            写到 client-reference-manifest

这个三层的"分身术"是 RSC 的核心机制。你今天每写一个 'use client',都触发了上面这一整套

3. 业务示例 1:Context Provider 的正确写法

需求:根 layout 里要挂一个 ThemeProvider(提供 dark/light 主题),下面任何 client component 都能用 useContext 读它。

3.1 错误写法(最常见的坑)

// app/layout.tsx
import { createContext, useState } from "react"; // ← server component 不能 import 这个!

const ThemeContext = createContext("light");

export default function RootLayout({ children }) {
  const [theme, setTheme] = useState("light"); // ← 报错:useState in Server Component
  return (
    <ThemeContext.Provider value={theme}>
      <html>
        <body>{children}</body>
      </html>
    </ThemeContext.Provider>
  );
}

这会立刻报错:"useState is not a function" 或 "React functions like useState only work in Client Components"。

3.2 正确写法:抽出 client-only Provider

// app/_components/theme-provider.tsx
"use client";
import { createContext, useContext, useState } from "react";

const ThemeContext = createContext<{
  theme: string;
  toggle: () => void;
} | null>(null);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState("light");
  return (
    <ThemeContext.Provider
      value={{
        theme,
        toggle: () => setTheme((t) => (t === "light" ? "dark" : "light")),
      }}
    >
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error("useTheme must be used inside ThemeProvider");
  return ctx;
}
// app/layout.tsx  (server component)
import { ThemeProvider } from "./_components/theme-provider";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

为什么这能 work

  1. layout.tsx 是 server component,不导入 useState,只导入 ThemeProvider —— 后者经过上一节描述的"proxy 替换",在 server graph 里就是个占位符;server 不会执行 useState
  2. 渲染时 server 端 SSR pass(ssr layer)跑 ThemeProvider 真实实现,输出 HTML。
  3. 客户端 hydration 时 app-pages-browser 层接管,useState 在浏览器里跑起来。

正确的 mental model:server component 是"骨架",client component 是"开关"。Provider 是开关,所以必须是 client。

4. 业务示例 2:第三方 client-only 包

需求:用 react-toastify 弹通知。这个库 import 时会引用 windowdocument

4.1 错误写法

// app/some-page.tsx (server component)
import { toast, ToastContainer } from "react-toastify"; // ← 错!server 跑会报 window is not defined

export default function Page() {
  return <ToastContainer />;
}

server 会立刻在 import 阶段就崩溃,因为 react-toastify 模块顶部就读了 window

4.2 正确:用 client wrapper

// app/_components/toast-container-client.tsx
"use client";
export { toast, ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
// app/some-page.tsx (server component)
import { ToastContainer } from "../_components/toast-container-client";

export default function Page() {
  return <ToastContainer />;
}

通过 'use client' boundary 把第三方包"圈"进 client graph,server 就不会去 require 它实现,只持有 client reference。

4.3 更彻底:动态导入

如果这个组件只在客户端用、首屏不显示,可以再加一层 next/dynamic

"use client";
import dynamic from "next/dynamic";
export const Toaster = dynamic(
  () => import("react-toastify").then((m) => m.ToastContainer),
  {
    ssr: false, // 完全跳过 SSR pass
  },
);

ssr: false 让这个组件在 ssr layer 也不执行,只在浏览器 layer 加载——彻底避免任何 server 端解析。

这一招还能减小服务端 bundle 体积,详见第 26 讲。

5. 业务示例 3:Props 序列化的陷阱

Server Component 给 Client Component 传 props 时,props 必须能被 React 序列化(实际是 RSC 序列化协议)。

允许:number、string、boolean、null、undefined、Array、plain object、Date、Promise、Map、Set、TypedArray、React.Element、Server Action 函数。

不允许:自定义 class 实例、function(非 Server Action)、Symbol、Map 里的复杂 key。

5.1 错误写法

// app/products/page.tsx (server)
import { ProductDetail } from "./product-detail"; // client component

class Product {
  constructor(
    public id: string,
    public name: string,
  ) {}
  formatName() {
    return this.name.toUpperCase();
  }
}

export default function Page() {
  const p = new Product("001", "Widget");
  return <ProductDetail product={p} />; // ← 错!class 实例无法序列化
}

报错:Only plain objects can be passed to Client Components from Server Components.

5.2 正确写法

要么传 plain object:

return <ProductDetail product={{ id: p.id, name: p.name }} />;

要么把方法拆开传:

return <ProductDetail id={p.id} name={p.name} />;

5.3 传函数?必须是 Server Action

// app/orders/page.tsx (server)
async function cancelOrder(id: string) {
  "use server";
  // ... DB delete
}

return <OrderRow order={order} onCancel={cancelOrder} />; // OK

这里 cancelOrder 不是普通函数,编译期被 swc 转成 Server Action reference({ $$typeof: 'react.server.reference', $$id: ... }),客户端调用 = 发 RPC。详见第 12 讲。

6. 业务示例 4:Hooks 哪些能用、哪些不能

App Router 引入的 Server-friendly hooks 与传统 React hooks 有明确分界:

HookServer ComponentClient Component
useState / useReducer
useEffect / useLayoutEffect
useRef
useMemo / useCallback
useContext
use()
usePathname / useRouter / useSearchParams
cookies() / headers() / draftMode()✅(async fn)
notFound() / redirect()也可(throws)
useFormState / useFormStatus / useOptimistic

速记:能 throw 异常、读 request 元数据的(cookies / headers / notFound)是 server-only;和 React state lifecycle 相关的全是 client-only。

7. 业务示例 5:Client → Server?只能走 Server Action

下面这个绝对不行

"use client";
import { getUserData } from "../server-data"; // ← 错,假设 server-data.ts 是 server-only

export function Profile() {
  const [data, setData] = useState(null);
  useEffect(() => {
    getUserData("123").then(setData); // 直接在 client 里调 server 函数
  }, []);
}

server-data.ts 不会被打到 client bundle(甚至如果它 import 了 fs,client compile 直接报错)。

正确做法:把 server function 标 Server Action:

// app/_actions/get-user-data.ts
"use server";
export async function getUserData(id: string) {
  // ... fetch DB
  return { id, name: "Alice" };
}
"use client";
import { getUserData } from "../_actions/get-user-data";
// 这次 import 拿到的是 server reference proxy
// 调用它 = 发 POST 到 server

或者更自然:用 server 渲染 + 把数据作为 prop 传下去(推荐):

// app/profile/page.tsx (server)
import { getUserData } from "./data";
import { Profile } from "./profile-client";

export default async function Page() {
  const data = await getUserData("123");
  return <Profile data={data} />;
}

8. 重难点:client component 在 server 也跑

很多用户被这个 mental model 绕晕:

"我不是写了 'use client' 吗?为什么 server log 里能看到这个组件的 console?"

答案:'use client' 不是"只在客户端运行",而是"这个文件是 client/server 边界"

它造成两个具体效果:

  1. 这个文件作为客户端 entry,会被打到 client bundle(之前 flight-client-entry-plugin 干的事)。
  2. 服务端 SSR 阶段(ssr layer)会也执行这个组件一次,把 HTML 输出给浏览器作为首屏。

所以SSR 阶段你的 useState 也会跑(用初始值),useEffect 不跑(effect 只在浏览器跑)。这是 React 18 SSR 流式渲染的标准行为。

8.1 派生:何时该用 dynamic({ ssr: false })

如果你确实需要"完全不在 server 跑":

  • 组件依赖 window.requestAnimationFrame / IntersectionObserver 等只有浏览器有的 API。
  • 组件是 reactivity 极强的 chart 库、要等 layout 计算后才能工作。
  • 想缩短 SSR 耗时(虽然代价是首屏没有这块内容)。

next/dynamic({ ssr: false })

注意 server component 里不能直接 import dynamic 然后 ssr: false——这语义本身就是 client-only 行为,必须包在 'use client' 文件里再 dynamic。

8.2 派生:hydration mismatch 怎么定位

最常见原因:

  1. 服务端时间戳 vs 客户端时间戳new Date().toLocaleString() 在 server 与 client 因时区或 locale 不同,hydration 不一致。
  2. localStorage 读取:server 没有 localStorage,初始值 fallback;client 读到真值,DOM 已经渲染完,mismatch。
  3. 随机 IDMath.random() 在两边不同。Next.js 给了 useId() 解决。

定位手段:

  • 关掉 React DevTools 的 hide-warning。
  • 在 dev overlay 看具体哪个组件 mismatch(错误信息会指出 server / client 输出的具体差异)。
  • suppressHydrationWarning 屏蔽确定无害的差异(如时间)。

9. 业务示例 6:把 React Query 接入

React Query 是典型"client-only 状态管理"。它的 Provider 必须挂在 client tree 顶端。

// app/_components/query-provider.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";

export function QueryProvider({ children }: { children: React.ReactNode }) {
  const [client] = useState(() => new QueryClient());
  return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
}
// app/layout.tsx (server)
import { QueryProvider } from "./_components/query-provider";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <QueryProvider>{children}</QueryProvider>
      </body>
    </html>
  );
}

关键点

  • useState(() => new QueryClient()) 用 lazy 初始化,避免每次渲染创建新实例。
  • 不要在模块顶层 const client = new QueryClient()——SSR 与浏览器会共享同一个实例,跨请求泄漏数据。

10. 业务示例 7:在 server component 里访问敏感数据

需求:渲染当前登录用户邮箱。

server-only:

// app/account/page.tsx (server)
import { cookies } from "next/headers";
import { getUserByToken } from "./data";

export default async function AccountPage() {
  const cookieStore = await cookies();
  const token = cookieStore.get("session")?.value;
  const user = await getUserByToken(token);
  return <p>欢迎 {user.email}</p>;
}

cookies() 是 server-only API(packages/next/src/server/request/cookies.ts)。它通过 ALS 读到当前请求的 cookie——不会被打到 client bundle

但如果你不小心:

"use client";
import { cookies } from "next/headers"; // ← 编译期报错

Next.js 的 ESLint 规则(@next/eslint-plugin-next 里的 no-server-import-in-page 之类)会立刻报错。当前 server-only 模块通过 'server-only' 包做编译期检查:

import "server-only"; // 在 client layer 编译会失败

类似有 'client-only',反向防御。

11. webpack rule 是怎么决定一个文件去哪一层的

packages/next/src/build/webpack-config.ts 在配置 webpack 时,给每条 rule 加 issuerLayerlayer 字段。简化版逻辑:

- 文件路径在 app/* 下,且文件顶部有 'use client' → 进入 ssr & app-pages-browser
- app/* 下其他文件 → 进入 rsc layer
- 文件路径含 server-only 包 → 仅 rsc layer,client layer require 报错

完整规则在 webpack-config-rules/(第 23 讲深入)。

12. 配套 fixture 推荐实验

fixtures/lecture-07/ 包含 7 个边界场景,每个都是一个独立目录:

  1. app/example-1-context/ — ThemeProvider 抽到 client、layout 仍是 server。
  2. app/example-2-third-party/ — 用 next/dynamic({ ssr: false }) 包 client-only 库。
  3. app/example-3-serialization/ — 故意传 class 实例,看 dev overlay 报错。
  4. app/example-4-hooks/usePathname 在 client,cookies() 在 server。
  5. app/example-5-action/ — Client 调 Server Action。
  6. app/example-6-react-query/ — QueryClient Provider 接入。
  7. app/example-7-hydration-mismatch/ — 故意制造时间不一致,看 dev 报警。

每个目录里都有详细注释解释"为什么这样写"。

12.1 必看实验:dump RSC payload

cd fixtures/lecture-07
pnpm dev

# 另一个 terminal
curl -s -H 'RSC: 1' http://localhost:3007/example-1-context?_rsc=1 | head -40

你会看到 RSC payload 里出现类似:

1:I["./app/_components/theme-provider.tsx",["app/example-1-context/page",...],""]
0:["$","$L1",null,{children:[...]}]

$L1 就是 client reference 占位符;1:I[...] 是 manifest 引用——浏览器拿到后会去加载对应 chunk。

13. 重难点小结

13.1 不要在 'use client' 模块里 import server-only

会编译失败或运行报错,依赖具体路径。最佳实践是:

  • 所有 server-only utility 放 lib/server/,加 import 'server-only'
  • 所有 client-only utility 放 lib/client/,加 import 'client-only'
  • 共享代码(pure function)放 lib/shared/

13.2 不要在 'use server' 文件里 export 非 function

Server Actions 文件只能 export async function

"use server";
export const MY_CONSTANT = 42; // ← 错!server action file 只能 export 函数
export async function doIt() {
  /* ... */
}

否则 build 报错。要分享常量到 client,单独写个共享 module。

13.3 'use client'整个文件级的

不是函数级。你不能:

function MyClientFn() {
  "use client"; // ← 错,directive 必须在文件顶部
  // ...
}

要拆出去单独一个文件。

13.4 多个 'use client' 边界的"传染"

A.tsx (server)
 └─ imports B.tsx ('use client')
     └─ imports C.tsx (no directive)

C.tsx 会被打到 client graph(被 B 引用),即使 C 没有 'use client'。这意味着 C 的代码会出现在浏览器 bundle 里——如果 C 用了 server-only API,会爆。

记住:'use client' 是"以下文件全部被打到 client"的语义边界

13.5 dev overlay 的"层"诊断信息

dev 错误页会在堆栈上方显示形如 rsc:ssr:app-pages-browser: 的前缀——这就是 webpack layer。看到错误前缀立刻知道是哪层

14. 检验问题

  1. 一个 'use client' 组件在 server 端会被执行吗?什么时候?哪一层 layer?
  2. SWC、next-flight-loader、flight-client-entry-plugin 三者各自做什么?
  3. server component 给 client component 传一个 class 实例,会发生什么?怎么修?
  4. 什么时候应该用 next/dynamic({ ssr: false }) 而不是 'use client'
  5. 'use server' 文件可以 export 非 function 吗?为什么?
  6. Context Provider 必须放在哪一层?为什么不能直接放 root layout?
  7. import 'server-only'import 'client-only' 的工作原理是什么?
  8. RSC payload 里看到的 $L1 是什么?怎么从它跳到真正的 client component 实现?
  9. 你的同事说:"我把组件标了 'use client'window is not defined 还在报",怎么排查?
  10. Server Action 函数与普通函数在 props 序列化时为何被特殊处理?

15. 延伸阅读

  • React 官方文档:Server Components / Client Components RFC
  • crates/next-custom-transforms/src/transforms/react_server_components.rs(Rust 端 SWC visitor)
  • packages/next/src/build/webpack/loaders/next-flight-loader/(loader 完整目录)
  • packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts(entry 注册逻辑,900+ 行)
  • .claude/skills/react-vendoring/SKILL.md(vendored React 与 entry-base.ts 约束)
  • .claude/skills/dce-edge/SKILL.md(在 edge runtime 下 RSC 行为差异)
  • 配套 fixture:fixtures/lecture-07/

下一讲预告

第 08 讲|Layout、Template、并行路由与 LoaderTree:上一讲我们认识了 LoaderTree 数据结构,本讲深入 createComponentTree 的装配过程——server 怎么把 LoaderTree 转成 React Element tree,parallel route 与 intercepting route 在这里如何特殊处理。