Skip to content

Commit

Permalink
feat: onServerPrefetch (#3070)
Browse files Browse the repository at this point in the history
Support equivalent of `serverPrefetch` option via Composition API.
  • Loading branch information
Akryum authored May 7, 2021
1 parent 4aceec7 commit 349eb0f
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 21 deletions.
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(() => {})
}
return p.then(() => renderComponentSubTree(instance, slotScopeId))
} else {
Expand Down

0 comments on commit 349eb0f

Please sign in to comment.