Skip to content

feat: onServerPrefetch #3070

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions packages/runtime-core/src/apiLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,17 @@ export function injectHook(
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 onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
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>(
Expand All @@ -83,15 +85,15 @@ export const onRenderTracked = 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)
}
12 changes: 9 additions & 3 deletions packages/runtime-core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export type Component<

export { ComponentOptions }

type LifecycleHook = Function[] | null
type LifecycleHook<TFn = Function> = TFn[] | null

export const enum LifecycleHooks {
BEFORE_CREATE = 'bc',
Expand All @@ -168,7 +168,8 @@ export const enum LifecycleHooks {
ACTIVATED = 'a',
RENDER_TRIGGERED = 'rtg',
RENDER_TRACKED = 'rtc',
ERROR_CAPTURED = 'ec'
ERROR_CAPTURED = 'ec',
SERVER_PREFETCH = 'sp'
}

export interface SetupContext<E = EmitsOptions> {
Expand Down Expand Up @@ -414,6 +415,10 @@ export interface ComponentInternalInstance {
* @internal
*/
[LifecycleHooks.ERROR_CAPTURED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise<unknown>>
}

const emptyAppContext = createAppContext()
Expand Down Expand Up @@ -497,7 +502,8 @@ export function createComponentInstance(
a: null,
rtg: null,
rtc: null,
ec: null
ec: null,
sp: null
}
if (__DEV__) {
instance.ctx = createRenderContext(instance)
Expand Down
7 changes: 6 additions & 1 deletion packages/runtime-core/src/componentOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ import {
onDeactivated,
onRenderTriggered,
DebuggerHook,
ErrorCapturedHook
ErrorCapturedHook,
onServerPrefetch
} from './apiLifecycle'
import {
reactive,
Expand Down Expand Up @@ -555,6 +556,7 @@ export function applyOptions(
renderTracked,
renderTriggered,
errorCaptured,
serverPrefetch,
// public API
expose
} = options
Expand Down Expand Up @@ -798,6 +800,9 @@ export function applyOptions(
if (unmounted) {
onUnmounted(unmounted.bind(publicThis))
}
if (serverPrefetch) {
onServerPrefetch(serverPrefetch.bind(publicThis))
}

if (__COMPAT__) {
if (
Expand Down
3 changes: 2 additions & 1 deletion packages/runtime-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ export {
onDeactivated,
onRenderTracked,
onRenderTriggered,
onErrorCaptured
onErrorCaptured,
onServerPrefetch
} from './apiLifecycle'
export { provide, inject } from './apiInject'
export { nextTick } from './scheduler'
Expand Down
210 changes: 209 additions & 1 deletion packages/server-renderer/__tests__/render.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import {
watchEffect,
createVNode,
resolveDynamicComponent,
renderSlot
renderSlot,
onErrorCaptured,
onServerPrefetch
} from 'vue'
import { escapeHtml } from '@vue/shared'
import { renderToString } from '../src/renderToString'
Expand Down Expand Up @@ -859,5 +861,211 @@ function testRender(type: string, render: typeof 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')
})
})
}
20 changes: 12 additions & 8 deletions packages/server-renderer/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
Comment,
Component,
ComponentInternalInstance,
ComponentOptions,
DirectiveBinding,
Fragment,
mergeProps,
Expand Down Expand Up @@ -87,13 +86,18 @@ export function renderComponentVNode(
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(() => {})
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: error display is already done by the wrapped lifecycle hook function.

}
return p.then(() => renderComponentSubTree(instance, slotScopeId))
} else {
Expand Down