HTML streaming

Examples & docs

Examples:

Stream 文档 & API:

基础

// renderer/_deault.page.server.js

export { render }

import { escapeInject } from 'vite-plugin-ssr'
import { renderToStream } from 'some-ui-framework' // React, Vue, ...

async function render(pageContext) {
  const { Page } = pageContext

  const stream = renderToStream(Page)

  return escapeInject`<!DOCTYPE html>
    <html>
      <body>
        <div id="page-view">${stream}</div>
      </body>
    </html>`
}

Node.js 平台 (Vercel, AWS EC2, AWS Lambda, ...):

// server.js

import { renderPage } from 'vite-plugin-ssr'

app.get('*', async (req, res, next) => {
  const pageContextInit = { urlOriginal: req.url }
  const pageContext = await renderPage(pageContextInit)
  const { httpResponse } = pageContext
  if (!httpResponse) return next()
  // `httpResponse.pipe()` works with Node.js Streams as well as Web Streams.
  httpResponse.pipe(res)
})

Edge 平台 (例如 Cloudflare Workers):

// worker.js

import { renderPage } from 'vite-plugin-ssr'

addEventListener('fetch', (event) => {
  event.respondWith(handleFetchEvent(event))
})

async function handleFetchEvent(event) {
  const pageContextInit = { urlOriginal: event.request.url }
  const pageContext = await renderPage(pageContextInit)
  const { httpResponse } = pageContext
  if (!httpResponse) {
    return null
  } else {
    // `httpResponse.getReadableWebStream()` 仅适用于网络流
    const readable = httpResponse.getReadableWebStream()
    const { statusCode, contentType } = httpResponse
    return new Response(readable, {
      headers: { 'content-type': contentType },
      status: statusCode,
    })
  }
}

pageContext.enableEagerStreaming

默认情况下,HTML 模板(render() hook 中提供的)不会立即写入流:相反,vite-plugin-ssr 等待 UI 框架写入流

import { renderToStream } from 'some-ui-framework' // React, Vue, ...

async function render(pageContext) {
  const { Page } = pageContext

  const stream = renderToStream(Page)

  // HTML 模板(例如 `<title>`)不会立即写入流
  // 相反,vite-plugin-ssr 等待 `stream` 开始
  return escapeInject`<!DOCTYPE html>
    <html>
      <head>
        <title>Hello</title>
      </head>
      <body>
        <div id="page-view">${stream}</div>
      </body>
    </html>`
}

如果我们将 pageContext.enableEagerStreaming 设置为 true,那么 vite-plugin-ssr 会立即开始编写 HTML 模板

async function render(pageContext) {
  // HTML 模板(例如 `<title>`)立即写入流
  const documentHtml = escapeInject`<!DOCTYPE html>
    <html>
      <head>
        <title>Hello</title>
      </head>
      <body>
        <div id="page-view">${renderToStream(pageContext.Page)}</div>
      </body>
    </html>`

  return {
    documentHtml,
    pageContext: {
      enableEagerStreaming: true,
    },
  }
}

Stream 转 string

我们可以将流转换为字符串:

/* 不会生效(不能同步使用流)
const { body } = httpResponse
res.send(body)
*/

// 但是我们可以:
const body = await httpResponse.getBody()
assert(typeof body === 'string')
res.send(body)

Stream pipes

为了开启 stream pipes,我们需要使用 stampPipe()

// renderer/_deault.page.server.js

export { render }

import { renderToStreamPipe } from 'some-ui-framework' // React, Vue, ...
import { escapeInject, stampPipe } from 'vite-plugin-ssr'

async function render(pageContext) {
  const { Page } = pageContext

  const pipe = renderToStreamPipe(Page)

  // 如果 `pipe(writable)` 期望 `writable` 是一个可写的 Node.js Stream
  stampPipe(pipe, 'web-stream')
  // 如果 `pipe(writable)` 期望 `writable` 是一个可写的 Web Stream
  stampPipe(pipe, 'node-stream')

  return escapeInject`<!DOCTYPE html>
    <html>
      <body>
        <div id="page-view">${pipe}</div>
      </body>
    </html>`
}

For Node.js:

// server.js

const pageContext = await renderPage(pageContextInit)
const { httpResponse } = pageContext
// 像平常一样使用 `httpResponse.pipe()`
httpResponse.pipe(res)

对于像 Cloudflare Worker 这种需要可读流的 Edge 平台,我们可以使用 new TransformStream()

// worker.js

const { readable, writable } = new TransformStream()
httpResponse.pipe(writable)
const resp = new Response(readable)

对于像 Vue 这样的 UI 框架,我们需要一个管道包装器

// renderer/_deault.page.server.js

import { pipePageToWritable } from 'some-ui-framework'
import { stampPipe, escapeInject } from 'vite-plugin-ssr'

export function render(pageContext) {
  const { Page } = pageContext

  // 为了 `pipePageToWritable()` 可以访问 `Page`,我们使用管道包装器
  const pipeWrapper = (writable) => {
    pipePageToWritable(Page, writable)
  }
  stampPipe(pipeWrapper, 'node-stream')

  return escapeInject`<!DOCTYPE html>
    <html>
      <body>
        <div id="page-view">${pipeWrapper}</div>
      </body>
    </html>`
}

有关将管道包装器与 Vue 的 pipeToWebWritable()/pipeToNodeWritable()一起使用以及在 Cloudflare Workers 中使用 new TransformStream() 的例子,请参考 /examples/cloudflare-workers-vue

流结束后初始化数据

一些数据获取工具,例如 Relay,仅在流结束后才提供初始数据

这种情况,我们可以在 render() hook 中返回 pageContext promise:

// renderer/_deault.page.server.js

export { render }
export { passToClient }

import { escapeInject } from 'vite-plugin-ssr'
import { renderToStream } from 'some-ui-framework' // React, Vue, ...

const passToClient = ['initialData']

async function render(pageContext) {
  const { Page } = pageContext

  const stream = renderToStream(Page)

  const documentHtml = escapeInject`<!DOCTYPE html>
    <html>
      <body>
        <div id="page-view">${stream}</div>
      </body>
    </html>`

  const pageContextPromise = (async () => {
    return {
      // 流结束后才被提供的 `initialData`
      initialData,
    }
  })()

  return {
    documentHtml,
    pageContext: pageContextPromise,
  }
}