React 18 实现一个支持 Suspense 的 renderToString

2022-09-08 · 492 chars · 3 min read

React 18 在服务端渲染(SSR)方面有很大的提升,特别是 Suspense。但是 renderToString 方法却不支持与其一起使用(支持有限),直接使用会输出如下的 HTML:

<div><!--$!--><template data-msg="The server did not finish this Suspense boundary: The server used &quot;renderToString&quot; which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to &quot;renderToPipeableStream&quot; which supports Suspense on the server" data-stck="
    at div
    at Suspense
    at div"></template><p>loading...</p><!--/$--></div>

官方的建议是用 renderToPipeableStream (Node.js) 或者 renderToReadableStream (for Web Streams) 代替。但是在一些场景下,直使用 stream 还是太重了,而且也没有必要。比如 SSG,我们是期望在构建时能直接生成 HTML 的。

这里分享一个代码片段,实现支持 SuspenserenderToString,将 renderToPipeableStream 封装在内部。stream 属于 “一看就会,一写就废”,使用成本较高。

import { Writable } from 'node:stream'
import { renderToPipeableStream } from 'react-dom/server'

const renderToString = (element) =>
  new Promise((resolve, reject) => {
    const stream = renderToPipeableStream(element, {
      onAllReady() {
        const chunks = []

        const writable = new Writable({
          write(chunk, encoding, callback) {
            chunks.push(Buffer.from(chunk))
            callback()
          },
        })

        writable.on('error', (error) => reject(error))

        writable.on('finish', () => {
          resolve(Buffer.concat(chunks).toString('utf8'))
        })

        stream.pipe(writable)
      },
      onError(error) {
        reject(error)
      },
    })
  })

export default renderToString

示例#

先写两个 React 组件,代码如下:

// App 组件

import React, { Suspense, lazy } from 'react'

const OtherComponent = lazy(async () => {
  // 延迟
  await new Promise((resolve) => {
    setTimeout(() => {
      resolve()
    }, 1000)
  })

  return import('./other-component.jsx')
})

const App = () => (
  <div>
    <Suspense fallback={<p>loading...</p>}>
      <div>
        <OtherComponent />
      </div>
    </Suspense>
  </div>
)

export default App
// OtherComponent 组件

import React from 'react'

const OtherComponent = () => <p>hello world!</p>

export default OtherComponent

执行 render:

import renderToString from './render-to-string.js' // 上面的代码段
import App from './dist/app.js'

const html = await renderToString(App());

console.log(html);

输出如下:

<div>
  <!--$-->
  <div>
    <p>hello world!</p>
  </div>
  <!--/$-->
</div>

最后说明下,这段代码是我本地写 Demo 时用的,未经严格测试,大家如果要使用的话,记得好好测试下。

赞赏

微信