Análise do componente React Server

prefácio

O conceito de React Server Component (doravante referido como RSC) foi apresentado por um longo tempo, mas eu tenho apenas um pouco de compreensão dele. Desta vez, vou usar o feriado de primeiro de maio para descobrir. Vamos aprender através do exemplo no site oficial , mas este exemplo também precisa instalar o postgres, para simplificar, usamos outra versão do fork .

Demonstração do componente Shallow Play Server

Depois que as dependências estiverem instaladas e iniciadas, abra-o no navegador http://localhost:4000e você verá a seguinte página:

1.png

Este é um aplicativo simples de "notas" (componente App). O lado esquerdo contém pesquisa (SearchField), novo botão (EditButton), lista de notas (NoteList) e outros componentes, e o lado direito é o detalhe ou novo componente de nota (Nota ). Dentre eles, o componente azul é o Componente Cliente (somente renderizado no lado do cliente, doravante denominado CC), e o vermelho é o Componente Servidor (somente renderizado no lado servidor, doravante denominado SC). Além disso, existe também um Componente Compartilhado, ou seja, um componente que pode ser usado nas duas pontas, quando é introduzido por CC, passa a ser CC e vice-versa.

O chamado SC é para renderizar apenas no lado do servidor, e seu código não aparecerá no lado do cliente:

2.png

A vantagem disso é que pode economizar muita transmissão de código JS e deixar o cliente mais leve.

Como a renderização é realizada no lado do servidor, muitas APIs do lado do servidor podem ser usadas no SC, como obter dados do banco de dados por meio do SQL:

3.png

Sem dúvida, isso é útil para melhorar o desempenho. Originalmente, para renderizar um componente, você precisa fazer o download do código JS primeiro e, em seguida, obter os dados por meio da solicitação da API antes da renderização. Agora você pode obter diretamente o componente serializado renderizado no lado do servidor, o que economiza uma solicitação de rede.

Então, como isso funciona? Vamos dividi-lo brevemente.

Análise do princípio de realização do Componente Servidor

Vamos começar com a entrada do cliente:

export default function Root({initialCache}) {
  return (
    <Suspense fallback={null}>
      <ErrorBoundary FallbackComponent={Error}>
        <Content />
      </ErrorBoundary>
    </Suspense>
  )
}

function Content() {
  const [location, setLocation] = useState({
    selectedId: null,
    isEditing: false,
    searchText: '',
  })
  const response = useServerResponse(location)
  return (
    <LocationContext.Provider value={[location, setLocation]}>
      {response.readRoot()}
    </LocationContext.Provider>
  )
}

export function useServerResponse(location) {
  const key = JSON.stringify(location)
  const cache = unstable_getCacheForType(createResponseCache)
  let response = cache.get(key)
  if (response) {
    return response
  }
  response = createFromFetch(
    fetch('/react?location=' + encodeURIComponent(key))
  )
  cache.set(key, response)
  return response
}
复制代码

O código acima tem dois pontos principais:

  1. 页面如何渲染取决于 response.readRoot() 的返回
  2. 调用 useServerResponse 会发起一个 /react?location= 的请求

第一次渲染的时候,由于 /react?location= 请求还没有返回,response.readRoot() 会 throw 一个 Chunk 对象:

4.png

之前在 React 之 Suspense 中有提到过 React 在进行渲染时有 try catch 的逻辑,不过那里的 error 是 Promise 对象,这里是 Chunk 对象而已。同样的,React 会 catch 住这个错误,并显示最近 Suspense 的 fallback,等到 Chunk 准备好了才会开始渲染。

接下来我们看看 /react?location= 的返回内容:

5.png

该数据中,每一行表示一个 Chunk,每一行格式如下:

第一个字母表示 Chunk 的类型。M 表示 Module,等于 Webpack 中的 Module,即我们写的组件;S 表示 Symbol,即 React 的内置组件;J 表示 Model,用于描述整个应用的模型。

第二个数字表示 Chunk 的编号,可以通过它来唯一索引一个 Chunk

冒号后面的内容表示 Chunk 的具体数据。

对于这个 Demo,readRoot 时会去获取第 0 个 Chunk,我们把它格式化一下:

[
  "$",
  "div",
  null,
  {
    "className": "main",
    "children": [
      [
        "$",
        "section",
        null,
        {
          "className": "col sidebar",
          "children": [
            [
              "$",
              "section",
              null,
              {
                "className": "sidebar-header",
                "children": [
                 ...
                ]
              }
            ],
            [
              "$",
              "section",
              null,
              {
                "className": "sidebar-menu",
                "role": "menubar",
                "children": [
                  ["$", "@1", null, {}],
                  ["$", "@2", null, {"noteId": null, "children": "New"}]
                ]
              }
            ],
            ...
          ]
        }
      ],
      ...
    ]
  }
]
复制代码

