Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ssr): ssrPrefetch option #9017

Merged
merged 11 commits into from
Dec 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions flow/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ declare type ComponentOptions = {
beforeDestroy?: Function;
destroyed?: Function;
errorCaptured?: () => boolean | void;
ssrPrefetch?: Function;

// assets
directives?: { [key: string]: Object };
Expand Down
15 changes: 15 additions & 0 deletions src/server/create-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -106,13 +109,25 @@ 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)
renderStream.on('error', err => {
templateStream.emit('error', err)
})
renderStream.pipe(templateStream)
if (context && context.rendered) {
const rendered = context.rendered
renderStream.once('beforeEnd', () => {
rendered(context)
})
}
return templateStream
}
}
Expand Down
1 change: 1 addition & 0 deletions src/server/render-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
49 changes: 41 additions & 8 deletions src/server/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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`)
}
}
Expand Down Expand Up @@ -48,6 +49,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++) {
Copy link

@galvez galvez Dec 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Akryum pardon me the intrusion -- just thought i'd drop this here -- I see that you're using a pattern for for loops that I used to employ myself, but, unless you assign the length to an external variable, this idiom actually makes things slower :(

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)
Expand Down Expand Up @@ -165,13 +187,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) {
Expand Down Expand Up @@ -391,6 +420,10 @@ export function createRenderFunction (
})
installSSRHelpers(component)
normalizeRender(component)
renderNode(component._render(), true, context)

const resolve = () => {
renderNode(component._render(), true, context)
}
waitForSsrPrefetch(component, resolve, done)
}
}
3 changes: 2 additions & 1 deletion src/shared/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ export const LIFECYCLE_HOOKS = [
'destroyed',
'activated',
'deactivated',
'errorCaptured'
'errorCaptured',
'ssrPrefetch'
]
22 changes: 22 additions & 0 deletions test/ssr/ssr-stream.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
})
188 changes: 188 additions & 0 deletions test/ssr/ssr-string.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1277,6 +1277,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) {
Expand Down