diff --git a/flow/options.js b/flow/options.js
index 2e138a62ab9..ceaf8571e62 100644
--- a/flow/options.js
+++ b/flow/options.js
@@ -44,6 +44,7 @@ declare type ComponentOptions = {
beforeDestroy?: Function;
destroyed?: Function;
errorCaptured?: () => boolean | void;
+ ssrPrefetch?: Function;
// assets
directives?: { [key: string]: Object };
diff --git a/src/server/create-renderer.js b/src/server/create-renderer.js
index c045a426fe6..0d335462147 100644
--- a/src/server/create-renderer.js
+++ b/src/server/create-renderer.js
@@ -79,6 +79,9 @@ export function createRenderer ({
}, cb)
try {
render(component, write, context, err => {
+ if (context && context.rendered) {
+ context.rendered(context)
+ }
if (template) {
result = templateRenderer.renderSync(result, context)
}
@@ -106,6 +109,12 @@ export function createRenderer ({
render(component, write, context, done)
})
if (!template) {
+ if (context && context.rendered) {
+ const rendered = context.rendered
+ renderStream.once('beforeEnd', () => {
+ rendered(context)
+ })
+ }
return renderStream
} else {
const templateStream = templateRenderer.createStream(context)
@@ -113,6 +122,12 @@ export function createRenderer ({
templateStream.emit('error', err)
})
renderStream.pipe(templateStream)
+ if (context && context.rendered) {
+ const rendered = context.rendered
+ renderStream.once('beforeEnd', () => {
+ rendered(context)
+ })
+ }
return templateStream
}
}
diff --git a/src/server/render-stream.js b/src/server/render-stream.js
index d76012afb5b..bc358e20e95 100644
--- a/src/server/render-stream.js
+++ b/src/server/render-stream.js
@@ -42,6 +42,7 @@ export default class RenderStream extends stream.Readable {
})
this.end = () => {
+ this.emit('beforeEnd')
// the rendering is finished; we should push out the last of the buffer.
this.done = true
this.push(this.buffer)
diff --git a/src/server/render.js b/src/server/render.js
index 581a6dfd52c..e6195e0038d 100644
--- a/src/server/render.js
+++ b/src/server/render.js
@@ -19,6 +19,7 @@ let warned = Object.create(null)
const warnOnce = msg => {
if (!warned[msg]) {
warned[msg] = true
+ // eslint-disable-next-line no-console
console.warn(`\n\u001b[31m${msg}\u001b[39m\n`)
}
}
@@ -49,6 +50,27 @@ const normalizeRender = vm => {
}
}
+function waitForSsrPrefetch (vm, resolve, reject) {
+ let handlers = vm.$options.ssrPrefetch
+ if (isDef(handlers)) {
+ if (!Array.isArray(handlers)) handlers = [handlers]
+ try {
+ const promises = []
+ for (let i = 0, j = handlers.length; i < j; i++) {
+ const result = handlers[i].call(vm, vm)
+ if (result && typeof result.then === 'function') {
+ promises.push(result)
+ }
+ }
+ Promise.all(promises).then(resolve).catch(reject)
+ return
+ } catch (e) {
+ reject(e)
+ }
+ }
+ resolve()
+}
+
function renderNode (node, isRoot, context) {
if (node.isString) {
renderStringNode(node, context)
@@ -166,13 +188,20 @@ function renderComponentInner (node, isRoot, context) {
context.activeInstance
)
normalizeRender(child)
- const childNode = child._render()
- childNode.parent = node
- context.renderStates.push({
- type: 'Component',
- prevActive
- })
- renderNode(childNode, isRoot, context)
+
+ const resolve = () => {
+ const childNode = child._render()
+ childNode.parent = node
+ context.renderStates.push({
+ type: 'Component',
+ prevActive
+ })
+ renderNode(childNode, isRoot, context)
+ }
+
+ const reject = context.done
+
+ waitForSsrPrefetch(child, resolve, reject)
}
function renderAsyncComponent (node, isRoot, context) {
@@ -394,6 +423,10 @@ export function createRenderFunction (
})
installSSRHelpers(component)
normalizeRender(component)
- renderNode(component._render(), true, context)
+
+ const resolve = () => {
+ renderNode(component._render(), true, context)
+ }
+ waitForSsrPrefetch(component, resolve, done)
}
}
diff --git a/src/shared/constants.js b/src/shared/constants.js
index 84d019fb4ca..018d657b662 100644
--- a/src/shared/constants.js
+++ b/src/shared/constants.js
@@ -17,5 +17,6 @@ export const LIFECYCLE_HOOKS = [
'destroyed',
'activated',
'deactivated',
- 'errorCaptured'
+ 'errorCaptured',
+ 'ssrPrefetch'
]
diff --git a/test/ssr/ssr-stream.spec.js b/test/ssr/ssr-stream.spec.js
index 5973b5ed0d8..04e79658984 100644
--- a/test/ssr/ssr-stream.spec.js
+++ b/test/ssr/ssr-stream.spec.js
@@ -102,4 +102,26 @@ describe('SSR: renderToStream', () => {
stream1.read(1)
stream2.read(1)
})
+
+ it('should call context.rendered', done => {
+ let a = 0
+ const stream = renderToStream(new Vue({
+ template: `
+
Hello
+ `
+ }), {
+ rendered: () => {
+ a = 42
+ }
+ })
+ let res = ''
+ stream.on('data', chunk => {
+ res += chunk
+ })
+ stream.on('end', () => {
+ expect(res).toContain('Hello
')
+ expect(a).toBe(42)
+ done()
+ })
+ })
})
diff --git a/test/ssr/ssr-string.spec.js b/test/ssr/ssr-string.spec.js
index 2f98bab8fae..112b50e1b18 100644
--- a/test/ssr/ssr-string.spec.js
+++ b/test/ssr/ssr-string.spec.js
@@ -1311,6 +1311,194 @@ describe('SSR: renderToString', () => {
done()
})
})
+
+ it('should support ssrPrefetch option', done => {
+ renderVmWithOptions({
+ template: `
+ {{ count }}
+ `,
+ data: {
+ count: 0
+ },
+ ssrPrefetch () {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ this.count = 42
+ resolve()
+ }, 1)
+ })
+ }
+ }, result => {
+ expect(result).toContain('42
')
+ done()
+ })
+ })
+
+ it('should support ssrPrefetch option (nested)', done => {
+ renderVmWithOptions({
+ template: `
+
+ {{ count }}
+
+
+ `,
+ data: {
+ count: 0
+ },
+ ssrPrefetch () {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ this.count = 42
+ resolve()
+ }, 1)
+ })
+ },
+ components: {
+ nestedPrefetch: {
+ template: `
+ {{ message }}
+ `,
+ data () {
+ return {
+ message: ''
+ }
+ },
+ ssrPrefetch () {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ this.message = 'vue.js'
+ resolve()
+ }, 1)
+ })
+ }
+ }
+ }
+ }, result => {
+ expect(result).toContain('')
+ done()
+ })
+ })
+
+ it('should support ssrPrefetch option (nested async)', done => {
+ renderVmWithOptions({
+ template: `
+
+ {{ count }}
+
+
+ `,
+ data: {
+ count: 0
+ },
+ ssrPrefetch () {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ this.count = 42
+ resolve()
+ }, 1)
+ })
+ },
+ components: {
+ nestedPrefetch (resolve) {
+ resolve({
+ template: `
+ {{ message }}
+ `,
+ data () {
+ return {
+ message: ''
+ }
+ },
+ ssrPrefetch () {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ this.message = 'vue.js'
+ resolve()
+ }, 1)
+ })
+ }
+ })
+ }
+ }
+ }, result => {
+ expect(result).toContain('')
+ done()
+ })
+ })
+
+ it('should merge ssrPrefetch option', done => {
+ const mixin = {
+ data: {
+ message: ''
+ },
+ ssrPrefetch () {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ this.message = 'vue.js'
+ resolve()
+ }, 1)
+ })
+ }
+ }
+ renderVmWithOptions({
+ mixins: [mixin],
+ template: `
+
+
{{ count }}
+
{{ message }}
+
+ `,
+ data: {
+ count: 0
+ },
+ ssrPrefetch () {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ this.count = 42
+ resolve()
+ }, 1)
+ })
+ }
+ }, result => {
+ expect(result).toContain('')
+ done()
+ })
+ })
+
+ it(`should skip ssrPrefetch option that doesn't return a promise`, done => {
+ renderVmWithOptions({
+ template: `
+ {{ count }}
+ `,
+ data: {
+ count: 0
+ },
+ ssrPrefetch () {
+ setTimeout(() => {
+ this.count = 42
+ }, 1)
+ }
+ }, result => {
+ expect(result).toContain('0
')
+ done()
+ })
+ })
+
+ it('should call context.rendered', done => {
+ let a = 0
+ renderToString(new Vue({
+ template: 'Hello
'
+ }), {
+ rendered: () => {
+ a = 42
+ }
+ }, (err, res) => {
+ expect(err).toBeNull()
+ expect(res).toContain('Hello
')
+ expect(a).toBe(42)
+ done()
+ })
+ })
})
function renderVmWithOptions (options, cb) {
diff --git a/test/ssr/ssr-template.spec.js b/test/ssr/ssr-template.spec.js
index cf1ec6579e4..c9cc001b7bd 100644
--- a/test/ssr/ssr-template.spec.js
+++ b/test/ssr/ssr-template.spec.js
@@ -99,6 +99,41 @@ describe('SSR: template option', () => {
})
})
+ it('renderToString with interpolation and context.rendered', done => {
+ const renderer = createRenderer({
+ template: interpolateTemplate
+ })
+
+ const context = {
+ title: '',
+ snippet: 'foo
',
+ head: '',
+ styles: '',
+ state: { a: 0 },
+ rendered: context => {
+ context.state.a = 1
+ }
+ }
+
+ renderer.renderToString(new Vue({
+ template: 'hi
'
+ }), context, (err, res) => {
+ expect(err).toBeNull()
+ expect(res).toContain(
+ `` +
+ // double mustache should be escaped
+ `<script>hacks</script>` +
+ `${context.head}${context.styles}` +
+ `hi
` +
+ `` +
+ // triple should be raw
+ `foo
` +
+ ``
+ )
+ done()
+ })
+ })
+
it('renderToStream', done => {
const renderer = createRenderer({
template: defaultTemplate
@@ -166,6 +201,46 @@ describe('SSR: template option', () => {
})
})
+ it('renderToStream with interpolation and context.rendered', done => {
+ const renderer = createRenderer({
+ template: interpolateTemplate
+ })
+
+ const context = {
+ title: '',
+ snippet: 'foo
',
+ head: '',
+ styles: '',
+ state: { a: 0 },
+ rendered: context => {
+ context.state.a = 1
+ }
+ }
+
+ const stream = renderer.renderToStream(new Vue({
+ template: 'hi
'
+ }), context)
+
+ let res = ''
+ stream.on('data', chunk => {
+ res += chunk
+ })
+ stream.on('end', () => {
+ expect(res).toContain(
+ `` +
+ // double mustache should be escaped
+ `<script>hacks</script>` +
+ `${context.head}${context.styles}` +
+ `hi
` +
+ `` +
+ // triple should be raw
+ `foo
` +
+ ``
+ )
+ done()
+ })
+ })
+
it('bundleRenderer + renderToString', done => {
createBundleRenderer('app.js', {
asBundle: true,
diff --git a/types/options.d.ts b/types/options.d.ts
index d43b58d2ef2..0e98b4a9626 100644
--- a/types/options.d.ts
+++ b/types/options.d.ts
@@ -96,6 +96,7 @@ export interface ComponentOptions<
activated?(): void;
deactivated?(): void;
errorCaptured?(err: Error, vm: Vue, info: string): boolean | void;
+ ssrPrefetch?(this: V): Promise;
directives?: { [key: string]: DirectiveFunction | DirectiveOptions };
components?: { [key: string]: Component | AsyncComponent };
diff --git a/types/test/options-test.ts b/types/test/options-test.ts
index ce0e68deccb..4041aad44b1 100644
--- a/types/test/options-test.ts
+++ b/types/test/options-test.ts
@@ -241,6 +241,9 @@ Vue.component('component', {
info.toUpperCase()
return true
},
+ ssrPrefetch () {
+ return Promise.resolve()
+ },
directives: {
a: {