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,
}
}