Skip to content

Commit d7a533d

Browse files
Akryumyyx990803
authored andcommitted
feat(ssr): ssrPrefetch option + context.rendered hook (#9017)
1 parent f036cce commit d7a533d

10 files changed

+349
-9
lines changed

flow/options.js

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ declare type ComponentOptions = {
4444
beforeDestroy?: Function;
4545
destroyed?: Function;
4646
errorCaptured?: () => boolean | void;
47+
ssrPrefetch?: Function;
4748

4849
// assets
4950
directives?: { [key: string]: Object };

src/server/create-renderer.js

+15
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ export function createRenderer ({
7979
}, cb)
8080
try {
8181
render(component, write, context, err => {
82+
if (context && context.rendered) {
83+
context.rendered(context)
84+
}
8285
if (template) {
8386
result = templateRenderer.renderSync(result, context)
8487
}
@@ -106,13 +109,25 @@ export function createRenderer ({
106109
render(component, write, context, done)
107110
})
108111
if (!template) {
112+
if (context && context.rendered) {
113+
const rendered = context.rendered
114+
renderStream.once('beforeEnd', () => {
115+
rendered(context)
116+
})
117+
}
109118
return renderStream
110119
} else {
111120
const templateStream = templateRenderer.createStream(context)
112121
renderStream.on('error', err => {
113122
templateStream.emit('error', err)
114123
})
115124
renderStream.pipe(templateStream)
125+
if (context && context.rendered) {
126+
const rendered = context.rendered
127+
renderStream.once('beforeEnd', () => {
128+
rendered(context)
129+
})
130+
}
116131
return templateStream
117132
}
118133
}

src/server/render-stream.js

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export default class RenderStream extends stream.Readable {
4242
})
4343

4444
this.end = () => {
45+
this.emit('beforeEnd')
4546
// the rendering is finished; we should push out the last of the buffer.
4647
this.done = true
4748
this.push(this.buffer)

src/server/render.js

+41-8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ let warned = Object.create(null)
1919
const warnOnce = msg => {
2020
if (!warned[msg]) {
2121
warned[msg] = true
22+
// eslint-disable-next-line no-console
2223
console.warn(`\n\u001b[31m${msg}\u001b[39m\n`)
2324
}
2425
}
@@ -49,6 +50,27 @@ const normalizeRender = vm => {
4950
}
5051
}
5152

53+
function waitForSsrPrefetch (vm, resolve, reject) {
54+
let handlers = vm.$options.ssrPrefetch
55+
if (isDef(handlers)) {
56+
if (!Array.isArray(handlers)) handlers = [handlers]
57+
try {
58+
const promises = []
59+
for (let i = 0, j = handlers.length; i < j; i++) {
60+
const result = handlers[i].call(vm, vm)
61+
if (result && typeof result.then === 'function') {
62+
promises.push(result)
63+
}
64+
}
65+
Promise.all(promises).then(resolve).catch(reject)
66+
return
67+
} catch (e) {
68+
reject(e)
69+
}
70+
}
71+
resolve()
72+
}
73+
5274
function renderNode (node, isRoot, context) {
5375
if (node.isString) {
5476
renderStringNode(node, context)
@@ -166,13 +188,20 @@ function renderComponentInner (node, isRoot, context) {
166188
context.activeInstance
167189
)
168190
normalizeRender(child)
169-
const childNode = child._render()
170-
childNode.parent = node
171-
context.renderStates.push({
172-
type: 'Component',
173-
prevActive
174-
})
175-
renderNode(childNode, isRoot, context)
191+
192+
const resolve = () => {
193+
const childNode = child._render()
194+
childNode.parent = node
195+
context.renderStates.push({
196+
type: 'Component',
197+
prevActive
198+
})
199+
renderNode(childNode, isRoot, context)
200+
}
201+
202+
const reject = context.done
203+
204+
waitForSsrPrefetch(child, resolve, reject)
176205
}
177206

178207
function renderAsyncComponent (node, isRoot, context) {
@@ -394,6 +423,10 @@ export function createRenderFunction (
394423
})
395424
installSSRHelpers(component)
396425
normalizeRender(component)
397-
renderNode(component._render(), true, context)
426+
427+
const resolve = () => {
428+
renderNode(component._render(), true, context)
429+
}
430+
waitForSsrPrefetch(component, resolve, done)
398431
}
399432
}

src/shared/constants.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ export const LIFECYCLE_HOOKS = [
1717
'destroyed',
1818
'activated',
1919
'deactivated',
20-
'errorCaptured'
20+
'errorCaptured',
21+
'ssrPrefetch'
2122
]

test/ssr/ssr-stream.spec.js

+22
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,26 @@ describe('SSR: renderToStream', () => {
102102
stream1.read(1)
103103
stream2.read(1)
104104
})
105+
106+
it('should call context.rendered', done => {
107+
let a = 0
108+
const stream = renderToStream(new Vue({
109+
template: `
110+
<div>Hello</div>
111+
`
112+
}), {
113+
rendered: () => {
114+
a = 42
115+
}
116+
})
117+
let res = ''
118+
stream.on('data', chunk => {
119+
res += chunk
120+
})
121+
stream.on('end', () => {
122+
expect(res).toContain('<div data-server-rendered="true">Hello</div>')
123+
expect(a).toBe(42)
124+
done()
125+
})
126+
})
105127
})

test/ssr/ssr-string.spec.js

