服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

【第2914】从Next.js 13 认识React Server Components

日期: 来源:前端早读课收集编辑:莫力全 Kyle Mo

前言

一个认识 React Server Components 的好时机。今日前端早读课文章由 @莫力全 Kyle Mo 授权分享。

前端早读课:重视你的信息价值密度

正文从这开始~~

React Server Components 其实不是一个新概念了,React 官方第一次提出这个技术得回溯到 2020 年的 12 月,时至今日已经超过两年了。不过这个技术却一直待在研究与开发的阶段,没有正式的发布,可以说几乎就要被 React 开发者们给遗忘了…,直到去年底 Next.js 13 的发布,部分新功能整合了 React Server Components,才让 Server Components 又再一次受到关注。

React Core Team 本来就有提过会跟 Next.js 或 Remix 这类成熟的 meta frameworks 合作,在这些框架中尝试一些未来想要推出的功能与架构,所以我们对于这次 Next.js 13 抢先推出 Server Components 其实不用感到太讶异。

虽然 Next 13 这次推出的关于 Server Components 的功能都还是实验性质,未来各种 API 都有可能会做出改变,很多部分甚至还有严重的 bugs,但我认为这是一个认识 React Server Components 的好时机,我们也可以透过 Next.js 目前整合的现况预测与想像未来用 React 开发应用程序会与现今我们已经熟悉的模式有什么样的区别。

这篇文章会简单复习一下 React Server Components 的概念,并探索 Next.js 13 整合的状况以及在开发上带来的改变,最后可能也会分享一下自己试用后的心得感想。

【第2816期】React Server Component: 混合式渲染

什么是 React Server Components?

(如果想要更详细地理解 Server Components,建议把官方的介绍视频看过一次)

React Server Components 是一种新型态的 component,过往我们熟悉的组件则相对被称作 Client Components,它还是一个实验性质的功能,不过不出意外的话这个 feature 会是 React 未来发展的方向,如果稳定发布后,也许会重新定义前端与后端的分工,也会改变我们熟悉的 React 开发方式。

在这个 model 底下,我们可以将组件区分为以下三种:

  • Server Components (.server.js 结尾):在 Server Side 渲染的组件,具有访问 DB、file system 的能力,但没办法做事件绑定,也就是缺少了「互动性」。

  • Client Components (.client.js 结尾):在 Client Side 渲染的组件,拥有互动性。

  • Share Components:可以在 Server Side 也可以在 Client Side 渲染,具体要看是什么组件引入它,如果被 Server Components 引入就由 server 渲染,反之则由 client 渲染。

我们可以把这个 model 下的 React Component Tree 看成它是由 Server Side 与 Client Side 混合渲染的一个树状结构,React 在 Server Side 将 Server Components 渲染好后传给 client 端,如果 server side 在渲染的过程遇到 client components,它就会用一个 placeholder 来标注它(请注意它不会实际执行或渲染它),未来让 client side 知道这是需要它来渲染的组件。一般来说 client 端在接收到 JS bundle 后会进行 hydration,不过 Server Components 只会在 Server Side 做渲染,不会在 client 端进行 hydration。

混合式渲染,橘色为 Server Components,蓝色为 Client Components

混合渲染是怎么进行的?

简单而言可以拆分为以下三步骤:

  • Render Root

  • Request For Server Components

  • React Runtime Rendering

Render Root

浏览器拿到页面 HTML 后,会发出 request 请求主要的 JS bundle, 比如说 main.js,这个 JS bundle 包含了 React Runtime 与 Client Root,Client Root 执行后会创建一个 Context,这个 Context 可以用来储存 Client Side 的 state。

Request For Server Components

Client Root 的程序代码被执行后,同一时间,浏览器也会向 Server Side 的某个 API endpoint 发出一个请求,也就是上图的 useServerResponse(以官方范例来说,这个 endpoint 是 /react),当然在打这个请求的时候也需要带一些 data 过去,可以预料到这些 data 会包含一些 React Tree 的信息,Server Side 才知道要渲染哪些 components。服务器端接受到请求后就会开始进行 Server Components 的渲染。

服务器会从 Server Component Root 开始渲染,并形成一颗混合的组件树

这颗混合的组件树会形成类似下方的对象,带有 React 必要的信息

 module.exports  = {
tag: 'Server Root' ,
props: { ... },
children: [
{ tag: "Client Component1" , props: { ... } : children: [] },
{ tag: "Server Component1" , props: { ... } : children: [
{ tag: "Server Component2" , props: { ... } : children: [] },
{tag: "Server Component3" , props: { ... } : children: [] },
]}
]
}

