mirror of
https://github.com/vuejs/vue.git
synced 2024-11-22 04:39:46 +00:00
feat(ssr): ssrPrefetch option + context.rendered hook (#9017)
This commit is contained in:
parent
f036cce163
commit
d7a533d6f8
@ -44,6 +44,7 @@ declare type ComponentOptions = {
|
||||
beforeDestroy?: Function;
|
||||
destroyed?: Function;
|
||||
errorCaptured?: () => boolean | void;
|
||||
ssrPrefetch?: Function;
|
||||
|
||||
// assets
|
||||
directives?: { [key: string]: Object };
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -17,5 +17,6 @@ export const LIFECYCLE_HOOKS = [
|
||||
'destroyed',
|
||||
'activated',
|
||||
'deactivated',
|
||||
'errorCaptured'
|
||||
'errorCaptured',
|
||||
'ssrPrefetch'
|
||||
]
|
||||
|
@ -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: `
|
||||
<div>Hello</div>
|
||||
`
|
||||
}), {
|
||||
rendered: () => {
|
||||
a = 42
|
||||
}
|
||||
})
|
||||
let res = ''
|
||||
stream.on('data', chunk => {
|
||||
res += chunk
|
||||
})
|
||||
stream.on('end', () => {
|
||||
expect(res).toContain('<div data-server-rendered="true">Hello</div>')
|
||||
expect(a).toBe(42)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1311,6 +1311,194 @@ describe('SSR: renderToString', () => {
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should support ssrPrefetch option', done => {
|
||||
renderVmWithOptions({
|
||||
template: `
|
||||
<div>{{ count }}</div>
|
||||
`,
|
||||
data: {
|
||||
count: 0
|
||||
},
|
||||
ssrPrefetch () {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
this.count = 42
|
||||
resolve()
|
||||
}, 1)
|
||||
})
|
||||
}
|
||||
}, result => {
|
||||
expect(result).toContain('<div data-server-rendered="true">42</div>')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should support ssrPrefetch option (nested)', done => {
|
||||
renderVmWithOptions({
|
||||
template: `
|
||||
<div>
|
||||
<span>{{ count }}</span>
|
||||
<nested-prefetch></nested-prefetch>
|
||||
</div>
|
||||
`,
|
||||
data: {
|
||||
count: 0
|
||||
},
|
||||
ssrPrefetch () {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
this.count = 42
|
||||
resolve()
|
||||
}, 1)
|
||||
})
|
||||
},
|
||||
components: {
|
||||
nestedPrefetch: {
|
||||
template: `
|
||||
<div>{{ message }}</div>
|
||||
`,
|
||||
data () {
|
||||
return {
|
||||
message: ''
|
||||
}
|
||||
},
|
||||
ssrPrefetch () {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
this.message = 'vue.js'
|
||||
resolve()
|
||||
}, 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}, result => {
|
||||
expect(result).toContain('<div data-server-rendered="true"><span>42</span> <div>vue.js</div></div>')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should support ssrPrefetch option (nested async)', done => {
|
||||
renderVmWithOptions({
|
||||
template: `
|
||||
<div>
|
||||
<span>{{ count }}</span>
|
||||
<nested-prefetch></nested-prefetch>
|
||||
</div>
|
||||
`,
|
||||
data: {
|
||||
count: 0
|
||||
},
|
||||
ssrPrefetch () {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
this.count = 42
|
||||
resolve()
|
||||
}, 1)
|
||||
})
|
||||
},
|
||||
components: {
|
||||
nestedPrefetch (resolve) {
|
||||
resolve({
|
||||
template: `
|
||||
<div>{{ message }}</div>
|
||||
`,
|
||||
data () {
|
||||
return {
|
||||
message: ''
|
||||
}
|
||||
},
|
||||
ssrPrefetch () {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
this.message = 'vue.js'
|
||||
resolve()
|
||||
}, 1)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}, result => {
|
||||
expect(result).toContain('<div data-server-rendered="true"><span>42</span> <div>vue.js</div></div>')
|
||||
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: `
|
||||
<div>
|
||||
<span>{{ count }}</span>
|
||||
<div>{{ message }}</div>
|
||||
</div>
|
||||
`,
|
||||
data: {
|
||||
count: 0
|
||||
},
|
||||
ssrPrefetch () {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
this.count = 42
|
||||
resolve()
|
||||
}, 1)
|
||||
})
|
||||
}
|
||||
}, result => {
|
||||
expect(result).toContain('<div data-server-rendered="true"><span>42</span> <div>vue.js</div></div>')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it(`should skip ssrPrefetch option that doesn't return a promise`, done => {
|
||||
renderVmWithOptions({
|
||||
template: `
|
||||
<div>{{ count }}</div>
|
||||
`,
|
||||
data: {
|
||||
count: 0
|
||||
},
|
||||
ssrPrefetch () {
|
||||
setTimeout(() => {
|
||||
this.count = 42
|
||||
}, 1)
|
||||
}
|
||||
}, result => {
|
||||
expect(result).toContain('<div data-server-rendered="true">0</div>')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call context.rendered', done => {
|
||||
let a = 0
|
||||
renderToString(new Vue({
|
||||
template: '<div>Hello</div>'
|
||||
}), {
|
||||
rendered: () => {
|
||||
a = 42
|
||||
}
|
||||
}, (err, res) => {
|
||||
expect(err).toBeNull()
|
||||
expect(res).toContain('<div data-server-rendered="true">Hello</div>')
|
||||
expect(a).toBe(42)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function renderVmWithOptions (options, cb) {
|
||||
|
@ -99,6 +99,41 @@ describe('SSR: template option', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('renderToString with interpolation and context.rendered', done => {
|
||||
const renderer = createRenderer({
|
||||
template: interpolateTemplate
|
||||
})
|
||||
|
||||
const context = {
|
||||
title: '<script>hacks</script>',
|
||||
snippet: '<div>foo</div>',
|
||||
head: '<meta name="viewport" content="width=device-width">',
|
||||
styles: '<style>h1 { color: red }</style>',
|
||||
state: { a: 0 },
|
||||
rendered: context => {
|
||||
context.state.a = 1
|
||||
}
|
||||
}
|
||||
|
||||
renderer.renderToString(new Vue({
|
||||
template: '<div>hi</div>'
|
||||
}), context, (err, res) => {
|
||||
expect(err).toBeNull()
|
||||
expect(res).toContain(
|
||||
`<html><head>` +
|
||||
// double mustache should be escaped
|
||||
`<title><script>hacks</script></title>` +
|
||||
`${context.head}${context.styles}</head><body>` +
|
||||
`<div data-server-rendered="true">hi</div>` +
|
||||
`<script>window.__INITIAL_STATE__={"a":1}</script>` +
|
||||
// triple should be raw
|
||||
`<div>foo</div>` +
|
||||
`</body></html>`
|
||||
)
|
||||
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: '<script>hacks</script>',
|
||||
snippet: '<div>foo</div>',
|
||||
head: '<meta name="viewport" content="width=device-width">',
|
||||
styles: '<style>h1 { color: red }</style>',
|
||||
state: { a: 0 },
|
||||
rendered: context => {
|
||||
context.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>` +
|
||||
// double mustache should be escaped
|
||||
`<title><script>hacks</script></title>` +
|
||||
`${context.head}${context.styles}</head><body>` +
|
||||
`<div data-server-rendered="true">hi</div>` +
|
||||
`<script>window.__INITIAL_STATE__={"a":1}</script>` +
|
||||
// triple should be raw
|
||||
`<div>foo</div>` +
|
||||
`</body></html>`
|
||||
)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('bundleRenderer + renderToString', done => {
|
||||
createBundleRenderer('app.js', {
|
||||
asBundle: true,
|
||||
|
1
types/options.d.ts
vendored
1
types/options.d.ts
vendored
@ -96,6 +96,7 @@ export interface ComponentOptions<
|
||||
activated?(): void;
|
||||
deactivated?(): void;
|
||||
errorCaptured?(err: Error, vm: Vue, info: string): boolean | void;
|
||||
ssrPrefetch?(this: V): Promise<void>;
|
||||
|
||||
directives?: { [key: string]: DirectiveFunction | DirectiveOptions };
|
||||
components?: { [key: string]: Component<any, any, any, any> | AsyncComponent<any, any, any, any> };
|
||||
|
@ -241,6 +241,9 @@ Vue.component('component', {
|
||||
info.toUpperCase()
|
||||
return true
|
||||
},
|
||||
ssrPrefetch () {
|
||||
return Promise.resolve()
|
||||
},
|
||||
|
||||
directives: {
|
||||
a: {
|
||||
|
Loading…
Reference in New Issue
Block a user