Skip to content

Commit 349eb0f

Browse files
authored
feat: onServerPrefetch (#3070)
Support equivalent of `serverPrefetch` option via Composition API.
1 parent 4aceec7 commit 349eb0f

File tree

6 files changed

+247
-21
lines changed

6 files changed

+247
-21
lines changed

packages/runtime-core/src/apiLifecycle.ts

+9-7
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,17 @@ export function injectHook(
6565
export const createHook = <T extends Function = () => any>(
6666
lifecycle: LifecycleHooks
6767
) => (hook: T, target: ComponentInternalInstance | null = currentInstance) =>
68-
// post-create lifecycle registrations are noops during SSR
69-
!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)
7071

7172
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
7273
export const onMounted = createHook(LifecycleHooks.MOUNTED)
7374
export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
7475
export const onUpdated = createHook(LifecycleHooks.UPDATED)
7576
export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
7677
export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)
78+
export const onServerPrefetch = createHook(LifecycleHooks.SERVER_PREFETCH)
7779

7880
export type DebuggerHook = (e: DebuggerEvent) => void
7981
export const onRenderTriggered = createHook<DebuggerHook>(
@@ -83,15 +85,15 @@ export const onRenderTracked = createHook<DebuggerHook>(
8385
LifecycleHooks.RENDER_TRACKED
8486
)
8587

86-
export type ErrorCapturedHook = (
87-
err: unknown,
88+
export type ErrorCapturedHook<TError = unknown> = (
89+
err: TError,
8890
instance: ComponentPublicInstance | null,
8991
info: string
9092
) => boolean | void
9193

92-
export const onErrorCaptured = (
93-
hook: ErrorCapturedHook,
94+
export function onErrorCaptured<TError = Error>(
95+
hook: ErrorCapturedHook<TError>,
9496
target: ComponentInternalInstance | null = currentInstance
95-
) => {
97+
) {
9698
injectHook(LifecycleHooks.ERROR_CAPTURED, hook, target)
9799
}

packages/runtime-core/src/component.ts

+9-3
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

+6-1
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/runtime-core/src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export {
3737
onDeactivated,
3838
onRenderTracked,
3939
onRenderTriggered,
40-
onErrorCaptured
40+
onErrorCaptured,
41+
onServerPrefetch
4142
} from './apiLifecycle'
4243
export { provide, inject } from './apiInject'
4344
export { nextTick } from './scheduler'

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

+209-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import {
1414
watchEffect,
1515
createVNode,
1616
resolveDynamicComponent,
17-
renderSlot
17+
renderSlot,
18+
onErrorCaptured,
19+
onServerPrefetch
1820
} from 'vue'
1921
import { escapeHtml } from '@vue/shared'
2022
import { renderToString } from '../src/renderToString'
@@ -859,5 +861,211 @@ function testRender(type: string, render: typeof renderToString) {
859861
)
860862
).toBe(`<div>A</div><div>B</div>`)
861863
})
864+
865+
test('onServerPrefetch', async () => {
866+
const msg = Promise.resolve('hello')
867+
const app = createApp({
868+
setup() {
869+
const message = ref('')
870+
onServerPrefetch(async () => {
871+
message.value = await msg
872+
})
873+
return {
874+
message
875+
}
876+
},
877+
render() {
878+
return h('div', this.message)
879+
}
880+
})
881+
const html = await render(app)
882+
expect(html).toBe(`<div>hello</div>`)
883+
})
884+
885+
test('multiple onServerPrefetch', async () => {
886+
const msg = Promise.resolve('hello')
887+
const msg2 = Promise.resolve('hi')
888+
const msg3 = Promise.resolve('bonjour')
889+
const app = createApp({
890+
setup() {
891+
const message = ref('')
892+
const message2 = ref('')
893+
const message3 = ref('')
894+
onServerPrefetch(async () => {
895+
message.value = await msg
896+
})
897+
onServerPrefetch(async () => {
898+
message2.value = await msg2
899+
})
900+
onServerPrefetch(async () => {
901+
message3.value = await msg3
902+
})
903+
return {
904+
message,
905+
message2,
906+
message3
907+
}
908+
},
909+
render() {
910+
return h('div', `${this.message} ${this.message2} ${this.message3}`)
911+
}
912+
})
913+
const html = await render(app)
914+
expect(html).toBe(`<div>hello hi bonjour</div>`)
915+
})
916+
917+
test('onServerPrefetch are run in parallel', async () => {
918+
const first = jest.fn(() => Promise.resolve())
919+
const second = jest.fn(() => Promise.resolve())
920+
let checkOther = [false, false]
921+
let done = [false, false]
922+
const app = createApp({
923+
setup() {
924+
onServerPrefetch(async () => {
925+
checkOther[0] = done[1]
926+
await first()
927+
done[0] = true
928+
})
929+
onServerPrefetch(async () => {
930+
checkOther[1] = done[0]
931+
await second()
932+
done[1] = true
933+
})
934+
},
935+
render() {
936+
return h('div', '')
937+
}
938+
})
939+
await render(app)
940+
expect(first).toHaveBeenCalled()
941+
expect(second).toHaveBeenCalled()
942+
expect(checkOther).toEqual([false, false])
943+
expect(done).toEqual([true, true])
944+
})
945+
946+
test('onServerPrefetch with serverPrefetch option', async () => {
947+
const msg = Promise.resolve('hello')
948+
const msg2 = Promise.resolve('hi')
949+
const app = createApp({
950+
data() {
951+
return {
952+
message: ''
953+
}
954+
},
955+
956+
async serverPrefetch() {
957+
this.message = await msg
958+
},
959+
960+
setup() {
961+
const message2 = ref('')
962+
onServerPrefetch(async () => {
963+
message2.value = await msg2
964+
})
965+
return {
966+
message2
967+
}
968+
},
969+
render() {
970+
return h('div', `${this.message} ${this.message2}`)
971+
}
972+
})
973+
const html = await render(app)
974+
expect(html).toBe(`<div>hello hi</div>`)
975+
})
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+
return false
1055+
})
1056+
},
1057+
render() {
1058+
return h('div', h(Child))
1059+
}
1060+
})
1061+
1062+
try {
1063+
await render(app)
1064+
} catch (e) {
1065+
renderError = e
1066+
}
1067+
expect(renderError).toBe(null)
1068+
expect(((capturedError as unknown) as Error).message).toBe('An error')
1069+
})
8621070
})
8631071
}

packages/server-renderer/src/render.ts

+12-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,
@@ -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)