怎么理解 React Server Component 和 Next.js 的关系

大家好,我卡颂。

最近Next.js v14发布,发布会的各种梗图刷爆了国外前端社区。

Next.js的诸多特性(比如Server ActionApp Router),都是在RSCReact Server Component)基础上衍生出的。

从名字可以看出,RSCReact的特性。那么,该怎么理解RSCNext.js的关系呢?

欢迎围观朋友圈、加入人类高质量前端交流群,带飞

React团队的宿愿

对于前端框架的开发范式,有三个重要衡量因素:

  1. 用户体验

  2. 维护成本

  3. 性能

但是,通常很难做到三者兼顾(具体原因本文不细究,感兴趣的同学可以看data-fetching-with-react-server-components

简单来说,在前端开发中,IO瓶颈 是影响内容渲染速度的重要因素(可以简单理解为,前端需要等待请求返回数据后,再根据数据渲染内容,这期间延迟的时间就是IO瓶颈)。

但是,前端框架能够掌控的范围局限在前端,所以无法对IO瓶颈做出极致优化,只能在三个因素中做出取舍(比如考虑用户体验与性能时,代码维护成本就高)。

React团队为了同时兼顾三者,需要对服务端拥有更多掌控。这就是RSC诞生的初衷。

但是,大部分React的受众只是把React当作前端view库,并不会直接使用RSC相关功能,所以React团队选择和Next.js团队合作,落地RSC

此时我们发现,React有三类受众:

  1. 普通前端开发者,用稳定的React做业务开发

  2. 其他合作团队(比如Next.js团队),React团队为他们提供API支持

  3. 喜欢尝鲜的开发者/团队,愿意尝试那些可能出现在未来版本中的特性(通常还不稳定)

React团队针对这三类受众,制定了三条版本迭代路径:

  1. Latest路径

  2. Canary路径

  3. Experimental路径

我们正常通过npm i react下载的React包就是Latest路径的打包产物。

通过npm update react@canary可以替换为canary包,RSC相关的功能就属于canary包。

同理,通过npm update react@xperimental可以替换experimental包。

脱离Next.js使用RSC

Next.jsApp Router模式,所有组件默认为服务端组件(即在服务端render的组件),只有当组件所在文件顶部标记了'use client'指令时,该组件是客户端组件(即在前端render的组件)。

比如下面就是个客户端组件:

js 复制代码
'use client'
import {useState} from 'react';

function Cpn() {
  const [num, update] = useState(0);
  // ...省略
}

实际上,这并不是Next.js自己的定义,而是RSC中的规范。在React文档中,我们可以看到'use client''use server'规范的定义,其中:

  • 'use client'用于标记客户端组件(在服务端,默认所有组件都是服务端组件,所以客户端组件需要专门标记)

  • 'use server'用于标记前端的某个函数为Server Action(可以在前端执行的服务端逻辑)

既然是规范,那就需要落地。在Next.js中,规范的落地都被收敛到Next.js框架内部实现了。如果要脱离Next.js使用RSC,就需要我们自己落地规范。

RSC规范的落地包括三部分:

  1. 服务端编译时

  2. 服务端运行时

  3. 客户端运行时

这三者都被收敛到react-server-dom-webpack包中。

接下来我们简单讲下这三部分的作用。

服务端编译时

通过react-server-dom-webpack/plugin名字中的webpackplugin字样能看出,这是个webpack插件,配置类似如下:

js 复制代码
const ReactServerWebpackPlugin = require("react-server-dom-webpack/plugin");

const config = {
  // ...省略其他配置
  plugins: [
    new ReactServerWebpackPlugin({ isServer: false }),
  ],
}

他的作用是识别项目中的'use client'指令,作用有些类似于全自动React.lazy

使用过React.lazy特性的同学会知道,当我们通过React.lazy懒加载组件时,dynamic import的组件会被打包工具(比如webpack)打包成独立的chunk。当前端需要该组件时,会通过Jsonp请求chunk文件。

比如下面代码中的./Cpn.jsx组件由于懒加载,会被打包成独立的chunk

js 复制代码
import React from 'react';

const LayCpn = React.lazy(() => import('./Cpn.jsx'));

function App(props) {
  return <LayCpn {...props} />; 
}

React.lazy类似,当我们在组件所在文件的顶部标记'use client'时,并在服务端组件的子孙组件中使用到该组件,该组件代码也会打包成独立的chunk。由于这个过程是全自动的,所以可以称为全自动React.lazy

服务端运行时

上面讲到的编译产物都是客户端组件对应chunk,所以他们是不会在服务端运行时使用的。

服务端运行时的作用类似SSR,都是给定JSX输入,经过render后获得输出。比如,给定如下输入:

js 复制代码
function App() {
  return <div>hello</div>;
}

对于SSR,会获得字符串'<div>hello</div>'的输出。

对于RSC规范,将输入传给react-server-dom-webpack/server导出的renderToPipeableStream方法,会获得如下序列化数据:

js 复制代码
0:"$L1"
1:["$","div",null,{"children":"hello"}]

再让我们看一个稍微复杂点的例子:

我们有个组件Cpn,由于他包含客户端状态(使用了useState),所以只能作为客户端组件(顶部标记'use client'):

js 复制代码
'use client'
import {useState} from 'react';

function Cpn() {
  const [num, update] = useState(0);
  // ...省略
}

现在,我们的服务端组件App返回值中包含了Cpn

js 复制代码
function App() {
  return <div><Cpn/></div>;
}

经由renderToPipeableStream方法,会获得如下序列化数据:

js 复制代码
0:"$L1"
2:I["./src/app/Test.jsx",["client0","client0.chunk.js"],"Test"]
1:["$","div",null,{"children":["$","$L2",null,{}]}]

可以发现,序列化数据中并不包含具体的客户端组件代码,而是组件代码对应的文件(client0.chunk.js),这个文件就是我们在服务端编译时 打包产生的chunk文件。

客户端运行时

服务端运行时 产生的序列化数据 传递给前端时,react-server-dom-webpack又出场了,这次使用的是react-server-dom-webpack/client

这个包提供了几个方法,用于将从不同数据源获取的序列化数据 转换为合法的React Element,比如:

  • createFromFetch:通过fetch方法获取序列化数据

  • createFromReadableStream:通过可读流获取序列化数据

对于上述序列化数据:

js 复制代码
0:"$L1"
2:I["./src/app/Test.jsx",["client0","client0.chunk.js"],"Test"]
1:["$","div",null,{"children":["$","$L2",null,{}]}]

经由react-server-dom-webpack/client中方法的转换,会得到一个React.lazy组件,这样前端的React就能正常render这个组件了。

总结

RSC规范属于React特性,来自于React Canary。规范的落地可以通过react-server-dom-webpack包实现。

整个工作流程包括三个阶段:

  1. 服务端编译时,对应react-server-dom-webpack/plugin

  2. 服务端运行时,对应react-server-dom-webpack/server

  3. 客户端运行时,对应react-server-dom-webpack/client

Next.js中,RSC规范的落地被集成到框架内部,做到了开箱即用的RSC,并在此基础上衍生出更完善的功能(App Router)。

相关推荐
王哈哈^_^44 分钟前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie1 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic2 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿2 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具2 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161773 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test4 小时前
js下载excel示例demo
前端·javascript·excel
Yaml44 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事4 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶4 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json