]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(server-renderer): adjust streaming API
authorEvan You <yyx990803@gmail.com>
Fri, 6 Aug 2021 16:41:40 +0000 (12:41 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 6 Aug 2021 16:41:40 +0000 (12:41 -0400)
- add `pipeToNodeWritable`
- add `pipeToWebWritable`

packages/global.d.ts
packages/server-renderer/README.md
packages/server-renderer/__tests__/render.spec.ts
packages/server-renderer/__tests__/webStream.spec.ts
packages/server-renderer/src/index.ts
packages/server-renderer/src/renderToStream.ts

index 007f8ffdd1929df14f6895c92629b8909bfab3eb..2796f658d0a65fe8c4ab4528465989da6d0f50f9 100644 (file)
@@ -36,5 +36,6 @@ declare module 'file-saver' {
 
 declare module 'stream/web' {
   const r: typeof ReadableStream
-  export { r as ReadableStream }
+  const t: typeof TransformStream
+  export { r as ReadableStream, t as TransformStream }
 }
index 826e2514f4fabf8ccaed19690da4637a71659f8d..d56b75be2694050d07974e7163e75d54ecf9dceb 100644 (file)
@@ -50,10 +50,7 @@ Renders input as a [Node.js Readable stream](https://nodejs.org/api/stream.html#
 **Signature**
 
 ```ts
-function renderToNodeStream(
-  input: App | VNode,
-  context?: SSRContext
-): Readable
+function renderToNodeStream(input: App | VNode, context?: SSRContext): Readable
 ```
 
 **Usage**
@@ -63,12 +60,27 @@ function renderToNodeStream(
 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:
+**Note:** This method is not supported in the ESM build of `@vue/server-renderer`, which is decoupled from Node.js environments. Use `pipeToNodeWritable` instead.
 
-```js
-import { Readable } from 'stream'
+### `pipeToNodeWritable`
+
+Render and pipe to an existing [Node.js Writable stream](https://nodejs.org/api/stream.html#stream_writable_streams) instance.
 
-renderToNodeStream(app, {}, Readable).pipe(res)
+**Signature**
+
+```ts
+function pipeToNodeWritable(
+  input: App | VNode,
+  context: SSRContext = {},
+  writable: Writable
+): void
+```
+
+**Usage**
+
+```js
+// inside a Node.js http handler
+pipeToNodeWritable(app, {}, res)
 ```
 
 ### `renderToWebStream`
@@ -88,19 +100,40 @@ function renderToWebStream(
 **Usage**
 
 ```js
-// e.g. inside an environment with ReadableStream support
+// inside an environment with ReadableStream support
 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:
+**Note:** in environments that do not expose `ReadableStream` constructor in the global scope, `pipeToWebWritable` should be used instead.
+
+### `pipeToWebWritable`
+
+Render and pipe to an existing [Web WritableStream](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream) instance.
+
+**Signature**
+
+```ts
+function pipeToWebWritable(
+  input: App | VNode,
+  context: SSRContext = {},
+  writable: WritableStream
+): void
+```
+
+**Usage**
+
+This is typically used in combination with [`TransformStream`](https://developer.mozilla.org/en-US/docs/Web/API/TransformStream):
 
 ```js
-import { ReadableStream } from 'stream/web'
+// TransformStream is available in environments such as CloudFlare workers.
+// in Node.js, TransformStream needs to be explicitly imported from 'stream/web'
+const { readable, writable } = new TransformStream()
+pipeToWebWritable(app, {}, writable)
 
-const stream = renderToWebStream(app, {}, ReadableStream)
+return new Response(readable)
 ```
 
-## `renderToSimpleStream`
+### `renderToSimpleStream`
 
 Renders input in streaming mode using a simple readable interface.
 
index c06af8d973c8d2cd224083b4c78e176fdfe07196..1079a29a6c2358703d3d693cdbb20a325a611993 100644 (file)
@@ -24,10 +24,10 @@ import {
 } from 'vue'
 import { escapeHtml } from '@vue/shared'
 import { renderToString } from '../src/renderToString'
-import { renderToNodeStream } from '../src/renderToStream'
+import { renderToNodeStream, pipeToNodeWritable } from '../src/renderToStream'
 import { ssrRenderSlot, SSRSlot } from '../src/helpers/ssrRenderSlot'
 import { ssrRenderComponent } from '../src/helpers/ssrRenderComponent'
-import { Readable } from 'stream'
+import { Readable, Transform } from 'stream'
 import { ssrRenderVNode } from '../src'
 
 const promisifyStream = (stream: Readable) => {
@@ -45,12 +45,25 @@ const promisifyStream = (stream: Readable) => {
   })
 }
 
-const renderToStream = (app: any, context?: any) =>
-  promisifyStream(renderToNodeStream(app, context))
+const renderToStream = (app: any, context?: any) => {
+  return promisifyStream(renderToNodeStream(app, context))
+}
+
+const pipeToWritable = (app: any, context?: any) => {
+  const stream = new Transform({
+    transform(data, _encoding, cb) {
+      this.push(data)
+      cb()
+    }
+  })
+  pipeToNodeWritable(app, context, stream)
+  return promisifyStream(stream)
+}
 
 // we run the same tests twice, once for renderToString, once for renderToStream
 testRender(`renderToString`, renderToString)
-testRender(`renderToStream`, renderToStream)
+testRender(`renderToNodeStream`, renderToStream)
+testRender(`pipeToNodeWritable`, pipeToWritable)
 
 function testRender(type: string, render: typeof renderToString) {
   describe(`ssr: ${type}`, () => {
@@ -760,7 +773,7 @@ function testRender(type: string, render: typeof renderToString) {
       test('handle compiler errors', async () => {
         await render(
           // render different content since compilation is cached
-          createApp({ template: `<${type === 'renderToString' ? 'div' : 'p'}` })
+          createApp({ template: `<div>${type}</` })
         )
 
         expect(
index f26c9491edc754c6833553ba1a7010776119346d..2bdc775354f9717c503985e97016155584ddb0e6 100644 (file)
@@ -3,10 +3,19 @@
  */
 
 import { createApp, h, defineAsyncComponent } from 'vue'
-import { ReadableStream } from 'stream/web'
-import { renderToWebStream } from '../src'
+import { ReadableStream, TransformStream } from 'stream/web'
+import { pipeToWebWritable, renderToWebStream } from '../src'
 
-test('should work', async () => {
+beforeEach(() => {
+  global.ReadableStream = ReadableStream
+})
+
+afterEach(() => {
+  // @ts-ignore
+  delete global.ReadableStream
+})
+
+test('renderToWebStream', async () => {
   const Async = defineAsyncComponent(() =>
     Promise.resolve({
       render: () => h('div', 'async')
@@ -16,14 +25,42 @@ test('should work', async () => {
     render: () => [h('div', 'parent'), h(Async)]
   }
 
-  const stream = renderToWebStream(createApp(App), {}, ReadableStream)
+  const stream = renderToWebStream(createApp(App))
 
   const reader = stream.getReader()
+  const decoder = new TextDecoder()
+
+  let res = ''
+  await reader.read().then(function read({ done, value }): any {
+    if (!done) {
+      res += decoder.decode(value)
+      return reader.read().then(read)
+    }
+  })
+
+  expect(res).toBe(`<!--[--><div>parent</div><div>async</div><!--]-->`)
+})
+
+test('pipeToWebWritable', async () => {
+  const Async = defineAsyncComponent(() =>
+    Promise.resolve({
+      render: () => h('div', 'async')
+    })
+  )
+  const App = {
+    render: () => [h('div', 'parent'), h(Async)]
+  }
+
+  const { readable, writable } = new TransformStream()
+  pipeToWebWritable(createApp(App), {}, writable)
+
+  const reader = readable.getReader()
+  const decoder = new TextDecoder()
 
   let res = ''
   await reader.read().then(function read({ done, value }): any {
     if (!done) {
-      res += value
+      res += decoder.decode(value)
       return reader.read().then(read)
     }
   })
index c4b907127a13fef5289932b8e3e004deb60e5e68..e5a9f650e478bf271fe1f31859284cd41e7f9711 100644 (file)
@@ -2,11 +2,14 @@
 export { SSRContext } from './render'
 export { renderToString } from './renderToString'
 export {
-  renderToStream,
   renderToSimpleStream,
   renderToNodeStream,
+  pipeToNodeWritable,
   renderToWebStream,
-  SimpleReadable
+  pipeToWebWritable,
+  SimpleReadable,
+  // deprecated
+  renderToStream
 } from './renderToStream'
 
 // internal runtime helpers
index ca69ce99a9e9217def34ae692e4f7bc98ce8f369..437e0b6cdac267fc0a1ddbaf810b01c667286f49 100644 (file)
@@ -8,7 +8,7 @@ import {
 } from 'vue'
 import { isString, isPromise } from '@vue/shared'
 import { renderComponentVNode, SSRBuffer, SSRContext } from './render'
-import { Readable } from 'stream'
+import { Readable, Writable } from 'stream'
 
 const { isVNode } = ssrUtils
 
@@ -99,51 +99,64 @@ export function renderToStream(
 
 export function renderToNodeStream(
   input: App | VNode,
-  context: SSRContext = {},
-  UserReadable?: typeof Readable
+  context: SSRContext = {}
 ): Readable {
-  const stream: Readable = UserReadable
-    ? new UserReadable()
-    : __NODE_JS__
+  const stream: Readable = __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)`
+      `ESM build of renderToStream() does not support renderToNodeStream(). ` +
+        `Use pipeToNodeWritable() with an existing Node.js Writable stream ` +
+        `instance instead.`
     )
   }
 
   return renderToSimpleStream(input, context, stream)
 }
 
-const hasGlobalWebStream = typeof ReadableStream === 'function'
+export function pipeToNodeWritable(
+  input: App | VNode,
+  context: SSRContext = {},
+  writable: Writable
+) {
+  renderToSimpleStream(input, context, {
+    push(content) {
+      if (content != null) {
+        writable.write(content)
+      } else {
+        writable.end()
+      }
+    },
+    destroy(err) {
+      writable.destroy(err)
+    }
+  })
+}
 
 export function renderToWebStream(
   input: App | VNode,
-  context: SSRContext = {},
-  Ctor?: { new (): ReadableStream }
+  context: SSRContext = {}
 ): ReadableStream {
-  if (!Ctor && !hasGlobalWebStream) {
+  if (typeof ReadableStream !== 'function') {
     throw new Error(
-      `ReadableStream constructor is not available 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)`
+      `ReadableStream constructor is not available in the global scope. ` +
+        `If the target environment does support web streams, consider using ` +
+        `pipeToWebWritable() with an existing WritableStream instance instead.`
     )
   }
 
+  const encoder = new TextEncoder()
   let cancelled = false
-  return new (Ctor || ReadableStream)({
+
+  return new ReadableStream({
     start(controller) {
       renderToSimpleStream(input, context, {
         push(content) {
           if (cancelled) return
           if (content != null) {
-            controller.enqueue(content)
+            controller.enqueue(encoder.encode(content))
           } else {
             controller.close()
           }
@@ -158,3 +171,29 @@ export function renderToWebStream(
     }
   })
 }
+
+export function pipeToWebWritable(
+  input: App | VNode,
+  context: SSRContext = {},
+  writable: WritableStream
+): void {
+  const writer = writable.getWriter()
+  const encoder = new TextEncoder()
+
+  writer.ready.then(() => {
+    renderToSimpleStream(input, context, {
+      push(content) {
+        if (content != null) {
+          writer.write(encoder.encode(content))
+        } else {
+          writer.close()
+        }
+      },
+      destroy(err) {
+        // TODO better error handling?
+        console.log(err)
+        writer.close()
+      }
+    })
+  })
+}