Skip to content

Commit 1c79592

Browse files
committed
feat: implement template option for vue-server-renderer
1 parent e71d70d commit 1c79592

11 files changed

+241
-46
lines changed

flow/modules.js

+10-4
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,18 @@ declare module 'de-indent' {
3131
}
3232

3333
declare module 'vue-ssr-html-stream' {
34+
declare interface parsedTemplate {
35+
head: string;
36+
neck: string;
37+
tail: string;
38+
}
3439
declare interface HTMLStreamOptions {
35-
template: string;
36-
context: Object;
40+
template: string | parsedTemplate;
41+
context?: ?Object;
3742
}
38-
declare class HTMLStream extends stream$Transform {
43+
declare class exports extends stream$Transform {
3944
constructor(options: HTMLStreamOptions): void;
45+
static parseTemplate(template: string): parsedTemplate;
46+
static renderTemplate(template: parsedTemplate, content: string, context?: ?Object): string;
4047
}
41-
declare module.exports: HTMLStream
4248
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
"selenium-server": "^2.53.1",
112112
"typescript": "^2.0.9",
113113
"uglify-js": "^2.6.2",
114+
"vue-ssr-html-stream": "^2.1.0",
114115
"vue-ssr-webpack-plugin": "^1.0.0",
115116
"webpack": "^2.2.0",
116117
"weex-js-runtime": "^0.17.0-alpha4",

packages/vue-server-renderer/README.md

+60-24
Original file line numberDiff line numberDiff line change
@@ -110,34 +110,16 @@ bundleRenderer
110110

111111
## Renderer Options
112112

113-
### directives
114-
115-
Allows you to provide server-side implementations for your custom directives:
116-
117-
``` js
118-
const renderer = createRenderer({
119-
directives: {
120-
example (vnode, directiveMeta) {
121-
// transform vnode based on directive binding metadata
122-
}
123-
}
124-
})
125-
```
126-
127-
As an example, check out [`v-show`'s server-side implementation](https://github.com/vuejs/vue/blob/dev/src/platforms/web/server/directives/show.js).
128-
129-
---
130-
131113
### cache
132114

133-
Provide a [component cache](#component-caching) implementation. The cache object must implement the following interface:
115+
Provide a [component cache](#component-caching) implementation. The cache object must implement the following interface (using Flow notations):
134116

135117
``` js
136-
{
137-
get: (key: string, [cb: Function]) => string | void,
138-
set: (key: string, val: string) => void,
139-
has?: (key: string, [cb: Function]) => boolean | void // optional
140-
}
118+
type RenderCache = {
119+
get: (key: string, cb?: Function) => string | void;
120+
set: (key: string, val: string) => void;
121+
has?: (key: string, cb?: Function) => boolean | void;
122+
};
141123
```
142124

143125
A typical usage is passing in an [lru-cache](https://github.com/isaacs/node-lru-cache):
@@ -170,6 +152,60 @@ const renderer = createRenderer({
170152
})
171153
```
172154

155+
---
156+
157+
### template
158+
159+
> New in 2.2.0
160+
161+
Provide a template for the entire page's HTML. The template should contain a comment `<!--vue-ssr-outlet-->` which serves as the placeholder for rendered app content.
162+
163+
In addition, when both a template and a render context is provided (e.g. when using the `bundleRenderer`), the renderer will also automatically inject the following properties found on the render context:
164+
165+
- `context.head`: (string) any head markup that should be injected into the head of the page. Note when using the bundle format generated with `vue-ssr-webpack-plugin`, this property will automatically contain `<link rel="preload/prefetch">` directives for chunks in the bundle.
166+
167+
- `context.styles`: (string) any inline CSS that should be injected into the head of the page. Note that `vue-loader` 10.2.0+ (which uses `vue-style-loader` 2.0) will automatically populate this property with styles used in rendered components.
168+
169+
- `context.state`: (Object) initial Vuex store state that should be inlined in the page as `window.__INITIAL_STATE__`. The inlined JSON is automatically sanitized with [serialize-javascript](https://github.com/yahoo/serialize-javascript).
170+
171+
**Example:**
172+
173+
``` js
174+
const renderer = createRenderer({
175+
template:
176+
'<!DOCTYPE html>' +
177+
'<html lang="en">' +
178+
'<head>' +
179+
'<meta charset="utf-8">' +
180+
// context.head will be injected here
181+
// context.styles will be injected here
182+
'</head>' +
183+
'<body>' +
184+
'<!--vue-ssr-outlet-->' + // <- app content rendered here
185+
// context.state will be injected here
186+
'</body>' +
187+
'</html>'
188+
})
189+
```
190+
191+
---
192+
193+
### directives
194+
195+
Allows you to provide server-side implementations for your custom directives:
196+
197+
``` js
198+
const renderer = createRenderer({
199+
directives: {
200+
example (vnode, directiveMeta) {
201+
// transform vnode based on directive binding metadata
202+
}
203+
}
204+
})
205+
```
206+
207+
As an example, check out [`v-show`'s server-side implementation](https://github.com/vuejs/vue/blob/dev/src/platforms/web/server/directives/show.js).
208+
173209
## Why Use `bundleRenderer`?
174210

175211
In a typical Node.js app, the server is a long-running process. If we directly require our application code, the instantiated modules will be shared across every request. This imposes some inconvenient restrictions to the application structure: we will have to avoid any use of global stateful singletons (e.g. the store), otherwise state mutations caused by one request will affect the result of the next.

packages/vue-server-renderer/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
},
2020
"dependencies": {
2121
"he": "^1.1.0",
22-
"de-indent": "^1.0.2"
22+
"de-indent": "^1.0.2",
23+
"source-map": "0.5.6",
24+
"vue-ssr-html-stream": "^2.1.0"
2325
},
2426
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/vue-server-renderer#readme"
2527
}

src/entries/web-server-renderer.js

+7-5
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@ export function createRenderer (options?: Object = {}): {
1212
renderToString: Function,
1313
renderToStream: Function
1414
} {
15-
// user can provide server-side implementations for custom directives
16-
// when creating the renderer.
17-
const directives = Object.assign(baseDirectives, options.directives)
1815
return _createRenderer({
1916
isUnaryTag,
2017
modules,
21-
directives,
22-
cache: options.cache
18+
// user can provide server-side implementations for custom directives
19+
// when creating the renderer.
20+
directives: Object.assign(baseDirectives, options.directives),
21+
// component cache (optional)
22+
cache: options.cache,
23+
// page template (optional)
24+
template: options.template
2325
})
2426
}
2527

src/server/create-bundle-renderer.js

+14-2
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export function createBundleRendererCreator (createRenderer: () => Renderer) {
6060
renderer.renderToString(app, (err, res) => {
6161
rewriteErrorTrace(err, maps)
6262
cb(err, res)
63-
})
63+
}, context)
6464
}
6565
})
6666
},
@@ -76,11 +76,23 @@ export function createBundleRendererCreator (createRenderer: () => Renderer) {
7676
})
7777
}).then(app => {
7878
if (app) {
79-
const renderStream = renderer.renderToStream(app)
79+
const renderStream = renderer.renderToStream(app, context)
80+
8081
renderStream.on('error', err => {
8182
rewriteErrorTrace(err, maps)
8283
res.emit('error', err)
8384
})
85+
86+
// relay HTMLStream special events
87+
if (rendererOptions && rendererOptions.template) {
88+
renderStream.on('beforeStart', () => {
89+
res.emit('beforeStart')
90+
})
91+
renderStream.on('beforeEnd', () => {
92+
res.emit('beforeEnd')
93+
})
94+
}
95+
8496
renderStream.pipe(res)
8597
}
8698
})

src/server/create-renderer.js

+28-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
/* @flow */
22

3+
const HTMLStream = require('vue-ssr-html-stream')
4+
35
import RenderStream from './render-stream'
46
import { createWriteFunction } from './write'
57
import { createRenderFunction } from './render'
68

79
export type Renderer = {
810
renderToString: (component: Component, cb: (err: ?Error, res: ?string) => void) => void;
9-
renderToStream: (component: Component) => RenderStream;
11+
renderToStream: (component: Component) => stream$Readable;
1012
};
1113

1214
type RenderCache = {
@@ -27,32 +29,54 @@ export function createRenderer ({
2729
modules = [],
2830
directives = {},
2931
isUnaryTag = (() => false),
32+
template,
3033
cache
3134
}: RenderOptions = {}): Renderer {
3235
const render = createRenderFunction(modules, directives, isUnaryTag, cache)
36+
const parsedTemplate = template && HTMLStream.parseTemplate(template)
3337

3438
return {
3539
renderToString (
3640
component: Component,
37-
done: (err: ?Error, res: ?string) => any
41+
done: (err: ?Error, res: ?string) => any,
42+
context?: ?Object
3843
): void {
3944
let result = ''
4045
const write = createWriteFunction(text => {
4146
result += text
4247
}, done)
4348
try {
4449
render(component, write, () => {
50+
if (parsedTemplate) {
51+
result = HTMLStream.renderTemplate(parsedTemplate, result, context)
52+
}
4553
done(null, result)
4654
})
4755
} catch (e) {
4856
done(e)
4957
}
5058
},
5159

52-
renderToStream (component: Component): RenderStream {
53-
return new RenderStream((write, done) => {
60+
renderToStream (
61+
component: Component,
62+
context?: ?Object
63+
): stream$Readable {
64+
const renderStream = new RenderStream((write, done) => {
5465
render(component, write, done)
5566
})
67+
if (!parsedTemplate) {
68+
return renderStream
69+
} else {
70+
const htmlStream = new HTMLStream({
71+
template: parsedTemplate,
72+
context
73+
})
74+
renderStream.on('error', err => {
75+
htmlStream.emit('error', err)
76+
})
77+
renderStream.pipe(htmlStream)
78+
return htmlStream
79+
}
5680
}
5781
}
5882
}

test/ssr/ssr-bundle-render.spec.js

+53-6
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,7 @@ import MemoeryFS from 'memory-fs'
44
import VueSSRPlugin from 'vue-ssr-webpack-plugin'
55
import { createBundleRenderer } from '../../packages/vue-server-renderer'
66

7-
const rendererCache = {}
87
function createRenderer (file, cb, options) {
9-
if (!options && rendererCache[file]) {
10-
return cb(rendererCache[file])
11-
}
12-
138
const asBundle = !!(options && options.asBundle)
149
if (options) delete options.asBundle
1510

@@ -41,7 +36,7 @@ function createRenderer (file, cb, options) {
4136
const bundle = asBundle
4237
? JSON.parse(fs.readFileSync('/vue-ssr-bundle.json', 'utf-8'))
4338
: fs.readFileSync('/bundle.js', 'utf-8')
44-
const renderer = rendererCache[file] = createBundleRenderer(bundle, options)
39+
const renderer = createBundleRenderer(bundle, options)
4540
cb(renderer)
4641
})
4742
}
@@ -224,4 +219,56 @@ describe('SSR: bundle renderer', () => {
224219
})
225220
}, { asBundle: true })
226221
})
222+
223+
it('renderToString with template', done => {
224+
createRenderer('app.js', renderer => {
225+
const context = {
226+
head: '<meta name="viewport" content="width=device-width">',
227+
styles: '<style>h1 { color: red }</style>',
228+
state: { a: 1 },
229+
url: '/test'
230+
}
231+
renderer.renderToString(context, (err, res) => {
232+
expect(err).toBeNull()
233+
expect(res).toContain(
234+
`<html><head>${context.head}${context.styles}</head><body>` +
235+
`<div server-rendered="true">/test</div>` +
236+
`<script>window.__INITIAL_STATE__={"a":1}</script>` +
237+
`</body></html>`
238+
)
239+
expect(context.msg).toBe('hello')
240+
done()
241+
})
242+
}, {
243+
template: `<html><head></head><body><!--vue-ssr-outlet--></body></html>`
244+
})
245+
})
246+
247+
it('renderToStream with template', done => {
248+
createRenderer('app.js', renderer => {
249+
const context = {
250+
head: '<meta name="viewport" content="width=device-width">',
251+
styles: '<style>h1 { color: red }</style>',
252+
state: { a: 1 },
253+
url: '/test'
254+
}
255+
const stream = renderer.renderToStream(context)
256+
let res = ''
257+
stream.on('data', chunk => {
258+
res += chunk.toString()
259+
})
260+
stream.on('end', () => {
261+
expect(res).toContain(
262+
`<html><head>${context.head}${context.styles}</head><body>` +
263+
`<div server-rendered="true">/test</div>` +
264+
`<script>window.__INITIAL_STATE__={"a":1}</script>` +
265+
`</body></html>`
266+
)
267+
expect(context.msg).toBe('hello')
268+
done()
269+
})
270+
}, {
271+
template: `<html><head></head><body><!--vue-ssr-outlet--></body></html>`
272+
})
273+
})
227274
})

test/ssr/ssr-stream.spec.js

+30
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,34 @@ describe('SSR: renderToStream', () => {
102102
stream1.read(1)
103103
stream2.read(1)
104104
})
105+
106+
it('should accept template option', done => {
107+
const renderer = createRenderer({
108+
template: `<html><head></head><body><!--vue-ssr-outlet--></body></html>`
109+
})
110+
111+
const context = {
112+
head: '<meta name="viewport" content="width=device-width">',
113+
styles: '<style>h1 { color: red }</style>',
114+
state: { a: 1 }
115+
}
116+
117+
const stream = renderer.renderToStream(new Vue({
118+
template: '<div>hi</div>'
119+
}), context)
120+
121+
let res = ''
122+
stream.on('data', chunk => {
123+
res += chunk
124+
})
125+
stream.on('end', () => {
126+
expect(res).toContain(
127+
`<html><head>${context.head}${context.styles}</head><body>` +
128+
`<div server-rendered="true">hi</div>` +
129+
`<script>window.__INITIAL_STATE__={"a":1}</script>` +
130+
`</body></html>`
131+
)
132+
done()
133+
})
134+
})
105135
})

0 commit comments

Comments
 (0)