经过格式化后是不是很眼熟。其实他就是 App.server.js 中组件 App 序列化后的数据:

export default function App({selectedId, isEditing, searchText}) {
  return (
    <div className='main'>
      <section className='col sidebar'>
        <section className='sidebar-header'>
          <img
            className='logo'
            src='logo.svg'
            width='22px'
            height='20px'
            alt=''
            role='presentation'
          />
          <strong>React Notes</strong>
        </section>
        <section className='sidebar-menu' role='menubar'>
          <SearchField />
          <EditButton noteId={null}>New</EditButton>
        </section>
        <nav>
          <Suspense fallback={<NoteListSkeleton />}>
            <NoteList searchText={searchText} />
          </Suspense>
        </nav>
      </section>
      <section key={selectedId} className='col note-viewer'>
        <Suspense fallback={<NoteSkeleton isEditing={isEditing} />}>
          <Note selectedId={selectedId} isEditing={isEditing} />
        </Suspense>
      </section>
    </div>
  )
}
复制代码

这个序列化的数据返回给 client 后会被重新解析成 ReactElement 对象:

6.png

其中,数据中 sidebar-menuchildren 是这样表示的:

...
className: 'sidebar-menu',
children: [
  ['$', '@1', null, {}],
  ['$', '@2', null, {noteId: null, children: 'New'}],
],
...
复制代码

这里的 @1, @2 表示其子组件分别是 1 号 和 2 号 Client Component:

5.png

这里还有一个值得学习的地方是在将 JSON 数据格式化为 ReactElement 时使用了 JSON.parse 的第二个参数,我们可以通过一个简单的例子来练习一下:

const jsonStr = JSON.stringify([
  'div',
  {
    className: 'cls1',
    children: [
      ['span', {className: 'cls2', children: 'Hello'}],
      ['span', {className: 'cls3', children: 'World'}],
    ],
  },
])

const dom = JSON.parse(jsonStr, (key, value) => {
  if (Array.isArray(value) && typeof value[0] === 'string') {
    const ele = document.createElement(value[0])
    ele.className = value[1].className
    const children = value[1].children
    if (Array.isArray(children)) {
      children.forEach((child) => ele.appendChild(child))
    } else {
      ele.appendChild(document.createTextNode(children))
    }
    return ele
  }
  return value
})

console.log(dom.outerHTML)
复制代码

总结一下,所谓的 Server Component 其实就是在 Server 端将组件进行序列化后返回给 Client 端,Client 端再解析成 ReactElement 而已。因为组件要经过序列化后传输,所以 Server Component 不能有 Function 等无法序列化的参数类型,这也是为什么 Server Component 中不能有跟用户交互相关的代码。

RSC vs SSR

RSC 因为名字中带有 Server,很自然地会让我们跟 Server Side Rendering(SSR)进行对比,接下来我们就来讨论一下,我们先从 Client Side Rendering(CSR)开始说起。

CSR 我们很熟悉了,它的流程大概如下所示:

componente-servidor-CSR.png

它有如下缺点:

  1. 由于 Client 端从 Assets Server 获取到的 HTML 是一个空壳,页面内容需要等到 API Server 的数据返回后才能渲染出来,所以 CSR 的 First Contentful Paint(FCP)指标会比较差,且不利于 SEO。

  2. 随着应用的不停迭代,JS Bundle 的体积可能会越来越大,这会进一步拖慢 FCP,一般会用路由懒加载等方式来优化。

为了解决上面的问题,就出现了 SSR,它的流程如下所示:

componente-servidor-SSR.png

与 CSR 不同的是,初始访问页面时,API 请求从 Client 端移到了 Assets Server 端,由于 Assets Server 与 API Server 之间的通信效率一般要高于 Client 和 API Server,所以这样是有助于提高页面加载的速度。

并且,Assets Server 返回给 Client 的是有内容的 HTML,对于 SEO 也是有好处的。

不过 SSR 仍然存在一些问题:

  1. SSR 虽然提升了 FCP,但是整个应用仍然要等到“注水”完成后才能进行交互,也就是 Time to Interactive (TTI)指标仍然比较差。

  2. 当跳转到其他页面时,其流程还是跟 CSR 是一样的,没有充分利用到 Assets Server 和 API Server 之间的通信效率。

接下来,我们看看 RSC:

componente-servidor-SC.png

整个流程前面两步跟 CSR 类似,第三步会发起 react? 的请求用来获取序列化后的组件数据,之后重点来了,第三步(返回序列化的组件数据)和第四步(从 API Server 获取数据)是并行的,我们可以在上面例子 NoteList.server.js 文件中加上这段代码来模拟:

