]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(server-renderer): decouple esm build from Node + improve stream API
authorEvan You <yyx990803@gmail.com>
Thu, 29 Jul 2021 17:12:50 +0000 (13:12 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 29 Jul 2021 17:14:02 +0000 (13:14 -0400)
- deprecate `renderToSTream`
- added `renderToNodeStream`
- added `renderToWebStream`
- added `renderToSimpleStream`

close #3467
close #3111
close #3460

packages/global.d.ts
packages/server-renderer/README.md
packages/server-renderer/__tests__/render.spec.ts
packages/server-renderer/__tests__/webStream.spec.ts [new file with mode: 0644]
packages/server-renderer/src/helpers/ssrCompile.ts
packages/server-renderer/src/index.ts
packages/server-renderer/src/renderToStream.ts

index 9b7e3795e69283b82e7e93a2db2aa6a746e6748b..007f8ffdd1929df14f6895c92629b8909bfab3eb 100644 (file)
@@ -33,3 +33,8 @@ declare module '*?raw' {
 declare module 'file-saver' {
   export function saveAs(blob: any, name: any): void
 }
+
+declare module 'stream/web' {
+  const r: typeof ReadableStream
+  export { r as ReadableStream }
+}
index 23831e51d3b4a1acc81b221cf95d7c3bed5bcc9b..9d082009ca066383db89a0afec7c6ecb2c01989a 100644 (file)
@@ -1,6 +1,21 @@
 # @vue/server-renderer
 
-``` js
+## Basic API
+
+### `renderToString`
+
+**Signature**
+
+```ts
+function renderToString(
+  input: App | VNode,
+  context?: SSRContext
+): Promise<string>
+```
+
+**Usage**
+
+```js
 const { createSSRApp } = require('vue')
 const { renderToString } = require('@vue/server-renderer')
 
@@ -14,3 +29,113 @@ const app = createSSRApp({
   console.log(html)
 })()
 ```
+
+### Handling Teleports
+
+If the rendered app contains teleports, the teleported content will not be part of the rendered string. Instead, they are exposed under the `teleports` property of the ssr context object:
+
+```js
+const ctx = {}
+const html = await renderToString(app, ctx)
+
+console.log(ctx.teleports) // { '#teleported': 'teleported content' }
+```
+
+## Streaming API
+
+### `renderToNodeStream`
+
+Renders input as a [Node.js Readable stream](https://nodejs.org/api/stream.html#stream_class_stream_readable).
+
+**Signature**
+
+```ts
+function renderToNodeStream(input: App | VNode, context?: SSRContext): Readable
+```
+
+**Usage**
+
+```js
+// inside a Node.js http handler
+renderToNodeStream(app).pipe(res)
+```
+
+In the ESM build of `@vue/server-renderer`, which is decoupled from Node.js environments, the `Readable` constructor must be explicitly passed in as the 3rd argument:
+
+```js
+import { Readable } from 'stream'
+
+renderToNodeStream(app, {}, Readable).pipe(res)
+```
+
+### `renderToWebStream`
+
+Renders input as a [Web ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API).
+
+**Signature**
+
+```ts
+function renderToWebStream(
+  input: App | VNode,
+  context?: SSRContext,
+  Ctor?: { new (): ReadableStream }
+): ReadableStream
+```
+
+**Usage**
+
+```js
+// e.g. inside a Cloudflare Worker
+return new Response(renderToWebStream(app))
+```
+
+Note in environments that do not expose `ReadableStream` constructor in the global scope, the constructor must be explicitly passed in as the 3rd argument. For example in Node.js 16.5.0+ where web streams are also supported:
+
+```js
+import { ReadableStream } from 'stream/web'
+
+const stream = renderToWebStream(app, {}, ReadableStream)
+```
+
+## `renderToSimpleStream`
+
+Renders input in streaming mode using a simple readable interface.
+
+**Signature**
+
+```ts
+function renderToSimpleStream(
+  input: App | VNode,
+  context: SSRContext,
+  options: SimpleReadable
+): SimpleReadable
+
+interface SimpleReadable {
+  push(content: string | null): void
+  destroy(err: any): void
+}
+```
+
+**Usage**
+
+```js
+let res = ''
+
+renderToSimpleStream(
+  app,
+  {},
+  {
+    push(chunk) {
+      if (chunk === null) {
+        // done
+        console(`render complete: ${res}`)
+      } else {
+        res += chunk
+      }
+    },
+    destroy(err) {
+      // error encountered
+    }
+  }
+)
+```
index 64ce7411d91edb5d03f2cd7e67654b0dd8aa270f..c06af8d973c8d2cd224083b4c78e176fdfe07196 100644 (file)
@@ -24,7 +24,7 @@ import {
 } from 'vue'
 import { escapeHtml } from '@vue/shared'
 import { renderToString } from '../src/renderToString'
-import { renderToStream as _renderToStream } from '../src/renderToStream'
+import { renderToNodeStream } from '../src/renderToStream'
 import { ssrRenderSlot, SSRSlot } from '../src/helpers/ssrRenderSlot'
 import { ssrRenderComponent } from '../src/helpers/ssrRenderComponent'
 import { Readable } from 'stream'
@@ -46,7 +46,7 @@ const promisifyStream = (stream: Readable) => {
 }
 
 const renderToStream = (app: any, context?: any) =>
-  promisifyStream(_renderToStream(app, context))
+  promisifyStream(renderToNodeStream(app, context))
 
 // we run the same tests twice, once for renderToString, once for renderToStream
 testRender(`renderToString`, renderToString)
diff --git a/packages/server-renderer/__tests__/webStream.spec.ts b/packages/server-renderer/__tests__/webStream.spec.ts
new file mode 100644 (file)
index 0000000..f26c949
--- /dev/null
@@ -0,0 +1,32 @@
+/**
+ * @jest-environment node
+ */
+
+import { createApp, h, defineAsyncComponent } from 'vue'
+import { ReadableStream } from 'stream/web'
+import { renderToWebStream } from '../src'
+
+test('should work', async () => {
+  const Async = defineAsyncComponent(() =>
+    Promise.resolve({
+      render: () => h('div', 'async')
+    })
+  )
+  const App = {
+    render: () => [h('div', 'parent'), h(Async)]
+  }
+
+  const stream = renderToWebStream(createApp(App), {}, ReadableStream)
+
+  const reader = stream.getReader()
+
+  let res = ''
+  await reader.read().then(function read({ done, value }): any {
+    if (!done) {
+      res += value
+      return reader.read().then(read)
+    }
+  })
+
+  expect(res).toBe(`<!--[--><div>parent</div><div>async</div><!--]-->`)
+})
index 19bf0489aa04ee8f50abb9a063a49b990f4689c4..39fd6c09ba6a43f2a8de5706054291d4b4adf9dc 100644 (file)
@@ -16,6 +16,14 @@ export function ssrCompile(
   template: string,
   instance: ComponentInternalInstance
 ): SSRRenderFunction {
+  if (!__NODE_JS__) {
+    throw new Error(
+      `On-the-fly template compilation is not supported in the ESM build of ` +
+        `@vue/server-renderer. All templates must be pre-compiled into ` +
+        `render functions.`
+    )
+  }
+
   const cached = compileCache[template]
   if (cached) {
     return cached
index 9c5066e851eca2bd60edbb6e481ee082e2a5f01f..c4b907127a13fef5289932b8e3e004deb60e5e68 100644 (file)
@@ -1,7 +1,13 @@
 // public
 export { SSRContext } from './render'
 export { renderToString } from './renderToString'
-export { renderToStream } from './renderToStream'
+export {
+  renderToStream,
+  renderToSimpleStream,
+  renderToNodeStream,
+  renderToWebStream,
+  SimpleReadable
+} from './renderToStream'
 
 // internal runtime helpers
 export { renderVNode as ssrRenderVNode } from './render'
index 4952b51c2676bb7e47a88d5a2466c215f2b0f371..516bff8ee8dcf4ab2c7069f5787ce7e3a3beae9b 100644 (file)
@@ -12,9 +12,14 @@ import { Readable } from 'stream'
 
 const { isVNode } = ssrUtils
 
+export interface SimpleReadable {
+  push(chunk: string | null): void
+  destroy(err: any): void
+}
+
 async function unrollBuffer(
   buffer: SSRBuffer,
-  stream: Readable
+  stream: SimpleReadable
 ): Promise<void> {
   if (buffer.hasAsync) {
     for (let i = 0; i < buffer.length; i++) {
@@ -35,7 +40,7 @@ async function unrollBuffer(
   }
 }
 
-function unrollBufferSync(buffer: SSRBuffer, stream: Readable) {
+function unrollBufferSync(buffer: SSRBuffer, stream: SimpleReadable) {
   for (let i = 0; i < buffer.length; i++) {
     let item = buffer[i]
     if (isString(item)) {
@@ -47,13 +52,18 @@ function unrollBufferSync(buffer: SSRBuffer, stream: Readable) {
   }
 }
 
-export function renderToStream(
+export function renderToSimpleStream<T extends SimpleReadable>(
   input: App | VNode,
-  context: SSRContext = {}
-): Readable {
+  context: SSRContext,
+  stream: T
+): T {
   if (isVNode(input)) {
     // raw vnode, wrap with app (for context)
-    return renderToStream(createApp({ render: () => input }), context)
+    return renderToSimpleStream(
+      createApp({ render: () => input }),
+      context,
+      stream
+    )
   }
 
   // rendering an app
@@ -62,8 +72,6 @@ export function renderToStream(
   // provide the ssr context to the tree
   input.provide(ssrContextKey, context)
 
-  const stream = new Readable()
-
   Promise.resolve(renderComponentVNode(vnode))
     .then(buffer => unrollBuffer(buffer, stream))
     .then(() => {
@@ -75,3 +83,78 @@ export function renderToStream(
 
   return stream
 }
+
+/**
+ * @deprecated
+ */
+export function renderToStream(
+  input: App | VNode,
+  context: SSRContext = {}
+): Readable {
+  console.warn(
+    `[@vue/server-renderer] renderToStream is deprecated - use renderToNodeStream instead.`
+  )
+  return renderToNodeStream(input, context)
+}
+
+export function renderToNodeStream(
+  input: App | VNode,
+  context: SSRContext = {},
+  UserReadable?: typeof Readable
+): Readable {
+  const stream: Readable = UserReadable
+    ? new UserReadable()
+    : __NODE_JS__
+    ? new (require('stream').Readable)()
+    : null
+
+  if (!stream) {
+    throw new Error(
+      `ESM build of renderToStream() requires explicitly passing in the Node.js ` +
+        `Readable constructor the 3rd argument. Example:\n\n` +
+        `  import { Readable } from 'stream'\n` +
+        `  const stream = renderToStream(app, {}, Readable)`
+    )
+  }
+
+  return renderToSimpleStream(input, context, stream)
+}
+
+const hasGlobalWebStream = typeof ReadableStream === 'function'
+
+export function renderToWebStream(
+  input: App | VNode,
+  context: SSRContext = {},
+  Ctor?: { new (): ReadableStream }
+): ReadableStream {
+  if (!Ctor && !hasGlobalWebStream) {
+    throw new Error(
+      `ReadableStream constructor is not avaialbe in the global scope and ` +
+        `must be explicitly passed in as the 3rd argument:\n\n` +
+        `  import { ReadableStream } from 'stream/web'\n` +
+        `  const stream = renderToWebStream(app, {}, ReadableStream)`
+    )
+  }
+
+  let cancelled = false
+  return new (Ctor || ReadableStream)({
+    start(controller) {
+      renderToSimpleStream(input, context, {
+        push(content) {
+          if (cancelled) return
+          if (content != null) {
+            controller.enqueue(content)
+          } else {
+            controller.close()
+          }
+        },
+        destroy(err) {
+          controller.error(err)
+        }
+      })
+    },
+    cancel() {
+      cancelled = true
+    }
+  })
+}