Skip to content

Commit 0867222

Browse files
committed
feat(server-renderer): decouple esm build from Node + improve stream API
- deprecate `renderToSTream` - added `renderToNodeStream` - added `renderToWebStream` - added `renderToSimpleStream` close #3467 close #3111 close #3460
1 parent 0affd4d commit 0867222

File tree

7 files changed

+271
-12
lines changed

7 files changed

+271
-12
lines changed

packages/global.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,8 @@ declare module '*?raw' {
3333
declare module 'file-saver' {
3434
export function saveAs(blob: any, name: any): void
3535
}
36+
37+
declare module 'stream/web' {
38+
const r: typeof ReadableStream
39+
export { r as ReadableStream }
40+
}

packages/server-renderer/README.md

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
# @vue/server-renderer
22

3-
``` js
3+
## Basic API
4+
5+
### `renderToString`
6+
7+
**Signature**
8+
9+
```ts
10+
function renderToString(
11+
input: App | VNode,
12+
context?: SSRContext
13+
): Promise<string>
14+
```
15+
16+
**Usage**
17+
18+
```js
419
const { createSSRApp } = require('vue')
520
const { renderToString } = require('@vue/server-renderer')
621
@@ -14,3 +29,113 @@ const app = createSSRApp({
1429
console.log(html)
1530
})()
1631
```
32+
33+
### Handling Teleports
34+
35+
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:
36+
37+
```js
38+
const ctx = {}
39+
const html = await renderToString(app, ctx)
40+
41+
console.log(ctx.teleports) // { '#teleported': 'teleported content' }
42+
```
43+
44+
## Streaming API
45+
46+
### `renderToNodeStream`
47+
48+
Renders input as a [Node.js Readable stream](https://nodejs.org/api/stream.html#stream_class_stream_readable).
49+
50+
**Signature**
51+
52+
```ts
53+
function renderToNodeStream(input: App | VNode, context?: SSRContext): Readable
54+
```
55+
56+
**Usage**
57+
58+
```js
59+
// inside a Node.js http handler
60+
renderToNodeStream(app).pipe(res)
61+
```
62+
63+
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:
64+
65+
```js
66+
import { Readable } from 'stream'
67+
68+
renderToNodeStream(app, {}, Readable).pipe(res)
69+
```
70+
71+
### `renderToWebStream`
72+
73+
Renders input as a [Web ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API).
74+
75+
**Signature**
76+
77+
```ts
78+
function renderToWebStream(
79+
input: App | VNode,
80+
context?: SSRContext,
81+
Ctor?: { new (): ReadableStream }
82+
): ReadableStream
83+
```
84+
85+
**Usage**
86+
87+
```js
88+
// e.g. inside a Cloudflare Worker
89+
return new Response(renderToWebStream(app))
90+
```
91+
92+
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:
93+
94+
```js
95+
import { ReadableStream } from 'stream/web'
96+
97+
const stream = renderToWebStream(app, {}, ReadableStream)
98+
```
99+
100+
## `renderToSimpleStream`
101+
102+
Renders input in streaming mode using a simple readable interface.
103+
104+
**Signature**
105+
106+
```ts
107+
function renderToSimpleStream(
108+
input: App | VNode,
109+
context: SSRContext,
110+
options: SimpleReadable
111+
): SimpleReadable
112+
113+
interface SimpleReadable {
114+
push(content: string | null): void
115+
destroy(err: any): void
116+
}
117+
```
118+
119+
**Usage**
120+
121+
```js
122+
let res = ''
123+
124+
renderToSimpleStream(
125+
app,
126+
{},
127+
{
128+
push(chunk) {
129+
if (chunk === null) {
130+
// done
131+
console(`render complete: ${res}`)
132+
} else {
133+
res += chunk
134+
}
135+
},
136+
destroy(err) {
137+
// error encountered
138+
}
139+
}
140+
)
141+
```

packages/server-renderer/__tests__/render.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
} from 'vue'
2525
import { escapeHtml } from '@vue/shared'
2626
import { renderToString } from '../src/renderToString'
27-
import { renderToStream as _renderToStream } from '../src/renderToStream'
27+
import { renderToNodeStream } from '../src/renderToStream'
2828
import { ssrRenderSlot, SSRSlot } from '../src/helpers/ssrRenderSlot'
2929
import { ssrRenderComponent } from '../src/helpers/ssrRenderComponent'
3030
import { Readable } from 'stream'
@@ -46,7 +46,7 @@ const promisifyStream = (stream: Readable) => {
4646
}
4747

4848
const renderToStream = (app: any, context?: any) =>
49-
promisifyStream(_renderToStream(app, context))
49+
promisifyStream(renderToNodeStream(app, context))
5050

5151
// we run the same tests twice, once for renderToString, once for renderToStream
5252
testRender(`renderToString`, renderToString)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
5+
import { createApp, h, defineAsyncComponent } from 'vue'
6+
import { ReadableStream } from 'stream/web'
7+
import { renderToWebStream } from '../src'
8+
9+
test('should work', async () => {
10+
const Async = defineAsyncComponent(() =>
11+
Promise.resolve({
12+
render: () => h('div', 'async')
13+
})
14+
)
15+
const App = {
16+
render: () => [h('div', 'parent'), h(Async)]
17+
}
18+
19+
const stream = renderToWebStream(createApp(App), {}, ReadableStream)
20+
21+
const reader = stream.getReader()
22+
23+
let res = ''
24+
await reader.read().then(function read({ done, value }): any {
25+
if (!done) {
26+
res += value
27+
return reader.read().then(read)
28+
}
29+
})
30+
31+
expect(res).toBe(`<!--[--><div>parent</div><div>async</div><!--]-->`)
32+
})

packages/server-renderer/src/helpers/ssrCompile.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ export function ssrCompile(
1616
template: string,
1717
instance: ComponentInternalInstance
1818
): SSRRenderFunction {
19+
if (!__NODE_JS__) {
20+
throw new Error(
21+
`On-the-fly template compilation is not supported in the ESM build of ` +
22+
`@vue/server-renderer. All templates must be pre-compiled into ` +
23+
`render functions.`
24+
)
25+
}
26+
1927
const cached = compileCache[template]
2028
if (cached) {
2129
return cached

packages/server-renderer/src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
// public
22
export { SSRContext } from './render'
33
export { renderToString } from './renderToString'
4-
export { renderToStream } from './renderToStream'
4+
export {
5+
renderToStream,
6+
renderToSimpleStream,
7+
renderToNodeStream,
8+
renderToWebStream,
9+
SimpleReadable
10+
} from './renderToStream'
511

612
// internal runtime helpers
713
export { renderVNode as ssrRenderVNode } from './render'

packages/server-renderer/src/renderToStream.ts

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@ import { Readable } from 'stream'
1212

1313
const { isVNode } = ssrUtils
1414

15+
export interface SimpleReadable {
16+
push(chunk: string | null): void
17+
destroy(err: any): void
18+
}
19+
1520
async function unrollBuffer(
1621
buffer: SSRBuffer,
17-
stream: Readable
22+
stream: SimpleReadable
1823
): Promise<void> {
1924
if (buffer.hasAsync) {
2025
for (let i = 0; i < buffer.length; i++) {
@@ -35,7 +40,7 @@ async function unrollBuffer(
3540
}
3641
}
3742

38-
function unrollBufferSync(buffer: SSRBuffer, stream: Readable) {
43+
function unrollBufferSync(buffer: SSRBuffer, stream: SimpleReadable) {
3944
for (let i = 0; i < buffer.length; i++) {
4045
let item = buffer[i]
4146
if (isString(item)) {
@@ -47,13 +52,18 @@ function unrollBufferSync(buffer: SSRBuffer, stream: Readable) {
4752
}
4853
}
4954

50-
export function renderToStream(
55+
export function renderToSimpleStream<T extends SimpleReadable>(
5156
input: App | VNode,
52-
context: SSRContext = {}
53-
): Readable {
57+
context: SSRContext,
58+
stream: T
59+
): T {
5460
if (isVNode(input)) {
5561
// raw vnode, wrap with app (for context)
56-
return renderToStream(createApp({ render: () => input }), context)
62+
return renderToSimpleStream(
63+
createApp({ render: () => input }),
64+
context,
65+
stream
66+
)
5767
}
5868

5969
// rendering an app
@@ -62,8 +72,6 @@ export function renderToStream(
6272
// provide the ssr context to the tree
6373
input.provide(ssrContextKey, context)
6474

65-
const stream = new Readable()
66-
6775
Promise.resolve(renderComponentVNode(vnode))
6876
.then(buffer => unrollBuffer(buffer, stream))
6977
.then(() => {
@@ -75,3 +83,78 @@ export function renderToStream(
7583

7684
return stream
7785
}
86+
87+
/**
88+
* @deprecated
89+
*/
90+
export function renderToStream(
91+
input: App | VNode,
92+
context: SSRContext = {}
93+
): Readable {
94+
console.warn(
95+
`[@vue/server-renderer] renderToStream is deprecated - use renderToNodeStream instead.`
96+
)
97+
return renderToNodeStream(input, context)
98+
}
99+
100+
export function renderToNodeStream(
101+
input: App | VNode,
102+
context: SSRContext = {},
103+
UserReadable?: typeof Readable
104+
): Readable {
105+
const stream: Readable = UserReadable
106+
? new UserReadable()
107+
: __NODE_JS__
108+
? new (require('stream').Readable)()
109+
: null
110+
111+
if (!stream) {
112+
throw new Error(
113+
`ESM build of renderToStream() requires explicitly passing in the Node.js ` +
114+
`Readable constructor the 3rd argument. Example:\n\n` +
115+
` import { Readable } from 'stream'\n` +
116+
` const stream = renderToStream(app, {}, Readable)`
117+
)
118+
}
119+
120+
return renderToSimpleStream(input, context, stream)
121+
}
122+
123+
const hasGlobalWebStream = typeof ReadableStream === 'function'
124+
125+
export function renderToWebStream(
126+
input: App | VNode,
127+
context: SSRContext = {},
128+
Ctor?: { new (): ReadableStream }
129+
): ReadableStream {
130+
if (!Ctor && !hasGlobalWebStream) {
131+
throw new Error(
132+
`ReadableStream constructor is not avaialbe in the global scope and ` +
133+
`must be explicitly passed in as the 3rd argument:\n\n` +
134+
` import { ReadableStream } from 'stream/web'\n` +
135+
` const stream = renderToWebStream(app, {}, ReadableStream)`
136+
)
137+
}
138+
139+
let cancelled = false
140+
return new (Ctor || ReadableStream)({
141+
start(controller) {
142+
renderToSimpleStream(input, context, {
143+
push(content) {
144+
if (cancelled) return
145+
if (content != null) {
146+
controller.enqueue(content)
147+
} else {
148+
controller.close()
149+
}
150+
},
151+
destroy(err) {
152+
controller.error(err)
153+
}
154+
})
155+
},
156+
cancel() {
157+
cancelled = true
158+
}
159+
})
160+
}

0 commit comments

Comments
 (0)