feat: onServerPrefetch (#3070)

Support equivalent of `serverPrefetch` option via Composition API.
This commit is contained in:
Guillaume Chau
2021-05-07 18:00:52 +02:00
committed by GitHub
parent 4aceec7b5e
commit 349eb0f0ad
6 changed files with 247 additions and 21 deletions

View File

@@ -14,7 +14,9 @@ import {
watchEffect,
createVNode,
resolveDynamicComponent,
renderSlot
renderSlot,
onErrorCaptured,
onServerPrefetch
} from 'vue'
import { escapeHtml } from '@vue/shared'
import { renderToString } from '../src/renderToString'
@@ -859,5 +861,211 @@ function testRender(type: string, render: typeof renderToString) {
)
).toBe(`<div>A</div><div>B</div>`)
})
test('onServerPrefetch', async () => {
const msg = Promise.resolve('hello')
const app = createApp({
setup() {
const message = ref('')
onServerPrefetch(async () => {
message.value = await msg
})
return {
message
}
},
render() {
return h('div', this.message)
}
})
const html = await render(app)
expect(html).toBe(`<div>hello</div>`)
})
test('multiple onServerPrefetch', async () => {
const msg = Promise.resolve('hello')
const msg2 = Promise.resolve('hi')
const msg3 = Promise.resolve('bonjour')
const app = createApp({
setup() {
const message = ref('')
const message2 = ref('')
const message3 = ref('')
onServerPrefetch(async () => {
message.value = await msg
})
onServerPrefetch(async () => {
message2.value = await msg2
})
onServerPrefetch(async () => {
message3.value = await msg3
})
return {
message,
message2,
message3
}
},
render() {
return h('div', `${this.message} ${this.message2} ${this.message3}`)
}
})
const html = await render(app)
expect(html).toBe(`<div>hello hi bonjour</div>`)
})
test('onServerPrefetch are run in parallel', async () => {
const first = jest.fn(() => Promise.resolve())
const second = jest.fn(() => Promise.resolve())
let checkOther = [false, false]
let done = [false, false]
const app = createApp({
setup() {
onServerPrefetch(async () => {
checkOther[0] = done[1]
await first()
done[0] = true
})
onServerPrefetch(async () => {
checkOther[1] = done[0]
await second()
done[1] = true
})
},
render() {
return h('div', '')
}
})
await render(app)
expect(first).toHaveBeenCalled()
expect(second).toHaveBeenCalled()
expect(checkOther).toEqual([false, false])
expect(done).toEqual([true, true])
})
test('onServerPrefetch with serverPrefetch option', async () => {
const msg = Promise.resolve('hello')
const msg2 = Promise.resolve('hi')
const app = createApp({
data() {
return {
message: ''
}
},
async serverPrefetch() {
this.message = await msg
},
setup() {
const message2 = ref('')
onServerPrefetch(async () => {
message2.value = await msg2
})
return {
message2
}
},
render() {
return h('div', `${this.message} ${this.message2}`)
}
})
const html = await render(app)
expect(html).toBe(`<div>hello hi</div>`)
})
test('mixed in serverPrefetch', async () => {
const msg = Promise.resolve('hello')
const app = createApp({
data() {
return {
msg: ''
}
},
mixins: [
{
async serverPrefetch() {
this.msg = await msg
}
}
],
render() {
return h('div', this.msg)
}
})
const html = await render(app)
expect(html).toBe(`<div>hello</div>`)
})
test('many serverPrefetch', async () => {
const foo = Promise.resolve('foo')
const bar = Promise.resolve('bar')
const baz = Promise.resolve('baz')
const app = createApp({
data() {
return {
foo: '',
bar: '',
baz: ''
}
},
mixins: [
{
async serverPrefetch() {
this.foo = await foo
}
},
{
async serverPrefetch() {
this.bar = await bar
}
}
],
async serverPrefetch() {
this.baz = await baz
},
render() {
return h('div', `${this.foo}${this.bar}${this.baz}`)
}
})
const html = await render(app)
expect(html).toBe(`<div>foobarbaz</div>`)
})
test('onServerPrefetch throwing error', async () => {
let renderError: Error | null = null
let capturedError: Error | null = null
const Child = {
setup() {
onServerPrefetch(async () => {
throw new Error('An error')
})
},
render() {
return h('span')
}
}
const app = createApp({
setup() {
onErrorCaptured(e => {
capturedError = e
return false
})
},
render() {
return h('div', h(Child))
}
})
try {
await render(app)
} catch (e) {
renderError = e
}
expect(renderError).toBe(null)
expect(((capturedError as unknown) as Error).message).toBe('An error')
})
})
}

View File

@@ -2,7 +2,6 @@ import {
Comment,
Component,
ComponentInternalInstance,
ComponentOptions,
DirectiveBinding,
Fragment,
mergeProps,
@@ -87,13 +86,18 @@ export function renderComponentVNode(
const instance = createComponentInstance(vnode, parentComponent, null)
const res = setupComponent(instance, true /* isSSR */)
const hasAsyncSetup = isPromise(res)
const prefetch = (vnode.type as ComponentOptions).serverPrefetch
if (hasAsyncSetup || prefetch) {
let p = hasAsyncSetup ? (res as Promise<void>) : Promise.resolve()
if (prefetch) {
p = p.then(() => prefetch.call(instance.proxy)).catch(err => {
warn(`[@vue/server-renderer]: Uncaught error in serverPrefetch:\n`, err)
})
const prefetches = instance.sp
if (hasAsyncSetup || prefetches) {
let p: Promise<unknown> = hasAsyncSetup
? (res as Promise<void>)
: Promise.resolve()
if (prefetches) {
p = p
.then(() =>
Promise.all(prefetches.map(prefetch => prefetch.call(instance.proxy)))
)
// Note: error display is already done by the wrapped lifecycle hook function.
.catch(() => {})
}
return p.then(() => renderComponentSubTree(instance, slotScopeId))
} else {