export const createHook = <T extends Function = () => any>(
lifecycle: LifecycleHooks
) => (hook: T, target: ComponentInternalInstance | null = currentInstance) =>
- // post-create lifecycle registrations are noops during SSR
- !isInSSRComponentSetup && injectHook(lifecycle, hook, target)
+ // post-create lifecycle registrations are noops during SSR (except for serverPrefetch)
+ (!isInSSRComponentSetup || lifecycle === LifecycleHooks.SERVER_PREFETCH) &&
+ injectHook(lifecycle, hook, target)
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
export const onMounted = createHook(LifecycleHooks.MOUNTED)
export const onUpdated = createHook(LifecycleHooks.UPDATED)
export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)
+export const onServerPrefetch = createHook(LifecycleHooks.SERVER_PREFETCH)
export type DebuggerHook = (e: DebuggerEvent) => void
export const onRenderTriggered = createHook<DebuggerHook>(
LifecycleHooks.RENDER_TRACKED
)
-export type ErrorCapturedHook = (
- err: unknown,
+export type ErrorCapturedHook<TError = unknown> = (
+ err: TError,
instance: ComponentPublicInstance | null,
info: string
) => boolean | void
-export const onErrorCaptured = (
- hook: ErrorCapturedHook,
+export function onErrorCaptured<TError = Error>(
+ hook: ErrorCapturedHook<TError>,
target: ComponentInternalInstance | null = currentInstance
-) => {
+) {
injectHook(LifecycleHooks.ERROR_CAPTURED, hook, target)
}
watchEffect,
createVNode,
resolveDynamicComponent,
- renderSlot
+ renderSlot,
+ onErrorCaptured,
+ onServerPrefetch
} from 'vue'
import { escapeHtml } from '@vue/shared'
import { renderToString } from '../src/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')
+ })
})
}
Comment,
Component,
ComponentInternalInstance,
- ComponentOptions,
DirectiveBinding,
Fragment,
mergeProps,
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 {