不过刚刚有说过 Server Side 在渲染的时候如果遇到 Client Components,只会用 placeholder 做一个注记, 这些 Client Components 需要送到 Client Side 做渲染,也就是说 React 必须将这些信息也送到 Client Side,所以 React 最后回传的其实是一个可序列化且带有特殊格式 JSON chunk response,以便于之后可以渐进式的在 Client 端渲染。

 M1:{"id":"./src/SearchField.client.js","chunks":["client5"],"name":""}
M2:{"id":"./src/EditButton.client.js","chunks":["client1"],"name":""}
S3:"react.suspense"
M4:{"id":"./src/SidebarNote.client.js","chunks":["client6"],"name":""}
J0:["$","div",null,{"className":"main","children":[["$","section",null,{"className":"col
sidebar","children":[["$","section",null,{"className":"sidebar-header","children":[["$","img",null,{"className":"logo","src":"logo.svg","width":"22px","height":"20px","alt":"","role":"presentation"}],["$","strong",null,{"children":"React

Notes"}]]}],["$","section",null,{"className":"sidebar-menu","role":"menubar","children":[["$","@1",null,{}],["$","@2",null,{"noteId":null,"children":"New"}]]}],["$","nav",null,{"children":["$","$3",null,{"fallback":["$","div",null,{"children":["$","ul",null,{"className":"notes-list
skeleton-container","children":[["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item
skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item
skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item
skeleton","style":{"height":"5em"}}]}]]}]}],"children":["$","ul",null,{"className":"notes-list","children":[["$","li","0",{"children":["$","@4",null,{"id":0,"title":"Meeting
Notes","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"This is an example note. It contains
Markdown!"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Meeting
Notes"}],["$","small",null,{"children":"10:04 PM"}]]}]}]}],["$","li","1",{"children":["$","@4",null,{"id":1,"title":"Make a
thing","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"It's very easy to make some words bold and other words italic with
Markdown. You can even link to React's..."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Make a
thing"}],["$","small",null,{"children":"10:04 PM"}]]}]}]}],["$","li","2",{"children":["$","@4",null,{"id":2,"title":"A note with a very long title because
sometimes you need more words","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"You can write all kinds of amazing notes in this
app! These note live on the server in the
notes..."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"A note with a very long title because
sometimes you need more words"}],["$","small",null,{"children":"10:04 PM"}]]}]}]}],["$","li","3",{"children":["$","@4",null,{"id":3,"title":"I wrote this note
today","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"It was an excellent
note."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"I wrote this note
today"}],["$","small",null,{"children":"10:04 PM"}]]}]}]}]]}]}]}]]}],["$","section","null",{"className":"col
note-viewer","children":["$","$3",null,{"fallback":["$","div",null,{"className":"note
skeleton-container","role":"progressbar","aria-busy":"true","children":[["$","div",null,{"className":"note-header","children":[["$","div",null,{"className":"note-title
skeleton","style":{"height":"3rem","width":"65%","marginInline":"12px 1em"}}],["$","div",null,{"className":"skeleton
skeleton--button","style":{"width":"8em","height":"2.5em"}}]]}],["$","div",null,{"className":"note-preview","children":[["$","div",null,{"className":"skeleton
v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton
v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton
v-stack","style":{"height":"1.5em"}}]]}]]}],"children":["$","div",null,{"className":"note--empty-state","children":["$","span",null,{"className":"note-text--empty-state","children":"Click
a note on the left to view something! 🥺"}]}]}]}]]}]

中的英文符号代表不同的资料模型,例如 M 代表 module (也就是 Client Components 所需的 JS Chunk 信息),S 代表 Symbol,E 则代表 Error,J 代表 Server Components 渲染出的类似 react element 格式的字串,React 会根据这些 chunk response 来渲染对应的 Native Elements 与 Client Components。有兴趣的读者可以再进一步参考 React 的 source code。

React Runtime Rendering

刚刚那些 JSON response 送到 Client Side 后,React Runtime 就接手开始工作,他会依据 chunk response 的内容渲染出真正的 HTML 组件。例如当它看到代表 Module 的「M」,就会发送请求获取 Client Components 所需的 JS Bundle,当浏览器载入 Client Components 的 Bundle 后,React 就可以进行渲染与 Hydration。如果看到代表 Server Components 渲染出的内容的「J」,React 就会将实际元素渲染出来。值得注意的是,React 在传输刚刚提到的 JSON chunk response 是采用 streaming 的方式,也就是说 React Runtime 不用等到拿到所有资料才能开始做进一步处理。

因此混合渲染的简易流程会如下图:

其实以上的流程都只是简化的版本,中间运作的过程其实需要前后端之间不少复杂的互动,有兴趣可以参考这篇文章,而像 Next.js 这种 meta framework 其实会与 module bundler 整合,把这些复杂的流程抽象化与简化,让我们未来在使用 Server Components 开发时不用面对那么底层复杂的问题,这也是 React 官方积极与这些 framework 合作探索这个新功能的原因之一。

React Server Components 带来什么优势?

React Server Components 主要解决了以下几个问个:

  • 减少 bundle size

  • 运用 Server Side 的能力

  • 自动化 Code Splitting

减少 bundle size

通常在开发前端应用的时候,我们会安装许多 dependencies packages,有些 packages 甚至还没办法做 tree-shaking,随着引入的软件包变多,应用的 bundle size 也会跟着增大,造成页面载入的效能下降。

Server Components 因为只在 Server 上做渲染,所以组件的程序代码 bundle 不用被下载到 Client Side, 如果软件包只被 Server Components 使用(React 官方的范例是一个处理 markdown 语法的软件包),就不用担心它会增加应用整体的 bundle size。

一个更加极端的例子是应用中大部分的组件如果都没有跟使用者互动的需求,那这些组件都可以使用 Server Components,这些 UI 都可以走「浏览器接受 JSON chunk response,React Runtime 接手渲染」的这个模式,这样的话理论上除了 React Runtime,是不需要其他 JS bundle 的(而 JSON response 体积很小跟 JS bundle 不是一个量级,可以忽略不计)。因为 React Runtime 的 bundle size 不会随着项目的扩展而变大,所以这是官方号称 RSC 为「Zero-Bundle-Size Components」的原因。

运用 Server Side 的能力

在 React Server Components 中,我们可以直接 access DB,甚至也可以 access file system,简单来说你透过 Node.js 能做到什么,Server Components 就很有可能也做得到(当然现在大多数还是聚焦在 data fetching 这个应用场景上,但如果脑洞大开,其实要在 RSC 做一些复杂的运算也是有可能,但这就等待未来发展了)。透过这样自由整合后端的能力,我们可以解决 Client-Server 往返过多,甚至造成 waterfall 请求的状况,典型的情境就是透过 nested 的 useEffect 来 call API 获取资料,我们需要等在上层的 component 抓取资料并 render 出下层 component 后,才知道下层的组件需要什么样的资料,这种模式对效能来说可能会产生很大的影响。

自动化 Code Splitting

在过去我们要做到 Code Splitting 必须自己用 React.lazy 搭配 Suspense 或是使用成熟的第三方软件包例如 loadable-component,这样的缺点就是需要由开发者自行手动分割、自行确认要分割的边界。

有了 React Server Components,这个麻烦似乎得到缓解,React 将所有的 Client Components 视为潜在的 Code Splitting 分割点,我们只需要按照拆分组件的思维去组织项目,React 会自动帮我们做到 Code Splitting。

 import  ClientComponent1  from  './ClientComponent1' ;

function ServerComponent ( ) {
return (
<div>
// Client Component 会自动被Code Splitting
<ClientComponent1 />
</div>
)
}
React Server Components 的缺点

最显而易见的缺点就是开发者的学习路径变得更陡峭了,Server Components 也需要跟 module bundler、服务器端做整合才能够使用,在设定上肯定会增加不少复杂度。

另外实际开发时拆解 component 的时候还要进一步去思考「我这个组件要用 Server Component 还是 Client Component ?」这不外乎会增加开发者的心理负担。

React Server Components vs Server Side Rendering

这两个名词很容易让人混淆,SSR 的机制是在 server 中 render 一个 HTML 传送到 client side 并在 client side 透过 react runtime 做 hydration,完成之后,页面才是一个可以互动的完整应用。除了 initial page load 以外,后续页面的 navigation,例如在 Next.js 中透过 Next/Link 进行页面跳转,它其实是走 client side 的 navigation,Next 会 call 一个 API endpoint 去执行 getServerSideProps function 去抓取需要的资料,但并不会重新产生一个 HTML。所以说即便是 SSR 的应用,在 navigation 时我们的 web app 其实就跟一般不是走 SSR 的 SPA 页面行为一致了。因此我们可以发现,SSR 的重点在于「页面的初始渲染」。

而 React Server Components 永远都是在 server 上渲染的,当这些组件需要 re-render 时,它们会在 server side 重新做 data-fetching,然后重新 merge 回 client side 中现有的 React Component Tree 。值得注意的是就算页面中部分 Server Components 重新再跟服务器要资料,与此同时 client side 的 state 是可以被保留的。

简单来说两者是完全不同的概念,也不互相冲突,使用 Server Components 不一定要走 SSR,使用 SSR 也不一定要用 Server Components,当然两者也可以结合使用,就像稍后会介绍的 Next.js 13 一样。

(想更了解两者的区别,可以参考这篇文章。)

Server Components With Next.js 13

Page Directory → App Directory

熟悉 Next.js 的人应该知道 pages 是一个特殊的文件夹路径,写在其中的文件会直接对应到前端应用的 Routing System,举例来说,/pages/about.tsx 会对应到应用的 /about 页面,/pages/nested/test.tsx 会对应到 /nested/test 页面,这样的方式让我们不用去另外定义 Router,非常的直观又方便。

以上的模式也被称作「Page Directory」,而 Next 13 提出了一个实验性质的模式 — 「App Directory」。

Next.js 13 App Directory

App Directory 与 Page Directory 在文件安排上最大的不同就是它让与 Page 没有直接相关的文件也可以放到路径文件夹底下,这样 colocation 的功能是 Page Directory 做不到的,因为只要放到 pages 文件夹底下的文件,都会自动建立出一个路径出来,所以我们不能把 CSS 文件或是 components 文件与页面文件放在一起。在 App Directory 中,真正可以决定页面路由的只有 page.js 这个文件,也就是说 /app/dashboard/page.js 会建立出 /dashboard 的页面、 /app/nested/test/page.js 则会建立出 /nested/test 页面,其他的文件则不会影响 Routing 结果。

在 App Directory 中,CSS 或测试等文件都可以 colocate 在一起而不会影响应用路由结果

虽然说现在文件可以随意 Colocate,但 Next 有规定一些特定的文件与它们特别的作用:

App Directory 的保留字文件

如果都有定义的话,这些文件也会被 Next 以特定的 Hierarchy 渲染出来,我们稍后也会探索其中的几个文件。

App Directory 作为 Next.js 新版的 Routing System,它还提供了一些额外的新功能:

App Directory 带来的新功能

其中最重要的新功能就属本篇的主角 Server Components 了,当然,其他的功能稍后也会有各自的段落来介绍(它们也跟 Server Components 的使用息息相关)。

(Next.js 13 以 App Directory 为起始点,其实带来非常多的新功能与改变,从官方特别为 App Directory 做了一份新的 Document Site 就可以得知这点。笔者在一篇文章内是不可能讲完所有功能的,因此感兴趣的读者非常推荐自行去阅读看看喔!)

Server & Client Components

不同于 React 官方最初提供的范例是以档名来判断一个元件是 Server Component 还是 Client Component (ex: test.server.tsxform.client.tsx ),在 Next.js 13 App Directory 中,所有的 Components 预设都是 Server Component,并且 Server Component 可以是一个 async function。而当你认为一个元件需要使用者互动或是呼叫 DOM 等 Web API 时,再透过 “use client” 标记组件为 Client Component。

App Directory 中,默认组件都是 RSC,并且可以是一个 async function

透过在文件开头注记 use client 宣告该组件为 Client Component

透过「默认情况下走 Server Component,若有使用者互动的需求则走 Client Component」 这个原则,比起原先需要手动在档案名称上注记 Server 或是 Client Component 的方式,我认为可以在一定程度上减少开发者的心理负担。

Next.js 官方文件也列出了 Server 与 Client Component 的使用时机:

Server Component 与 Client Component 的使用时机

另外,在 Server Component 与 Client Component 的使用上,Next.js 也提出了一些限制,例如说在 Client Component 里不能直接 import Server Component

而必须以 children props 的方式传入,因为前面有提到,Server Side 在渲染过程如果遇到 Client Component,它并不会去实际执行或渲染它,而是会把一些资讯注记起来传给 client side,所以如果是上面的写法,如果执行 Client Component 的文件的话,React 是不会知道它在 return function 需要去渲染那个 Server Component 的。透过下图 children props 这种 pattern React 才知道它需要在回传资料给 client 前先在 Server Side 渲染这个 Server Component,也就是说 React 透过这种方式才能在 Server Side 完整解析树状结构。

另一个限制是虽然 Server Component 可以透过 props 的方式传资料给 Client Component,但这些资料必须是 serializable 的,所以像是 functions 或 Dates 对象就不能直接 pass 给 Client Component。

官方有提到一个词要做「Network Boundary」,在过去版本的 Next, Network Boundary 存在于 getStaticProps/getServerSideProps 与 Page Components 之间,而在新版的 App Directory,这个 boundary 则介于 Server Components 与 Client Components 之间。当你想要跨越 Network Boundary 传递资料时,这些资料就必须是可序列化的,这就是为什么 Server Components 传递资料给 Client Components 有这个限制的原因。

再来是既然拆分出了 Server Component 与 Client Component 的概念,由于 JavaScript Module 是可以被在这两种组件中共用的,如何去避免只打算在服务器上运行的程序代码不小心被用户端执行也是一件很重要的事。举个例子来说:

这个 data fetching function 初步看下来是可以被 Server 与 Client Components 共用的,但如果你对 Next.js 比较熟悉或是眼睛比较锐利,其实就会发现上面用的 environment variable 并不是 NEXT_PUBLIC 开头的,按照 Next.js 的定义,这个 env 就只能在 Server Side 才能存取到。所以如果这个 function 被 Client Component 执行,该 env 会得到 undefined,会导致获得错误的执行结果。

为了避免这种 Client Side 与 Server Side 特定 features 误用的问题,Next.js 官方建议使用 server-only 这个 npm package,它可以在当有误用的状况发生时在 build-time 就可以抓到 error (反过来的,其实也有 client-only 这个 package 可以使用)。

透过 server-only package 限制 getData function 只能被 Server Components 使用

Next.js 官方也建议如果可以的话,请尽量把 Client Components 放到 component tree 的叶子节点中,这样做的好处是可以尽量减少要传给 client 端的 JavaScript bundle size,对于前端应用程式的效能来说是有帮助的。

共享资料是前端应用一个重要的功能,那么 Server Component 与 Server Component 之间要如何共享资料呢?讲到 sharing data,React 开发者可能都会自动联想到 Context,不过很可惜,在 Server Component 中是无法使用 React Context 的,但我们却可以利用一些 Design Patterns 例如 Singleton Pattern 搭配 JavaScript 的 module system 来达成在多个 Server Components 间共享资料。底下的例子就是在示范如何在多个 Server Components 间共享资料库的连线:

以上的内容大致简介了在 Next.js 13 中是如何整合 Server Components 这个概念,可以发现比起 React Core Team 当初提供的范例,它已经做出了非常多的修改,例如不用副档名做区别,还有在 Next.js 中目前并没有 share component 的概念,而如果不出意外的话,等到 stable 的版本推出,刚刚介绍的使用方式可能又会做出改变,我们必须先有这个认知与共识。

Data Fetching

App Directory 引入 Server Components 之后,最大的改变就是 data fetching 的模式了,而在今天的内容中,针对 data-fetching 我想讨论以下几个重点:

  • The fetch() API

  • 在 Server Components 做 data fetching

  • Component-Level data fetching & Caching

  • Parallel and Sequential data fetching

  • Static & Dynamic data fetching

The fetch() API

在 Next.js Server Components 中,官方建议使用 fetch () 这个 Web API 搭配 async/await 来做 data fetching,不过这个 fetch API 并不是以往我们熟悉的 fetch,而是经过封装的版本,它主要多了两个重要的功能:

  • automatic request deduplication

  • 提供更丰富的 options object 让使用者可以传入,使每个 request 可以分别设置 caching 与 revalidate 的规则

而这两点我们会在稍后分别介绍,现在只需要知道在 Next App Directory 中一般常使用 fetch 这个 API 来做 data fetching 就可以了。

在 Server Components 做 data fetching

其实上面的段落在介绍 Server Components 时就有提到,我们可以在 Server Components 做 data fetching,而这样做有一些好处,例如:

  • 我们对于 backend data resources 有直接的存取权,例如 Database 或是 file system

  • 在 Server Side 可以避免一些敏感资讯泄漏到 client side,例如 access token 或 API key… 等等。

  • 让资料的抓取与渲染在同一个环境中进行,这么做可以减少 client side 与 server side 间 back-and-forth 的沟通,也可以尽量减少 client side main thread 的工作量。

  • 也许可以在更靠近 data source 的状况下执行 data fetching,减少延迟以优化效能。

  • 减少 client-server 间的 waterfall 请求 (典型的例子是一层又一层的 useEffect)。

在 Server Components 中可以搭配 async/await 做 data fetching

需要补充说明的是,这并不代表我们就不能在 client components 做 data fetching ,毕竟偶尔还是会有这方面的需求(举例来说,当使用者点击时去抓某些资料)。不过 Next.js 官方建议如果要的话使用 swr 或是 react-query 等 third-party library 会比较适合。

在过去,在不使用上述提到的第三方软件包的状况下,如果在 Client Components 里面我们想要做 data fetching,并且还想要处理 loading 时跟发生 Error 时的状态,我们可能会这样写:

单就 Call API 的情境来说其实有点过于复杂。未来 React 官方也有可能会推出支持处理 promise 并搭配 Suspense 的 use() hook。(详情请参考 React RFC)。

它的使用方式很像 async/await,并且可以搭配 Suspense 与 React Error Boundary 使用,所以我们就不用自己定义 error 跟 loading 的 state,同一个情境,在单一 component 的程序代码变得简洁许多。

不过这个新的 hook 仍然在 RFC 的阶段,需要静待它未来的发展了。

Component-Level data fetching & Caching

在过去的 Next.js 版本,我们如果要在 Server Side 做 data fetching,需要透过在 page 的 root file 才有提供的一些 built in function 才能做到,例如 getServerSideProps, getStaticProps… 等等,可以说 data fetching 的思考模式是 page-level 的,我们需要在 page 的顶层把资料抓取完再依靠 props 的方式传给底层的组件,但有了 Server Components 之后,我们可以做到更细致的 Component-Level data fetching。

Next 官方也建议把 data fetching 的 function colocate 在需要这些 data 的 server component 旁边,例如上一个段落一样的例子:

假设我们想要在 page 的最上层与页面底下的某个组件共享同一个 fetch request 得到的资料,我们可以分别在 page component 与 child component 都呼叫这个 fetching function,而不是在上层的 page component 抓取后透过 props 传到子组件里,也就是大家一定听过的 Prop Drilling 这种多层次传递资料的模式 (你可能会想到 React Context,但很可惜,在 RSC 里面是不能用 Context 的)。你可能又会想,这样不是会打两次 request 造成效能问题?还记得刚刚提到 Next.js 封装了 fetch API,实作了「automatic request deduping」,这个概念其实就是 Next.js 实作了一个快取,如果发现是重复的 request(同一个 render cycle 中),就直接回传快取的结果,而不会再发出一次网路请求。

那如果我们的请求不是透过 fetch API 呢?毕竟在 Server Components 可以存取到的 data source 非常多元,假设是直接存取 DB 呢?要怎么做到 automatic request deduplication?

React 在某个 RFC 中提出了 cache function,可以将传入的 function 回传的结果快取起来,只要带入的参数不变,就可以从快取回传值。至于如何 invalidate cache,则要等待 React 社群对这个 API 未来的进展 (目前有说会有专门针对这个 API 的 RFC,但在写这篇文章的当下似乎还没出来)。

其实刚刚提过 fetch API 之所以可以做到 automatic request deduplication,就是经过这个 cache function 的封装。

值得一提的是,fetch API 针对 POST 请求并不会做 automatic request deduplication,所以当我们是用 GraphQL 的 POST endpoint 来做 data fetching 时,可以透过 cache function 开启这个行为。

Parallel and Sequential data fetching

提到在 Server Components 中做 data fetching,我们心中要有两种模式

  • Parallel Data Fetching

  • Sequential Data Fetching

蓝色为 request fetching ,紫色为 rendering

透过 Parallel Data Fetching,同一个页面的多个请求可以同时被 init 与 load data,这样避免 waterfall 的方式可以尽量减少总共花费的时间。

有时候你会需要请求是 Sequential 进行的,例如你需要第一个 request 拿回来的 data 作为第二个请求的 input,这种方式通常会需要花费较长的时间。

接着我们来看看在 Next.js Server Components 中分别要怎么实现这两个模式。

Parallel Data Fetching

要实现 Parallel Data Fetching,可以在 page component initiate 所有的 promise request,并搭配 Promise.all 等待所有的 promise 都 resolve。

这边要注意我们是先开始各个请求再呼叫 await,所以各个请求可以同时开始抓取资料,避免 waterfall 的情形。

但以上这种 Pattern 有一个潜在的问题,当其中一个 Promise 需要较长的时间才能 resolve,因为 Promise.all 的关系,我们会卡在那等待,不会进到 return function,因此使用者在所有请求都 resolve 前是看不到内容的,这可能对 UX 来说不是太好的体验。因此我们可以看另一个版本的写法:

对 UX 更好的 Parallel Data Fetching

在这个版本中,我们把需要耗费比较多时间的 data fetching promise 丢到下层组件去 await,并用 Suspense 把该组件包起来,这样在 artistData 先被 resolve 后,使用者可以先看到 {artist.name} 跟 suspense 的 loading UI,以使用者体验的角度来说是比较好的模式。

(关于 Parallel Data Fetching,官方还有提出一个更进阶的 Preload Pattern,有兴趣的朋友可以再自行看看)

至于 Sequential Data Fetching 也十分直觉

需要等到上层 page component 先抓取 id 并传入后才会开始在下层 component 做 data fetching。

这种将 data fetching 写在下层 component 的写法,需要等上层的 layout 或是 page component 完成抓取后才能够开始,造成 waterfall 的情形。

Static & Dynamic data fetching

提到 data fetching,我们可以更进一步思考资料的种类,一般来说,资料可以分为两种:

  • Static Data: 不会经常变动的资料,例如博客文章,这种资料就很适合放到快取中。

  • Dynamic Data: 会频繁更动的资料,例如文章留言。

我们在上面看了不少用 fetch API 抓取资料的例子,Next.js 预设会做 Static 的 data fetching,也就是说 Next 会在 build time 做 data fetching,并且存到快取中并在之后 reuse,这样做主要有两个好处:

  • 减少 server 与资料库的负担

  • 减少页面的 loading time

不过刚刚也说有些资料是需要频繁更新的,这时候一律做 static fetch 就不太合理了,开发者应该要对这个行为有更高的控制权。刚刚有提到 Next 封装了 fetch API,它其实就可以让我们做到对每一个 request 快取的控制。

透过 fetch API 的第二个对象参数,我们就可以控制每一个请求的快取行为,我们可以发现这些行为跟 Next 过去版本的 getServerSideProps, getStaticProps 可以做到 SSR, SSG, ISR 非常类似,不过过去我们只能控制 page level 的 requests,而在 Next13,我们可以以 per-request 的角度去思考这件事。

关于 Next.js 13 App Directory 的 data fetching,其实还有很多 features 没有 cover 到,建议有兴趣的读者进一步阅读官方文件的两篇文章:

  • https://beta.nextjs.org/docs/data-fetching/fundamentals

  • https://beta.nextjs.org/docs/data-fetching/fetching#static-data-fetchingmd

Streaming and Suspense

Next.js 在 App Directory 中,还引入了 Streaming with Suspense 这个功能。

要了解 Streaming 是怎么运作的,我们得先了解过去 SSR 的运作机制与它所受到的限制。过去如果页面是采用 SSR,需要经过几个步骤,使用者才能看见一个完整且能够进行互动的网页:

  • 在 server side 抓取想要的资料

  • Server render 出 HTML

  • 页面的 HTML, CSS, JS 被送到 client side

  • 使用者这时候可以看到画面,但还不能进行互动

  • React 进行 hydration,赋予 UI 互动能力

从上图就可以看出这些流程是顺序执行且 blocking 的,server 必须抓取完所有资料才能 render HTML,而在 client side,React 必须等到当前页面中所需要的元件的 JS code 都被载入后才能开始进行 hydration。

Next.js 透过这种方式让使用者可以尽快看到画面,尽管还是没办法互动的,不过以使用者体验的角度来看却可以减少用户等待页面 loading 的时间。

不过,如果我们需要在 Server Side 抓取大量的资料,那么页面的载入就会变得很慢,使用者看到页面的时间会因此被推延。而 Streaming 带来了解决这个问题的机会。

Streaming 让我们把页面的 HTML 拆分成多个较小的 chunks,并「渐进式」的把这些 chunks 从 server 端传送到 client 端。

Streaming

这么做可以让页面的部分组件可以比较快速地显示,而不用等待所有 data fetching 都完成后才能显示任何的 UI。

而 React 的 component model 其实刚好非常适合 Streaming,因为每个元件我们都可以把它想像成一个 chunk,对网页应用来说比较重要拥有较高 priority 的元件(例如说产品的资讯)或是不需要抓取资料的元件(例如 layout)就可以优先传送到 client side 让 React 更早的处理它们。相较之下,priority 比较低的元件可以在完成 data fetching 或复杂计算后再以 streaming 的方式传到 client 端。

如果你想要避免耗时的 data fetching block 到页面的渲染,造成一些指标分数的表现降低,例如 TTFB, FCP, TTI,那么 Streaming 也许可以给予极大的帮助。

SSR with Streaming

(关于 React 的 Streaming SSR,可以更进一步参考 GitHub 上的 discussion)

在 Next.js App Directory 中,我们可以把需要执行一些非同步操作,例如 data fetching 的元件用 <Suspense> 元件包起来,并且指定 fallback 的 UI,例如 loading spinner,并在该元件准备好后替换成完整的内容。

其实 suspense for data fetching 对于 React 官方来说还是一个 experimental 的 feature,Next.js 虽然抢先一步尝试,但也有很多方面是还没有很好的 solution 的,例如说官方的文件提供的范例都是在 Server Components 做 data fetching 并搭配 Suspense 显示 loading UI,至于如果要在 client components 做到类似的事,官方并没有说明如何做到,我们可能需要刚刚提过的 React 新的 “use” hook 才能协助处理 Promise,而这个 function 还在 React RFC 阶段,未来应该还会充满许多变数。

目前版本遇到的限制

我自己实际用 Next 13 App Directory 开发一些 side project 后,发现现有的版本其实遇到蛮多限制的(另外还蛮容易遇到 Bug 的,不过这就先不谈了)。

例如一开始在做技术选型时,我原本想要用自己习惯的 styled-components 这个 CSS-in-JS 的 framework,不过后来发现 Server Components 目前并不支援 runtime 的 CSS-in-JS solution。

再来像是在使用很多第三方套件时,如果该套件需要用到 client state 相关的功能,套件维护者却没有支援 client components,我们就需要手动做这件事情才能使用它们。

另外现行的版本对于 data 的 mutation 还没有很好的解决方案,例如 server components 抓取了一个 TODO list 的阵列资料,使用者可以更改每个 todo 的状态,但目前没有一个很好的方式去 trigger server components 重新抓取新的资料或是更新快取,只能透过 refresh 的 workaround 方式 (看起来它是极没有效率的从 Server Component Root 重新抓取一次,而我们希望的应该要是 Partial 的 Sever Components Re-Render),详情可以看官方的文件说明。

这边只是大概列了几点 Server Components 在开发上的限制,实际上一定还会随着开发的应用复杂度提升,会遇到更多的问题。这也许代表离 Server Components 变成稳定且主流的开发方式,我们还需要再等待一段时间。

Future Roadmap

关于 Next.js 13 App Directory 目前已经支援的功能以及正在进行开发的功能,可以参考官房提供的 Roadmap,相信未来支援的功能会越来越稳定与丰富。

  • https://beta.nextjs.org/docs/app-directory-roadmap

结论

未来如果 Server Components 正式推出了,想必会大大改变我们过往熟悉的 React 开发方式。透过这次研究这个新功能,我不禁再次感叹前端开发真的是一个很有趣的领域,正所谓分久必合,合久必分。前端从一开始的模板语言,到后来推崇的前后端分离,如今看似走了回头路,却又实际解决了一些过往处理起来非常棘手的问题。这个功能的推出也宣告着前端开发者不能仅仅着重在纯前端的问题,了解基本的后端知识与 JS 的 Server Side Runtime 几乎成了必备的能力,这部分就让我们继续看下去吧!

至于 Next.js 在最新版本提出了整合 Server Components 的 App Directory,虽然并没有正式发布,也还有很多问题待解决,但可以看出它们对于 Server Components 这项技术的重视与信心,因此我认为在现在就去接触了解绝对不嫌早!

关于本文
作者:@莫力全 Kyle Mo
原文:https://oldmo860617.medium.com/ 從 - next-js-13 - 認識 - react-server-components-37c2bad96d90

这期前端早读课
对你有帮助,帮” 赞 “一下,
期待下一期,帮” 在看” 一下 。

相关阅读

  • 第一次!民事、行政二审案件也可以网上立案啦!

  • “请问二审立案要多久呀,我有急事要回老家,在外地能申请上诉吗?”“别担心,现在网上就能立案了!在手机上搜索人民法院在线服务平台(陕西)微信小程序,方便快捷!”为便于当事人依法行使
  • 规划信息化历史资料征集倡议书

  • 规划信息化历史资料征集倡议书目前,中国城市规划学会城市规划新技术应用专业委员会正在开展规划信息化的历史资料整理工作,现向从业人员征集相关材料,以便更好地了解我国规划信
  • 警惕!扫码支付出现新骗局

  • 近日,手机支付出现新骗局骗子利用“收付款码切换”疯狂作案李女士经营着一家蛋糕店2月16日有人添加李女士为微信好友说要预定一个蛋糕双方确定好蛋糕的款式需要付款时套路出
  • 警惕!扫码支付出现新骗局→

  • 近日,手机支付出现新骗局骗子利用“收付款码切换”疯狂作案李女士经营着一家蛋糕店2月16日有人添加李女士为微信好友说要预订一个蛋糕双方确定好蛋糕的款式需要付款时套路出
  • B端设计|页面标签的认识和实操应用

  • 今天来做一个比较简单的分享,也是很多在做B端项目的同学发出过的疑问,那就是既然浏览器中可以使用页面标签,那为什么在项目中还需要使用这个组件和交互框架的形式。页面标签是
  • 【第2905期】远程组件实践

  • 前言从服务端远程下载一个 JS 文件并注册成组件。今日前端早读课文章由 @崔丽分享,公号:奇舞周刊授权。正文从这开始~~一、什么是远程组件这里是指在生产环境中,从服务端远程下载

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • 【第2914】从Next.js 13 认识React Server Components

  • 前言一个认识 React Server Components 的好时机。今日前端早读课文章由 @莫力全 Kyle Mo 授权分享。前端早读课:重视你的信息价值密度正文从这开始~~React Server Components
  • 收视破纪录,《人生之路》凭什么打动两代人?

  • 作者 | 郑乔尹又一年考研复试放榜季到来。每当这样的时间节点来临,一些回忆便涌上不少人的心头。在微博话题#你有没有为自己拼过命#下,不少人都纷纷回想起自己的高考时光。
  • 【早说】信息类型

  • 面对四种不同的信息,你会怎么做呢?现在现在这种环境,信息会导致越加焦虑浮躁。乔哈里窗把人们的信息 (或者意识,personalawareness) 划分为四种类型:第一种,我知道、你也知道的信
  • 饿了么开源自研多端框架 MorJS

  • MorJS 是什么?开源地址:https://github.com/eleme/morjs简介Mor (发音为 /mɔːr/,类似 more) 是饿了么开发的一款基于小程序 DSL 的,可扩展的多端研发框架。使用 MorJS,我们只需