Skip to content

Commit

Permalink
feat: implement template option for vue-server-renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Feb 13, 2017
1 parent e71d70d commit 1c79592
Show file tree
Hide file tree
Showing 11 changed files with 241 additions and 46 deletions.
14 changes: 10 additions & 4 deletions flow/modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,18 @@ declare module 'de-indent' {
}

declare module 'vue-ssr-html-stream' {
declare interface parsedTemplate {
head: string;
neck: string;
tail: string;
}
declare interface HTMLStreamOptions {
template: string;
context: Object;
template: string | parsedTemplate;
context?: ?Object;
}
declare class HTMLStream extends stream$Transform {
declare class exports extends stream$Transform {
constructor(options: HTMLStreamOptions): void;
static parseTemplate(template: string): parsedTemplate;
static renderTemplate(template: parsedTemplate, content: string, context?: ?Object): string;
}
declare module.exports: HTMLStream
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
"selenium-server": "^2.53.1",
"typescript": "^2.0.9",
"uglify-js": "^2.6.2",
"vue-ssr-html-stream": "^2.1.0",
"vue-ssr-webpack-plugin": "^1.0.0",
"webpack": "^2.2.0",
"weex-js-runtime": "^0.17.0-alpha4",
Expand Down
84 changes: 60 additions & 24 deletions packages/vue-server-renderer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,34 +110,16 @@ bundleRenderer

## Renderer Options

### directives

Allows you to provide server-side implementations for your custom directives:

``` js
const renderer = createRenderer({
directives: {
example (vnode, directiveMeta) {
// transform vnode based on directive binding metadata
}
}
})
```

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).

---

### cache

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

``` js
{
get: (key: string, [cb: Function]) => string | void,
set: (key: string, val: string) => void,
has?: (key: string, [cb: Function]) => boolean | void // optional
}
type RenderCache = {
get: (key: string, cb?: Function) => string | void;
set: (key: string, val: string) => void;
has?: (key: string, cb?: Function) => boolean | void;
};
```

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

---

### template

> New in 2.2.0
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.

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:

- `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.

- `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.

- `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).

**Example:**

``` js
const renderer = createRenderer({
template:
'<!DOCTYPE html>' +
'<html lang="en">' +
'<head>' +
'<meta charset="utf-8">' +
// context.head will be injected here
// context.styles will be injected here
'</head>' +
'<body>' +
'<!--vue-ssr-outlet-->' + // <- app content rendered here
// context.state will be injected here
'</body>' +
'</html>'
})
```

---

### directives

Allows you to provide server-side implementations for your custom directives:

``` js
const renderer = createRenderer({
directives: {
example (vnode, directiveMeta) {
// transform vnode based on directive binding metadata
}
}
})
```

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).

## Why Use `bundleRenderer`?

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.
Expand Down
4 changes: 3 additions & 1 deletion packages/vue-server-renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
},
"dependencies": {
"he": "^1.1.0",
"de-indent": "^1.0.2"
"de-indent": "^1.0.2",
"source-map": "0.5.6",
"vue-ssr-html-stream": "^2.1.0"
},
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/vue-server-renderer#readme"
}
12 changes: 7 additions & 5 deletions src/entries/web-server-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ export function createRenderer (options?: Object = {}): {
renderToString: Function,
renderToStream: Function
} {
// user can provide server-side implementations for custom directives
// when creating the renderer.
const directives = Object.assign(baseDirectives, options.directives)
return _createRenderer({
isUnaryTag,
modules,
directives,
cache: options.cache
// user can provide server-side implementations for custom directives
// when creating the renderer.
directives: Object.assign(baseDirectives, options.directives),
// component cache (optional)
cache: options.cache,
// page template (optional)
template: options.template
})
}

Expand Down
16 changes: 14 additions & 2 deletions src/server/create-bundle-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function createBundleRendererCreator (createRenderer: () => Renderer) {
renderer.renderToString(app, (err, res) => {
rewriteErrorTrace(err, maps)
cb(err, res)
})
}, context)
}
})
},
Expand All @@ -76,11 +76,23 @@ export function createBundleRendererCreator (createRenderer: () => Renderer) {
})
}).then(app => {
if (app) {
const renderStream = renderer.renderToStream(app)
const renderStream = renderer.renderToStream(app, context)

renderStream.on('error', err => {
rewriteErrorTrace(err, maps)
res.emit('error', err)
})

// relay HTMLStream special events
if (rendererOptions && rendererOptions.template) {
renderStream.on('beforeStart', () => {
res.emit('beforeStart')
})
renderStream.on('beforeEnd', () => {
res.emit('beforeEnd')
})
}

renderStream.pipe(res)
}
})
Expand Down
32 changes: 28 additions & 4 deletions src/server/create-renderer.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
/* @flow */

const HTMLStream = require('vue-ssr-html-stream')

import RenderStream from './render-stream'
import { createWriteFunction } from './write'
import { createRenderFunction } from './render'

export type Renderer = {
renderToString: (component: Component, cb: (err: ?Error, res: ?string) => void) => void;
renderToStream: (component: Component) => RenderStream;
renderToStream: (component: Component) => stream$Readable;
};

type RenderCache = {
Expand All @@ -27,32 +29,54 @@ export function createRenderer ({
modules = [],
directives = {},
isUnaryTag = (() => false),
template,
cache
}: RenderOptions = {}): Renderer {
const render = createRenderFunction(modules, directives, isUnaryTag, cache)
const parsedTemplate = template && HTMLStream.parseTemplate(template)

return {
renderToString (
component: Component,
done: (err: ?Error, res: ?string) => any
done: (err: ?Error, res: ?string) => any,
context?: ?Object
): void {
let result = ''
const write = createWriteFunction(text => {
result += text
}, done)
try {
render(component, write, () => {
if (parsedTemplate) {
result = HTMLStream.renderTemplate(parsedTemplate, result, context)
}
done(null, result)
})
} catch (e) {
done(e)
}
},

renderToStream (component: Component): RenderStream {
return new RenderStream((write, done) => {
renderToStream (
component: Component,
context?: ?Object
): stream$Readable {
const renderStream = new RenderStream((write, done) => {
render(component, write, done)
})
if (!parsedTemplate) {
return renderStream
} else {
const htmlStream = new HTMLStream({
template: parsedTemplate,
context
})
renderStream.on('error', err => {
htmlStream.emit('error', err)
})
renderStream.pipe(htmlStream)
return htmlStream
}
}
}
}
59 changes: 53 additions & 6 deletions test/ssr/ssr-bundle-render.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@ import MemoeryFS from 'memory-fs'
import VueSSRPlugin from 'vue-ssr-webpack-plugin'
import { createBundleRenderer } from '../../packages/vue-server-renderer'

const rendererCache = {}
function createRenderer (file, cb, options) {
if (!options && rendererCache[file]) {
return cb(rendererCache[file])
}

const asBundle = !!(options && options.asBundle)
if (options) delete options.asBundle

Expand Down Expand Up @@ -41,7 +36,7 @@ function createRenderer (file, cb, options) {
const bundle = asBundle
? JSON.parse(fs.readFileSync('/vue-ssr-bundle.json', 'utf-8'))
: fs.readFileSync('/bundle.js', 'utf-8')
const renderer = rendererCache[file] = createBundleRenderer(bundle, options)
const renderer = createBundleRenderer(bundle, options)
cb(renderer)
})
}
Expand Down Expand Up @@ -224,4 +219,56 @@ describe('SSR: bundle renderer', () => {
})
}, { asBundle: true })
})

it('renderToString with template', done => {
createRenderer('app.js', renderer => {
const context = {
head: '<meta name="viewport" content="width=device-width">',
styles: '<style>h1 { color: red }</style>',
state: { a: 1 },
url: '/test'
}
renderer.renderToString(context, (err, res) => {
expect(err).toBeNull()
expect(res).toContain(
`<html><head>${context.head}${context.styles}</head><body>` +
`<div server-rendered="true">/test</div>` +
`<script>window.__INITIAL_STATE__={"a":1}</script>` +
`</body></html>`
)
expect(context.msg).toBe('hello')
done()
})
}, {
template: `<html><head></head><body><!--vue-ssr-outlet--></body></html>`
})
})

it('renderToStream with template', done => {
createRenderer('app.js', renderer => {
const context = {
head: '<meta name="viewport" content="width=device-width">',
styles: '<style>h1 { color: red }</style>',
state: { a: 1 },
url: '/test'
}
const stream = renderer.renderToStream(context)
let res = ''
stream.on('data', chunk => {
res += chunk.toString()
})
stream.on('end', () => {
expect(res).toContain(
`<html><head>${context.head}${context.styles}</head><body>` +
`<div server-rendered="true">/test</div>` +
`<script>window.__INITIAL_STATE__={"a":1}</script>` +
`</body></html>`
)
expect(context.msg).toBe('hello')
done()
})
}, {
template: `<html><head></head><body><!--vue-ssr-outlet--></body></html>`
})
})
})
30 changes: 30 additions & 0 deletions test/ssr/ssr-stream.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,34 @@ describe('SSR: renderToStream', () => {
stream1.read(1)
stream2.read(1)
})

it('should accept template option', done => {
const renderer = createRenderer({
template: `<html><head></head><body><!--vue-ssr-outlet--></body></html>`
})

const context = {
head: '<meta name="viewport" content="width=device-width">',
styles: '<style>h1 { color: red }</style>',
state: { a: 1 }
}

const stream = renderer.renderToStream(new Vue({
template: '<div>hi</div>'
}), context)

let res = ''
stream.on('data', chunk => {
res += chunk
})
stream.on('end', () => {
expect(res).toContain(
`<html><head>${context.head}${context.styles}</head><body>` +
`<div server-rendered="true">hi</div>` +
`<script>window.__INITIAL_STATE__={"a":1}</script>` +
`</body></html>`
)
done()
})
})
})
Loading

0 comments on commit 1c79592

Please sign in to comment.