NextJS 与 RSC

NextJS 与 RSC

最近突然好奇,想要研究一下 NextJS 的 RSC 和 Nuxt 到底有什么具体的区别,摸了一天的鱼去调研了 NextJS 与 RSC 的渲染逻辑

2293字

首先先明确一件事:NextJS 本身就是 100% 的纯 SSR。而 RSC,则是更加变本加厉的终极 SSR—— No JavaScript。

实际上,对 NextJS 而言,渲染页面包含两种主要的形式:

  1. 用户初次访问某个页面,例如直接手动在浏览器 url 栏输入地址后回车;
  2. 用户与应用本身进行交互,点击某个按钮进行页内跳转。

对于两种情况,具体的 SSR 行为也有所不同。

初次访问

当用户初次访问时,其实 Next 本身会做三件事情:

  1. 构建最纯粹的 RSC Payload 纯文本数据流。
  2. 最传统、最经典的 SSR,根据 RSC Payload 去生成纯粹的 HTML 字符串
  3. 客户端组件进行水合。

我们一步步来看。

当用户初次访问时,Next 本身就立即会对该路由下的所有组件树——包括服务端组件和客户端组件——进行遍历,并对 RSC 和 RCC 执行不同的逻辑,最终生成一个二进制的、中间态的数据流,也就是 RSC Payload,这段字符串包含了各个 React 组件之间的嵌套关系以及对应的具体数据到底是什么。

对 RSC 来说,Next 直接执行这个函数(本身是函数式组件),包括其中的具体获取数据的逻辑,之后把获取到的数据包括这个组件本身全都通过一定方式序列化到 RSC Payload 里面。

对 RCC 来说,如果 Next 遍历到 RCC,则会立即停下来,在 RSC Payload 插入一段占位符,指示 XX 组件应该要加载的 JS 时xxx-chunk.js

然后,NextJS 会对完整的该路由下的全部组件进行一次 SSR,生成具体的 HTML 结构。这个阶段就结合我们之前生成的 RSC Payload 的具体数据,对服务端组件直接将数据写入 DOM。对 RCC,则也会进行 SSR 生成具体的骨架,也就是客户端的初始状态。当然,对 RCC 来说,必须要加载 JS。在初次访问页面的阶段,这个 SSR 的过程就会对在 RSC Payload 解析过程中遇到的所有 chunk 都以 <script> 标签的形式塞到 html 里面。这个阶段,NextJS 服务端的任务基本完成。

客户端接到这个 html 之后,结合 RSC Payload 也会被发到客户端。接下来,就是进行最经典的水合了。

Next Runtime 接到这个 RSC Payload,首先就会在客户端构建一个 VDom 树,根据这个 VDom 树和这个服务端发过来的 html 进行逐个递归匹配。当匹配到一个客户端组件时,由于会有一个多的字段,比如叫chunk,那么客户端就知道“哦,这个是客户端组件,它要执行 xxx chunk 了”,**由于我们在 SSR 的过程中,对应的 chunk js 早就被写到 HTML 里面了,浏览器接到之后就会直接开始下载这些 JS,**下载好了之后就会直接对这个 JS 进行执行,速度极快。这就是最经典的水合——客户端 VDom 比对服务端发来的完整 DOM 结构,然后加载 JS。

其实有一个问题:这个 RCC 的 RSC Payload 结构也没 DOM 结构的描述啊,那客户端是咋解析它生成用以描述 DOM 结构的 VDom 树的?

其实,当 RSC Payload Parser 解析到客户端组件后,会直接的执行一遍这个 chunks 内部的 .js 代码。这个 chunk 内部的代码,其实比我们想得都要纯粹的多:

JSX
export function LikeButton(props) {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(!liked)}>赞 {props.count}</button>;
}

就是我们写的组件代码。然后 Parser 直接调用这个代码,就拿到一个 VDom,直接更新到原本的大 VDom 树里面。

页内跳转

比如,你在/页面点击了一个跳转/about的按钮,你会被跳到/about。这就是页内跳转。

当触发业内跳转时,所做的工作比初次访问简单得多——只是 RSC Payload 的生成

同样的,当访问/about时,服务端会以相同的方式,遍历/about这个路由下的所有组件,包括 RSC 和 RCC。

对 RSC 还是老样子,直接执行它,里面拿数据也照样拿,全部执行好之后将具体数据和 DOM 结构全都用 RSC Payload 序列化写入,大概的结构是:

JSON
{
  "type": "div",
  "props": {
    "className": "about-container",
    "children": {
      "type": "h1",
      "props": { "children": "关于我们" }
    }
  }
}

对 RCC,和首次渲染差不多,也是用占位符标记这是客户端组件,用 chunks 标记需要加载的 JS:

JSON
{
  "type": "$module_reference", // 特殊标记,告诉客户端这是一个客户端组件
  "props": {
    "count": 10 // 服务端传给这个客户端组件的 props
  },
  "_chunks": ["chunk-like-button-xyz.js"], // 核心!去哪里找代码
  "_name": "LikeButton"
}

然后,把/about的所有完整的 RSC Payload 发给客户端,由客户端的 Next Runtime 解析。解析的结果是什么?当然是一个全新的/aboutVDom 树。

生成全新的 VDom 树之后,就是 React 老本行,和 Fiber 差不多,直接和原本的/VDom 树进行逐一对比,算出差异的部分,然后再统一 Commit 出变更。这样就实现了不同路由之间的页面切换逻辑。

评论0