feat: onServerPrefetch (#3070)
Support equivalent of `serverPrefetch` option via Composition API.
This commit is contained in:
parent
4aceec7b5e
commit
349eb0f0ad
@ -65,8 +65,9 @@ export function injectHook(
|
|||||||
export const createHook = <T extends Function = () => any>(
|
export const createHook = <T extends Function = () => any>(
|
||||||
lifecycle: LifecycleHooks
|
lifecycle: LifecycleHooks
|
||||||
) => (hook: T, target: ComponentInternalInstance | null = currentInstance) =>
|
) => (hook: T, target: ComponentInternalInstance | null = currentInstance) =>
|
||||||
// post-create lifecycle registrations are noops during SSR
|
// post-create lifecycle registrations are noops during SSR (except for serverPrefetch)
|
||||||
!isInSSRComponentSetup && injectHook(lifecycle, hook, target)
|
(!isInSSRComponentSetup || lifecycle === LifecycleHooks.SERVER_PREFETCH) &&
|
||||||
|
injectHook(lifecycle, hook, target)
|
||||||
|
|
||||||
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
|
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
|
||||||
export const onMounted = createHook(LifecycleHooks.MOUNTED)
|
export const onMounted = createHook(LifecycleHooks.MOUNTED)
|
||||||
@ -74,6 +75,7 @@ export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
|
|||||||
export const onUpdated = createHook(LifecycleHooks.UPDATED)
|
export const onUpdated = createHook(LifecycleHooks.UPDATED)
|
||||||
export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
|
export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
|
||||||
export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)
|
export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)
|
||||||
|
export const onServerPrefetch = createHook(LifecycleHooks.SERVER_PREFETCH)
|
||||||
|
|
||||||
export type DebuggerHook = (e: DebuggerEvent) => void
|
export type DebuggerHook = (e: DebuggerEvent) => void
|
||||||
export const onRenderTriggered = createHook<DebuggerHook>(
|
export const onRenderTriggered = createHook<DebuggerHook>(
|
||||||
@ -83,15 +85,15 @@ export const onRenderTracked = createHook<DebuggerHook>(
|
|||||||
LifecycleHooks.RENDER_TRACKED
|
LifecycleHooks.RENDER_TRACKED
|
||||||
)
|
)
|
||||||
|
|
||||||
export type ErrorCapturedHook = (
|
export type ErrorCapturedHook<TError = unknown> = (
|
||||||
err: unknown,
|
err: TError,
|
||||||
instance: ComponentPublicInstance | null,
|
instance: ComponentPublicInstance | null,
|
||||||
info: string
|
info: string
|
||||||
) => boolean | void
|
) => boolean | void
|
||||||
|
|
||||||
export const onErrorCaptured = (
|
export function onErrorCaptured<TError = Error>(
|
||||||
hook: ErrorCapturedHook,
|
hook: ErrorCapturedHook<TError>,
|
||||||
target: ComponentInternalInstance | null = currentInstance
|
target: ComponentInternalInstance | null = currentInstance
|
||||||
) => {
|
) {
|
||||||
injectHook(LifecycleHooks.ERROR_CAPTURED, hook, target)
|
injectHook(LifecycleHooks.ERROR_CAPTURED, hook, target)
|
||||||
}
|
}
|
||||||
|
@ -153,7 +153,7 @@ export type Component<
|
|||||||
|
|
||||||
export { ComponentOptions }
|
export { ComponentOptions }
|
||||||
|
|
||||||
type LifecycleHook = Function[] | null
|
type LifecycleHook<TFn = Function> = TFn[] | null
|
||||||
|
|
||||||
export const enum LifecycleHooks {
|
export const enum LifecycleHooks {
|
||||||
BEFORE_CREATE = 'bc',
|
BEFORE_CREATE = 'bc',
|
||||||
@ -168,7 +168,8 @@ export const enum LifecycleHooks {
|
|||||||
ACTIVATED = 'a',
|
ACTIVATED = 'a',
|
||||||
RENDER_TRIGGERED = 'rtg',
|
RENDER_TRIGGERED = 'rtg',
|
||||||
RENDER_TRACKED = 'rtc',
|
RENDER_TRACKED = 'rtc',
|
||||||
ERROR_CAPTURED = 'ec'
|
ERROR_CAPTURED = 'ec',
|
||||||
|
SERVER_PREFETCH = 'sp'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetupContext<E = EmitsOptions> {
|
export interface SetupContext<E = EmitsOptions> {
|
||||||
@ -414,6 +415,10 @@ export interface ComponentInternalInstance {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
[LifecycleHooks.ERROR_CAPTURED]: LifecycleHook
|
[LifecycleHooks.ERROR_CAPTURED]: LifecycleHook
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
[LifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise<unknown>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyAppContext = createAppContext()
|
const emptyAppContext = createAppContext()
|
||||||
@ -497,7 +502,8 @@ export function createComponentInstance(
|
|||||||
a: null,
|
a: null,
|
||||||
rtg: null,
|
rtg: null,
|
||||||
rtc: null,
|
rtc: null,
|
||||||
ec: null
|
ec: null,
|
||||||
|
sp: null
|
||||||
}
|
}
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
instance.ctx = createRenderContext(instance)
|
instance.ctx = createRenderContext(instance)
|
||||||
|
@ -40,7 +40,8 @@ import {
|
|||||||
onDeactivated,
|
onDeactivated,
|
||||||
onRenderTriggered,
|
onRenderTriggered,
|
||||||
DebuggerHook,
|
DebuggerHook,
|
||||||
ErrorCapturedHook
|
ErrorCapturedHook,
|
||||||
|
onServerPrefetch
|
||||||
} from './apiLifecycle'
|
} from './apiLifecycle'
|
||||||
import {
|
import {
|
||||||
reactive,
|
reactive,
|
||||||
@ -555,6 +556,7 @@ export function applyOptions(
|
|||||||
renderTracked,
|
renderTracked,
|
||||||
renderTriggered,
|
renderTriggered,
|
||||||
errorCaptured,
|
errorCaptured,
|
||||||
|
serverPrefetch,
|
||||||
// public API
|
// public API
|
||||||
expose
|
expose
|
||||||
} = options
|
} = options
|
||||||
@ -798,6 +800,9 @@ export function applyOptions(
|
|||||||
if (unmounted) {
|
if (unmounted) {
|
||||||
onUnmounted(unmounted.bind(publicThis))
|
onUnmounted(unmounted.bind(publicThis))
|
||||||
}
|
}
|
||||||
|
if (serverPrefetch) {
|
||||||
|
onServerPrefetch(serverPrefetch.bind(publicThis))
|
||||||
|
}
|
||||||
|
|
||||||
if (__COMPAT__) {
|
if (__COMPAT__) {
|
||||||
if (
|
if (
|
||||||
|
@ -37,7 +37,8 @@ export {
|
|||||||
onDeactivated,
|
onDeactivated,
|
||||||
onRenderTracked,
|
onRenderTracked,
|
||||||
onRenderTriggered,
|
onRenderTriggered,
|
||||||
onErrorCaptured
|
onErrorCaptured,
|
||||||
|
onServerPrefetch
|
||||||
} from './apiLifecycle'
|
} from './apiLifecycle'
|
||||||
export { provide, inject } from './apiInject'
|
export { provide, inject } from './apiInject'
|
||||||
export { nextTick } from './scheduler'
|
export { nextTick } from './scheduler'
|
||||||
|
@ -14,7 +14,9 @@ import {
|
|||||||
watchEffect,
|
watchEffect,
|
||||||
createVNode,
|
createVNode,
|
||||||
resolveDynamicComponent,
|
resolveDynamicComponent,
|
||||||
renderSlot
|
renderSlot,
|
||||||
|
onErrorCaptured,
|
||||||
|
onServerPrefetch
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import { escapeHtml } from '@vue/shared'
|
import { escapeHtml } from '@vue/shared'
|
||||||
import { renderToString } from '../src/renderToString'
|
import { renderToString } from '../src/renderToString'
|
||||||
@ -859,5 +861,211 @@ function testRender(type: string, render: typeof renderToString) {
|
|||||||
)
|
)
|
||||||
).toBe(`<div>A</div><div>B</div>`)
|
).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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import {
|
|||||||
Comment,
|
Comment,
|
||||||
Component,
|
Component,
|
||||||
ComponentInternalInstance,
|
ComponentInternalInstance,
|
||||||
ComponentOptions,
|
|
||||||
DirectiveBinding,
|
DirectiveBinding,
|
||||||
Fragment,
|
Fragment,
|
||||||
mergeProps,
|
mergeProps,
|
||||||
@ -87,13 +86,18 @@ export function renderComponentVNode(
|
|||||||
const instance = createComponentInstance(vnode, parentComponent, null)
|
const instance = createComponentInstance(vnode, parentComponent, null)
|
||||||
const res = setupComponent(instance, true /* isSSR */)
|
const res = setupComponent(instance, true /* isSSR */)
|
||||||
const hasAsyncSetup = isPromise(res)
|
const hasAsyncSetup = isPromise(res)
|
||||||
const prefetch = (vnode.type as ComponentOptions).serverPrefetch
|
const prefetches = instance.sp
|
||||||
if (hasAsyncSetup || prefetch) {
|
if (hasAsyncSetup || prefetches) {
|
||||||
let p = hasAsyncSetup ? (res as Promise<void>) : Promise.resolve()
|
let p: Promise<unknown> = hasAsyncSetup
|
||||||
if (prefetch) {
|
? (res as Promise<void>)
|
||||||
p = p.then(() => prefetch.call(instance.proxy)).catch(err => {
|
: Promise.resolve()
|
||||||
warn(`[@vue/server-renderer]: Uncaught error in serverPrefetch:\n`, err)
|
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))
|
return p.then(() => renderComponentSubTree(instance, slotScopeId))
|
||||||
} else {
|
} else {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user