简单来说,在前端开发中,IO瓶颈是影响内容渲染速度的重要因素(可以简单理解为,前端需要等待请求返回数据后,再根据数据渲染内容,这期间延迟的时间就是IO瓶颈)。但是,前端框架能够掌控的范围局限在前端,所以无法对IO瓶颈做出极致优化,只能在三个因素中做出取舍(比如考虑用户体验与性能时,代码维护成本就高)。
React团队为了同时兼顾三者,需要对服务端拥有更多掌控。这就是RSC诞生的初衷。但是,大部分React的受众只是把React当作前端view库,并不会直接使用RSC相关功能,所以React团队选择和Next.js团队合作,落地RSC。
我们正常通过npm i react下载的React包就是Latest路径的打包产物。通过npm update react@canary可以替换为canary包,RSC相关的功能就属于canary包。同理,通过npm update react@xperimental可以替换experimental包。
'use client' import {useState} from 'react'; function Cpn() { const [num, update] = useState(0); // ...省略 }实际上,这并不是Next.js自己的定义,而是RSC中的规范。在React文档中,我们可以看到'use client'与'use server'规范的定义,其中:
接下来我们简单讲下这三部分的作用。
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文件。
import React from 'react'; const LayCpn = React.lazy(() => import('./Cpn.jsx')); function App(props) { return <LayCpn {...props} />; }
与React.lazy类似,当我们在组件所在文件的顶部标记'use client'时,并在服务端组件的子孙组件中使用到该组件,该组件代码也会打包成独立的chunk。由于这个过程是全自动的,所以可以称为全自动React.lazy。
function App() { return <div>hello</div>; }对于SSR,会获得字符串'<div>hello</div>'的输出。
0:"$L1" 1:["$","div",null,{"children":"hello"}]再让我们看一个稍微复杂点的例子:
'use client' import {useState} from 'react'; function Cpn() { const [num, update] = useState(0); // ...省略 }现在,我们的服务端组件App返回值中包含了Cpn:
function App() { return <div><Cpn/></div>; }经由renderToPipeableStream方法,会获得如下序列化数据:
0:"$L1" 2:I["./src/app/Test.jsx",["client0","client0.chunk.js"],"Test"] 1:["$","div",null,{"children":["$","$L2",null,{}]}]
可以发现,序列化数据中并不包含具体的客户端组件代码,而是组件代码对应的文件(client0.chunk.js),这个文件就是我们在服务端编译时打包产生的chunk文件。
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这个组件了。