+188
Original file line numberDiff line numberDiff line change
@@ -1311,6 +1311,194 @@ describe('SSR: renderToString', () => {
13111311
done()
13121312
})
13131313
})
1314+
1315+
it('should support ssrPrefetch option', done => {
1316+
renderVmWithOptions({
1317+
template: `
1318+
<div>{{ count }}</div>
1319+
`,
1320+
data: {
1321+
count: 0
1322+
},
1323+
ssrPrefetch () {
1324+
return new Promise((resolve) => {
1325+
setTimeout(() => {
1326+
this.count = 42
1327+
resolve()
1328+
}, 1)
1329+
})
1330+
}
1331+
}, result => {
1332+
expect(result).toContain('<div data-server-rendered="true">42</div>')
1333+
done()
1334+
})
1335+
})
1336+
1337+
it('should support ssrPrefetch option (nested)', done => {
1338+
renderVmWithOptions({
1339+
template: `
1340+
<div>
1341+
<span>{{ count }}</span>
1342+
<nested-prefetch></nested-prefetch>
1343+
</div>
1344+
`,
1345+
data: {
1346+
count: 0
1347+
},
1348+
ssrPrefetch () {
1349+
return new Promise((resolve) => {
1350+
setTimeout(() => {
1351+
this.count = 42
1352+
resolve()
1353+
}, 1)
1354+
})
1355+
},
1356+
components: {
1357+
nestedPrefetch: {
1358+
template: `
1359+
<div>{{ message }}</div>
1360+
`,
1361+
data () {
1362+
return {
1363+
message: ''
1364+
}
1365+
},
1366+
ssrPrefetch () {
1367+
return new Promise((resolve) => {
1368+
setTimeout(() => {
1369+
this.message = 'vue.js'
1370+
resolve()
1371+
}, 1)
1372+
})
1373+
}
1374+
}
1375+
}
1376+
}, result => {
1377+
expect(result).toContain('<div data-server-rendered="true"><span>42</span> <div>vue.js</div></div>')
1378+
done()
1379+
})
1380+
})
1381+
1382+
it('should support ssrPrefetch option (nested async)', done => {
1383+
renderVmWithOptions({
1384+
template: `
1385+
<div>
1386+
<span>{{ count }}</span>
1387+
<nested-prefetch></nested-prefetch>
1388+
</div>
1389+
`,
1390+
data: {
1391+
count: 0
1392+
},
1393+
ssrPrefetch () {
1394+
return new Promise((resolve) => {
1395+
setTimeout(() => {
1396+
this.count = 42
1397+
resolve()
1398+
}, 1)
1399+
})
1400+
},
1401+
components: {
1402+
nestedPrefetch (resolve) {
1403+
resolve({
1404+
template: `
1405+
<div>{{ message }}</div>
1406+
`,
1407+
data () {
1408+
return {
1409+
message: ''
1410+
}
1411+
},
1412+
ssrPrefetch () {
1413+
return new Promise((resolve) => {
1414+
setTimeout(() => {
1415+
this.message = 'vue.js'
1416+
resolve()
1417+
}, 1)
1418+
})
1419+
}
1420+
})
1421+
}
1422+
}
1423+
}, result => {
1424+
expect(result).toContain('<div data-server-rendered="true"><span>42</span> <div>vue.js</div></div>')
1425+
done()
1426+
})
1427+
})
1428+
1429+
it('should merge ssrPrefetch option', done => {
1430+
const mixin = {
1431+
data: {
1432+
message: ''
1433+
},
1434+
ssrPrefetch () {
1435+
return new Promise((resolve) => {
1436+
setTimeout(() => {
1437+
this.message = 'vue.js'
1438+
resolve()
1439+
}, 1)
1440+
})
1441+
}
1442+
}
1443+
renderVmWithOptions({
1444+
mixins: [mixin],
1445+
template: `
1446+
<div>
1447+
<span>{{ count }}</span>
1448+
<div>{{ message }}</div>
1449+
</div>
1450+
`,
1451+
data: {
1452+
count: 0
1453+
},
1454+
ssrPrefetch () {
1455+
return new Promise((resolve) => {
1456+
setTimeout(() => {
1457+
this.count = 42
1458+
resolve()
1459+
}, 1)
1460+
})
1461+
}
1462+
}, result => {
1463+
expect(result).toContain('<div data-server-rendered="true"><span>42</span> <div>vue.js</div></div>')
1464+
done()
1465+
})
1466+
})
1467+
1468+
it(`should skip ssrPrefetch option that doesn't return a promise`, done => {
1469+
renderVmWithOptions({
1470+
template: `
1471+
<div>{{ count }}</div>
1472+
`,
1473+
data: {
1474+
count: 0
1475+
},
1476+
ssrPrefetch () {
1477+
setTimeout(() => {
1478+
this.count = 42
1479+
}, 1)
1480+
}
1481+
}, result => {
1482+
expect(result).toContain('<div data-server-rendered="true">0</div>')
1483+
done()
1484+
})
1485+
})
1486+
1487+
it('should call context.rendered', done => {
1488+
let a = 0
1489+
renderToString(new Vue({
1490+
template: '<div>Hello</div>'
1491+
}), {
1492+
rendered: () => {
1493+
a = 42
1494+
}
1495+
}, (err, res) => {
1496+
expect(err).toBeNull()
1497+
expect(res).toContain('<div data-server-rendered="true">Hello</div>')
1498+
expect(a).toBe(42)
1499+
done()
1500+
})
1501+
})
13141502
})
13151503

13161504
function renderVmWithOptions (options, cb) {

0 commit comments

Comments
 (0)