Skip to content

Commit 7edfc56

Browse files
committed
refactor: onServerPrefetch as a standard lifecycle hook
1 parent 1e40218 commit 7edfc56

File tree

5 files changed

+123
-45
lines changed

5 files changed

+123
-45
lines changed

packages/runtime-core/src/apiLifecycle.ts

+4-32
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {
22
ComponentInternalInstance,
3-
ComponentOptions,
43
currentInstance,
54
isInSSRComponentSetup,
65
LifecycleHooks,
@@ -66,15 +65,17 @@ export function injectHook(
6665
export const createHook = <T extends Function = () => any>(
6766
lifecycle: LifecycleHooks
6867
) => (hook: T, target: ComponentInternalInstance | null = currentInstance) =>
69-
// post-create lifecycle registrations are noops during SSR
70-
!isInSSRComponentSetup && injectHook(lifecycle, hook, target)
68+
// post-create lifecycle registrations are noops during SSR (except for serverPrefetch)
69+
(!isInSSRComponentSetup || lifecycle === LifecycleHooks.SERVER_PREFETCH) &&
70+
injectHook(lifecycle, hook, target)
7171

7272
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
7373
export const onMounted = createHook(LifecycleHooks.MOUNTED)
7474
export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
7575
export const onUpdated = createHook(LifecycleHooks.UPDATED)
7676
export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
7777
export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)
78+
export const onServerPrefetch = createHook(LifecycleHooks.SERVER_PREFETCH)
7879

7980
export type DebuggerHook = (e: DebuggerEvent) => void
8081
export const onRenderTriggered = createHook<DebuggerHook>(
@@ -96,32 +97,3 @@ export function onErrorCaptured<TError = Error>(
9697
) {
9798
injectHook(LifecycleHooks.ERROR_CAPTURED, hook, target)
9899
}
99-
100-
export function onServerPrefetch<
101-
T extends () => Promise<any> = () => Promise<unknown>
102-
>(handler: T) {
103-
const target = currentInstance
104-
if (target) {
105-
if (isInSSRComponentSetup) {
106-
const type = target.type as ComponentOptions
107-
let hook = type.serverPrefetch
108-
if (hook) {
109-
// Merge hook
110-
type.serverPrefetch = () =>
111-
Promise.all([handler(), (hook as Function).call(target.proxy)])
112-
} else {
113-
type.serverPrefetch = handler
114-
}
115-
}
116-
} else if (__DEV__) {
117-
warn(
118-
`onServerPrefetch is called when there is no active component instance to be ` +
119-
`associated with. ` +
120-
`Lifecycle injection APIs can only be used during execution of setup().` +
121-
(__FEATURE_SUSPENSE__
122-
? ` If you are using async setup(), make sure to register lifecycle ` +
123-
`hooks before the first await statement.`
124-
: ``)
125-
)
126-
}
127-
}

packages/runtime-core/src/component.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export type Component<
148148

149149
export { ComponentOptions }
150150

151-
type LifecycleHook = Function[] | null
151+
type LifecycleHook<TFn = Function> = TFn[] | null
152152

153153
export const enum LifecycleHooks {
154154
BEFORE_CREATE = 'bc',
@@ -163,7 +163,8 @@ export const enum LifecycleHooks {
163163
ACTIVATED = 'a',
164164
RENDER_TRIGGERED = 'rtg',
165165
RENDER_TRACKED = 'rtc',
166-
ERROR_CAPTURED = 'ec'
166+
ERROR_CAPTURED = 'ec',
167+
SERVER_PREFETCH = 'sp'
167168
}
168169

169170
export interface SetupContext<E = EmitsOptions> {
@@ -395,6 +396,10 @@ export interface ComponentInternalInstance {
395396
* @internal
396397
*/
397398
[LifecycleHooks.ERROR_CAPTURED]: LifecycleHook
399+
/**
400+
* @internal
401+
*/
402+
[LifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise<unknown>>
398403
}
399404

400405
const emptyAppContext = createAppContext()
@@ -475,7 +480,8 @@ export function createComponentInstance(
475480
a: null,
476481
rtg: null,
477482
rtc: null,
478-
ec: null
483+
ec: null,
484+
sp: null
479485
}
480486
if (__DEV__) {
481487
instance.ctx = createRenderContext(instance)

packages/runtime-core/src/componentOptions.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ import {
3535
onDeactivated,
3636
onRenderTriggered,
3737
DebuggerHook,
38-
ErrorCapturedHook
38+
ErrorCapturedHook,
39+
onServerPrefetch
3940
} from './apiLifecycle'
4041
import {
4142
reactive,
@@ -504,6 +505,7 @@ export function applyOptions(
504505
renderTracked,
505506
renderTriggered,
506507
errorCaptured,
508+
serverPrefetch,
507509
// public API
508510
expose
509511
} = options
@@ -791,6 +793,9 @@ export function applyOptions(
791793
if (unmounted) {
792794
onUnmounted(unmounted.bind(publicThis))
793795
}
796+
if (serverPrefetch) {
797+
onServerPrefetch(serverPrefetch.bind(publicThis))
798+
}
794799

795800
if (isArray(expose)) {
796801
if (!asMixin) {

packages/server-renderer/__tests__/render.spec.ts

+95-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
defineComponent,
1010
createTextVNode,
1111
createStaticVNode,
12-
onServerPrefetch
12+
onServerPrefetch,
13+
onErrorCaptured
1314
} from 'vue'
1415
import { escapeHtml } from '@vue/shared'
1516
import { renderToString } from '../src/renderToString'
@@ -828,5 +829,98 @@ function testRender(type: string, render: typeof renderToString) {
828829
const html = await render(app)
829830
expect(html).toBe(`<div>hello hi</div>`)
830831
})
832+
833+
test('mixed in serverPrefetch', async () => {
834+
const msg = Promise.resolve('hello')
835+
const app = createApp({
836+
data() {
837+
return {
838+
msg: ''
839+
}
840+
},
841+
mixins: [
842+
{
843+
async serverPrefetch() {
844+
this.msg = await msg
845+
}
846+
}
847+
],
848+
render() {
849+
return h('div', this.msg)
850+
}
851+
})
852+
const html = await render(app)
853+
expect(html).toBe(`<div>hello</div>`)
854+
})
855+
856+
test('many serverPrefetch', async () => {
857+
const foo = Promise.resolve('foo')
858+
const bar = Promise.resolve('bar')
859+
const baz = Promise.resolve('baz')
860+
const app = createApp({
861+
data() {
862+
return {
863+
foo: '',
864+
bar: '',
865+
baz: ''
866+
}
867+
},
868+
mixins: [
869+
{
870+
async serverPrefetch() {
871+
this.foo = await foo
872+
}
873+
},
874+
{
875+
async serverPrefetch() {
876+
this.bar = await bar
877+
}
878+
}
879+
],
880+
async serverPrefetch() {
881+
this.baz = await baz
882+
},
883+
render() {
884+
return h('div', `${this.foo}${this.bar}${this.baz}`)
885+
}
886+
})
887+
const html = await render(app)
888+
expect(html).toBe(`<div>foobarbaz</div>`)
889+
})
890+
891+
test('onServerPrefetch throwing error', async () => {
892+
let renderError: Error | null = null
893+
let capturedError: Error | null = null
894+
895+
const Child = {
896+
setup() {
897+
onServerPrefetch(async () => {
898+
throw new Error('An error')
899+
})
900+
},
901+
render() {
902+
return h('span')
903+
}
904+
}
905+
906+
const app = createApp({
907+
setup() {
908+
onErrorCaptured(e => {
909+
capturedError = e
910+
})
911+
},
912+
render() {
913+
return h('div', h(Child))
914+
}
915+
})
916+
try {
917+
await render(app)
918+
} catch (e) {
919+
renderError = e
920+
}
921+
expect(`Unhandled error`).toHaveBeenWarned()
922+
expect(renderError).toBe(null)
923+
expect(((capturedError as unknown) as Error).message).toBe('An error')
924+
})
831925
})
832926
}

packages/server-renderer/src/render.ts

+9-8
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
Comment,
33
Component,
44
ComponentInternalInstance,
5-
ComponentOptions,
65
DirectiveBinding,
76
Fragment,
87
mergeProps,
@@ -85,17 +84,19 @@ export function renderComponentVNode(
8584
const instance = createComponentInstance(vnode, parentComponent, null)
8685
const res = setupComponent(instance, true /* isSSR */)
8786
const hasAsyncSetup = isPromise(res)
88-
const prefetch = (vnode.type as ComponentOptions).serverPrefetch
89-
if (hasAsyncSetup || prefetch) {
90-
let p = hasAsyncSetup
87+
const prefetches = instance.sp
88+
if (hasAsyncSetup || prefetches) {
89+
let p: Promise<unknown> = hasAsyncSetup
9190
? (res as Promise<void>).catch(err => {
9291
warn(`[@vue/server-renderer]: Uncaught error in async setup:\n`, err)
9392
})
9493
: Promise.resolve()
95-
if (prefetch) {
96-
p = p.then(() => prefetch.call(instance.proxy)).catch(err => {
97-
warn(`[@vue/server-renderer]: Uncaught error in serverPrefetch:\n`, err)
98-
})
94+
if (prefetches) {
95+
p = p
96+
.then(() =>
97+
Promise.all(prefetches.map(prefetch => prefetch.call(instance.proxy)))
98+
)
99+
.catch(() => {})
99100
}
100101
return p.then(() => renderComponentSubTree(instance))
101102
} else {

0 commit comments

Comments
 (0)