export default function NoteList({searchText}) {
  ...
  // Now let's see how the Suspense boundary above lets us not block on this.
  fetch('http://localhost:4000/sleep/10000')

  return ...
}
复制代码

此时,页面列表部分会展示一个骨架:

dormir.png

直到 API 请求返回后才会更新为真正的内容(第六步)。

而跳转到下一个页面也仍然是这样的流程,只是不需要第一二步了而已。

这样有几个好处:

  1. 由于第三步和第四步是同时进行的,用户可以更快的进行交互,提升了 TTI 指标。上面的例子中,我们可以在列表还没有渲染出来时就点击 New 按钮进行新增。但 React18 提出的 Streaming Server Rendering with Suspense 似乎也能达到这个效果,关于这个,可以查看小弟另一篇文章React SSR Stream Rendering & Suspense for Data Fetching.

  2. 即使跳转到了下一个页面,接口请求的逻辑仍然发生在 Assets Server 和 API Server 之间,可以充分利用到两者之间的通信效率。

这样看来,RSC 的出现,确实是有一定的道理的。不过,聪明的你一定发现了,RSC 好像在 SEO 方面并没有任何优势。所以,我觉得未来可能会把两者结合起来,整个应用仍然是基于 RSC 的模型来开发,当用户首次访问应用时,在 Server 端返回渲染好的 HTML:

Serialized Component ----> ReactElement --(react-dom/server)--> HTML
复制代码

当用户跳转到下一个页面时,在 Client 解析序列化后的组件为 DOM:

Serialized Component ----> ReactElement --(react-dom/client)--> DOM
复制代码

思考

RSC 虽然学习完了,但是还是不免想多说几句。

前端这个领域,新框架新概念层出不穷,有时不免会想,为什么一个简单的网页开发工作会变得越来越复杂?

先不急着回答这个问题,还是上面的这个例子,如果我们回到 10 年前,会怎么开发它呢?答案也有很多种,不过一定有一种是这样的:

  1. 选一门后端语言(如 Node.js),选择个模板引擎(如 mustache),获取数据,后端直出 HTML。
  2. JS 写点用户交互逻辑(可能会用到 jQuery),CSS 写点样式。

Vendo uma maneira tão original, você definitivamente reclamará. Mas não se empolgue, pode ser o melhor desempenho. como dizer? Em primeiro lugar, é provável que a eficiência de renderização do mecanismo de modelo seja maior do que a do React (afinal, o React tem o custo de executar a própria biblioteca, bem como o processo Diff etc.), portanto, o índice FCP deve ser melhor do que SSR. Ao mesmo tempo, devido à falta de muitos códigos JS relacionados à biblioteca, o tamanho do JS será muito menor e o TTI será mais rápido.

Um problema da página desenvolvida acima é que a experiência do usuário não é muito boa, antes que o HTML seja devolvido, o usuário precisa aguardar (a página fica em branco durante todo o processo). Para resolver esse problema, o Facebook propôs uma vez uma tecnologia chamada BigPipe , que pode devolver parte do conteúdo da página ao usuário primeiro. Sobre BigPipe, você também pode ler o artigo BigPipe de Node.js escrito pelo irmão mais novo . Seu princípio usa principalmente HTTP Transfer-Encoding: chunked.

Então, veja bem, sem o React e sem os conceitos de SSR, RSP e Stream Rendering, ainda podemos obter uma página da Web de alto desempenho com uma boa experiência do usuário.

No entanto, a desvantagem desse método é que o código não é propício à manutenção.Os calçados infantis que passaram por esse tipo de desenvolvimento devem se sentir da mesma forma. É por isso que surgiram as bibliotecas de UI front-end baseadas em componentes ou estruturas como React, que desde então mudou a forma de desenvolvimento web e trouxe a profissão de front-end para o palco da história.

Além disso, depois de usar essas bibliotecas ou estruturas de interface do usuário, a eficiência do desenvolvimento da página da Web também foi aprimorada. Da maneira original anterior, os desenvolvedores precisam prestar atenção ao processamento de dados e às operações do objeto DOM, mas agora eles só precisam prestar atenção ao primeiro.

Então, por que um simples trabalho de desenvolvimento web está se tornando cada vez mais complicado? Porque este é o resultado de uma consideração abrangente de desempenho, experiência do usuário, manutenção de código e eficiência de desenvolvimento.

Bem-vindo a prestar atenção à conta oficial "Front-end Tour" e obter mais artigos técnicos de front-end o mais rápido possível.

Acho que você gosta

Origin juejin.im/post/7229229834937237541
Recomendado
Clasificación