Skip to content

Commit 249beb6

Browse files
Akryumyyx990803
authored andcommitted
refactor: onServerPrefetch as a standard lifecycle hook
1 parent 6642102 commit 249beb6

File tree

5 files changed

+125
-44
lines changed

5 files changed

+125
-44
lines changed

packages/runtime-core/src/apiLifecycle.ts

Lines changed: 4 additions & 32 deletions
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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ export type Component<
153153

154154
export { ComponentOptions }
155155

156-
type LifecycleHook = Function[] | null
156+
type LifecycleHook<TFn = Function> = TFn[] | null
157157

158158
export const enum LifecycleHooks {
159159
BEFORE_CREATE = 'bc',
@@ -168,7 +168,8 @@ export const enum LifecycleHooks {
168168
ACTIVATED = 'a',
169169
RENDER_TRIGGERED = 'rtg',
170170
RENDER_TRACKED = 'rtc',
171-
ERROR_CAPTURED = 'ec'
171+
ERROR_CAPTURED = 'ec',
172+
SERVER_PREFETCH = 'sp'
172173
}
173174

174175
export interface SetupContext<E = EmitsOptions> {
@@ -414,6 +415,10 @@ export interface ComponentInternalInstance {
414415
* @internal
415416
*/
416417
[LifecycleHooks.ERROR_CAPTURED]: LifecycleHook
418+
/**
419+
* @internal
420+
*/
421+
[LifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise<unknown>>
417422
}
418423

419424
const emptyAppContext = createAppContext()
@@ -497,7 +502,8 @@ export function createComponentInstance(
497502
a: null,
498503
rtg: null,
499504
rtc: null,
500-
ec: null
505+
ec: null,
506+
sp: null
501507
}
502508
if (__DEV__) {
503509
instance.ctx = createRenderContext(instance)

packages/runtime-core/src/componentOptions.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ import {
4040
onDeactivated,
4141
onRenderTriggered,
4242
DebuggerHook,
43-
ErrorCapturedHook
43+
ErrorCapturedHook,
44+
onServerPrefetch
4445
} from './apiLifecycle'
4546
import {
4647
reactive,
@@ -555,6 +556,7 @@ export function applyOptions(
555556
renderTracked,
556557
renderTriggered,
557558
errorCaptured,
559+
serverPrefetch,
558560
// public API
559561
expose
560562
} = options
@@ -798,6 +800,9 @@ export function applyOptions(
798800
if (unmounted) {
799801
onUnmounted(unmounted.bind(publicThis))
800802
}
803+
if (serverPrefetch) {
804+
onServerPrefetch(serverPrefetch.bind(publicThis))
805+
}
801806

802807
if (__COMPAT__) {
803808
if (

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

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
createVNode,
1616
resolveDynamicComponent,
1717
renderSlot,
18+
onErrorCaptured,
1819
onServerPrefetch
1920
} from 'vue'
2021
import { escapeHtml } from '@vue/shared'
@@ -972,5 +973,98 @@ function testRender(type: string, render: typeof renderToString) {
972973
const html = await render(app)
973974
expect(html).toBe(`<div>hello hi</div>`)
974975
})
976+
977+
test('mixed in serverPrefetch', async () => {
978+
const msg = Promise.resolve('hello')
979+
const app = createApp({
980+
data() {
981+
return {
982+
msg: ''
983+
}
984+
},
985+
mixins: [
986+
{
987+
async serverPrefetch() {
988+
this.msg = await msg
989+
}
990+
}
991+
],
992+
render() {
993+
return h('div', this.msg)
994+
}
995+
})
996+
const html = await render(app)
997+
expect(html).toBe(`<div>hello</div>`)
998+
})
999+
1000+
test('many serverPrefetch', async () => {
1001+
const foo = Promise.resolve('foo')
1002+
const bar = Promise.resolve('bar')
1003+
const baz = Promise.resolve('baz')
1004+
const app = createApp({
1005+
data() {
1006+
return {
1007+
foo: '',
1008+
bar: '',
1009+
baz: ''
1010+
}
1011+
},
1012+
mixins: [
1013+
{
1014+
async serverPrefetch() {
1015+
this.foo = await foo
1016+
}
1017+
},
1018+
{
1019+
async serverPrefetch() {
1020+
this.bar = await bar
1021+
}
1022+
}
1023+
],
1024+
async serverPrefetch() {
1025+
this.baz = await baz
1026+
},
1027+
render() {
1028+
return h('div', `${this.foo}${this.bar}${this.baz}`)
1029+
}
1030+
})
1031+
const html = await render(app)
1032+
expect(html).toBe(`<div>foobarbaz</div>`)
1033+
})
1034+
1035+
test('onServerPrefetch throwing error', async () => {
1036+
let renderError: Error | null = null
1037+
let capturedError: Error | null = null
1038+
1039+
const Child = {
1040+
setup() {
1041+
onServerPrefetch(async () => {
1042+
throw new Error('An error')
1043+
})
1044+
},
1045+
render() {
1046+
return h('span')
1047+
}
1048+
}
1049+
1050+
const app = createApp({
1051+
setup() {
1052+
onErrorCaptured(e => {
1053+
capturedError = e
1054+
})
1055+
},
1056+
render() {
1057+
return h('div', h(Child))
1058+
}
1059+
})
1060+
try {
1061+
await render(app)
1062+
} catch (e) {
1063+
renderError = e
1064+
}
1065+
expect(`Unhandled error`).toHaveBeenWarned()
1066+
expect(renderError).toBe(null)
1067+
expect(((capturedError as unknown) as Error).message).toBe('An error')
1068+
})
9751069
})
9761070
}

packages/server-renderer/src/render.ts

Lines changed: 12 additions & 8 deletions
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,
@@ -87,13 +86,18 @@ export function renderComponentVNode(
8786
const instance = createComponentInstance(vnode, parentComponent, null)
8887
const res = setupComponent(instance, true /* isSSR */)
8988
const hasAsyncSetup = isPromise(res)
90-
const prefetch = (vnode.type as ComponentOptions).serverPrefetch
91-
if (hasAsyncSetup || prefetch) {
92-
let p = hasAsyncSetup ? (res as Promise<void>) : Promise.resolve()
93-
if (prefetch) {
94-
p = p.then(() => prefetch.call(instance.proxy)).catch(err => {
95-
warn(`[@vue/server-renderer]: Uncaught error in serverPrefetch:\n`, err)
96-
})
89+
const prefetches = instance.sp
90+
if (hasAsyncSetup || prefetches) {
91+
let p: Promise<unknown> = hasAsyncSetup
92+
? (res as Promise<void>)
93+
: Promise.resolve()
94+
if (prefetches) {
95+
p = p
96+
.then(() =>
97+
Promise.all(prefetches.map(prefetch => prefetch.call(instance.proxy)))
98+
)
99+
// Note: error display is already done by the wrapped lifecycle hook function.
100+
.catch(() => {})
97101
}
98102
return p.then(() => renderComponentSubTree(instance, slotScopeId))
99103
} else {

0 commit comments

Comments
 (0)