Análisis del componente del servidor React

prefacio

El concepto de React Server Component (en lo sucesivo, RSC) se ha presentado durante mucho tiempo, pero solo tengo un poco de comprensión. Esta vez, usaré el feriado del Primero de Mayo para resolverlo. Aprendamos a través del ejemplo en el sitio web oficial , pero este ejemplo también necesita instalar postgres. Para simplificar, usamos otra versión de bifurcación .

Demostración del componente del servidor de reproducción superficial

Después de instalar e iniciar las dependencias, ábralo en el navegador http://localhost:4000y podrá ver la siguiente página:

1.png

Esta es una aplicación simple de "notas" (componente de la aplicación). El lado izquierdo contiene la búsqueda (SearchField), el botón nuevo (EditButton), la lista de notas (NoteList) y otros componentes, y el lado derecho es el detalle o el componente de la nueva nota (Nota ). Entre ellos, el componente azul es el Componente del Cliente (solo representado en el lado del cliente, en lo sucesivo denominado CC), y el rojo es el Componente del Servidor (solo representado en el lado del servidor, en lo sucesivo denominado SC). Además, también existe un Shared Component, es decir, un componente que se puede utilizar en ambos extremos, cuando se introduce por CC pasa a ser CC y viceversa.

El llamado SC es para renderizar solo en el lado del servidor, y su código no aparecerá en el lado del cliente:

2.png

La ventaja de esto es que puede ahorrar una gran cantidad de transmisión de código JS y hacer que el cliente sea más ligero.

Dado que la representación se realiza en el lado del servidor, muchas API del lado del servidor se pueden usar en SC, como obtener datos de la base de datos a través de SQL:

3.png

Sin duda, esto es útil para mejorar el rendimiento. Originalmente, para renderizar un componente, primero debe descargar el código JS y luego obtener los datos a través de la solicitud API antes de renderizar. Ahora puede obtener directamente el componente serializado renderizado en el lado del servidor. lo que ahorra una solicitud de red.

¿Entonces, cómo funciona? Vamos a desglosarlo brevemente.

Análisis del principio de realización del Componente Servidor

Comencemos con la entrada del 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
}
复制代码

El código anterior tiene dos puntos clave:

  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 我们很熟悉了,它的流程大概如下所示:

servidor-componente-CSR.png

它有如下缺点:

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

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

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

servidor-componente-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:

servidor-componente-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 写点样式。

Al ver una forma tan original, definitivamente te quejarás. Pero no te emociones, puede que sea la mejor actuación. ¿cómo decir? En primer lugar, es probable que la eficiencia de representación del motor de plantillas sea mayor que la de React (después de todo, React tiene el costo de ejecutar la biblioteca en sí, así como el proceso Diff, etc.), por lo que el índice FCP debería ser mejor que SSR. Al mismo tiempo, debido a la falta de muchos códigos JS relacionados con la biblioteca, el tamaño de JS será mucho más pequeño y TTI será más rápido.

Un problema con la página web desarrollada anteriormente es que la experiencia del usuario no es muy buena, antes de que se devuelva el HTML, el usuario debe esperar (la página está en blanco durante todo el proceso). Para solucionar este problema, Facebook propuso una vez una tecnología llamada BigPipe , que puede devolver primero parte del contenido de la página al usuario. Sobre BigPipe, también puedes leer el artículo BigPipe de Node.js escrito por el hermano menor . Su principio utiliza principalmente HTTP Transfer-Encoding: chunked.

Entonces, verá, sin React y sin los conceptos de SSR, RSP y Stream Rendering, aún podemos lograr una página web de alto rendimiento con una buena experiencia de usuario.

Sin embargo, la desventaja de este método es que el código no es propicio para el mantenimiento.Los zapatos de los niños que han experimentado este tipo de desarrollo deberían sentirse de la misma manera. Es por eso que aparecieron las bibliotecas o marcos de interfaz de usuario front-end basados ​​​​en componentes como React, que desde entonces ha cambiado la forma de desarrollo web y ha llevado la profesión front-end al escenario de la historia.

Además, después de usar estas bibliotecas o marcos de interfaz de usuario, la eficiencia del desarrollo de páginas web también ha mejorado. De la forma original anterior, los desarrolladores deben prestar atención tanto al procesamiento de datos como a las operaciones de objetos DOM, pero ahora solo deben prestar atención. al primero.

Entonces, ¿por qué un simple trabajo de desarrollo web se vuelve cada vez más complicado? Porque este es el resultado de una consideración integral del rendimiento, la experiencia del usuario, la capacidad de mantenimiento del código y la eficiencia del desarrollo.

Bienvenido a prestar atención a la cuenta oficial "Front-end Tour" y obtener más artículos técnicos de front-end lo antes posible.

Supongo que te gusta

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