feat(runtime-core): async component support
This commit is contained in:
parent
d425818901
commit
c3bb3169f4
464
packages/runtime-core/__tests__/apiAsyncComponent.spec.ts
Normal file
464
packages/runtime-core/__tests__/apiAsyncComponent.spec.ts
Normal file
@ -0,0 +1,464 @@
|
|||||||
|
import {
|
||||||
|
createAsyncComponent,
|
||||||
|
h,
|
||||||
|
Component,
|
||||||
|
ref,
|
||||||
|
nextTick,
|
||||||
|
Suspense
|
||||||
|
} from '../src'
|
||||||
|
import { createApp, nodeOps, serializeInner } from '@vue/runtime-test'
|
||||||
|
|
||||||
|
const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
|
||||||
|
|
||||||
|
describe('api: createAsyncComponent', () => {
|
||||||
|
test('simple usage', async () => {
|
||||||
|
let resolve: (comp: Component) => void
|
||||||
|
const Foo = createAsyncComponent(
|
||||||
|
() =>
|
||||||
|
new Promise(r => {
|
||||||
|
resolve = r as any
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const toggle = ref(true)
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
createApp({
|
||||||
|
components: { Foo },
|
||||||
|
render: () => (toggle.value ? h(Foo) : null)
|
||||||
|
}).mount(root)
|
||||||
|
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
|
resolve!(() => 'resolved')
|
||||||
|
// first time resolve, wait for macro task since there are multiple
|
||||||
|
// microtasks / .then() calls
|
||||||
|
await timeout()
|
||||||
|
expect(serializeInner(root)).toBe('resolved')
|
||||||
|
|
||||||
|
toggle.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
|
// already resolved component should update on nextTick
|
||||||
|
toggle.value = true
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe('resolved')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('with loading component', async () => {
|
||||||
|
let resolve: (comp: Component) => void
|
||||||
|
const Foo = createAsyncComponent({
|
||||||
|
loader: () =>
|
||||||
|
new Promise(r => {
|
||||||
|
resolve = r as any
|
||||||
|
}),
|
||||||
|
loading: () => 'loading',
|
||||||
|
delay: 1 // defaults to 200
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggle = ref(true)
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
createApp({
|
||||||
|
components: { Foo },
|
||||||
|
render: () => (toggle.value ? h(Foo) : null)
|
||||||
|
}).mount(root)
|
||||||
|
|
||||||
|
// due to the delay, initial mount should be empty
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
|
// loading show up after delay
|
||||||
|
await timeout(1)
|
||||||
|
expect(serializeInner(root)).toBe('loading')
|
||||||
|
|
||||||
|
resolve!(() => 'resolved')
|
||||||
|
await timeout()
|
||||||
|
expect(serializeInner(root)).toBe('resolved')
|
||||||
|
|
||||||
|
toggle.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
|
// already resolved component should update on nextTick without loading
|
||||||
|
// state
|
||||||
|
toggle.value = true
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe('resolved')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('with loading component + explicit delay (0)', async () => {
|
||||||
|
let resolve: (comp: Component) => void
|
||||||
|
const Foo = createAsyncComponent({
|
||||||
|
loader: () =>
|
||||||
|
new Promise(r => {
|
||||||
|
resolve = r as any
|
||||||
|
}),
|
||||||
|
loading: () => 'loading',
|
||||||
|
delay: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggle = ref(true)
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
createApp({
|
||||||
|
components: { Foo },
|
||||||
|
render: () => (toggle.value ? h(Foo) : null)
|
||||||
|
}).mount(root)
|
||||||
|
|
||||||
|
// with delay: 0, should show loading immediately
|
||||||
|
expect(serializeInner(root)).toBe('loading')
|
||||||
|
|
||||||
|
resolve!(() => 'resolved')
|
||||||
|
await timeout()
|
||||||
|
expect(serializeInner(root)).toBe('resolved')
|
||||||
|
|
||||||
|
toggle.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
|
// already resolved component should update on nextTick without loading
|
||||||
|
// state
|
||||||
|
toggle.value = true
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe('resolved')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('error without error component', async () => {
|
||||||
|
let resolve: (comp: Component) => void
|
||||||
|
let reject: (e: Error) => void
|
||||||
|
const Foo = createAsyncComponent(
|
||||||
|
() =>
|
||||||
|
new Promise((_resolve, _reject) => {
|
||||||
|
resolve = _resolve as any
|
||||||
|
reject = _reject
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const toggle = ref(true)
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
const app = createApp({
|
||||||
|
components: { Foo },
|
||||||
|
render: () => (toggle.value ? h(Foo) : null)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handler = (app.config.errorHandler = jest.fn())
|
||||||
|
|
||||||
|
app.mount(root)
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
|
const err = new Error('foo')
|
||||||
|
reject!(err)
|
||||||
|
await timeout()
|
||||||
|
expect(handler).toHaveBeenCalled()
|
||||||
|
expect(handler.mock.calls[0][0]).toBe(err)
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
|
toggle.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
|
// errored out on previous load, toggle and mock success this time
|
||||||
|
toggle.value = true
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
|
// should render this time
|
||||||
|
resolve!(() => 'resolved')
|
||||||
|
await timeout()
|
||||||
|
expect(serializeInner(root)).toBe('resolved')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('error with error component', async () => {
|
||||||
|
let resolve: (comp: Component) => void
|
||||||
|
let reject: (e: Error) => void
|
||||||
|
const Foo = createAsyncComponent({
|
||||||
|
loader: () =>
|
||||||
|
new Promise((_resolve, _reject) => {
|
||||||
|
resolve = _resolve as any
|
||||||
|
reject = _reject
|
||||||
|
}),
|
||||||
|
error: (props: { error: Error }) => props.error.message
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggle = ref(true)
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
const app = createApp({
|
||||||
|
components: { Foo },
|
||||||
|
render: () => (toggle.value ? h(Foo) : null)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handler = (app.config.errorHandler = jest.fn())
|
||||||
|
|
||||||
|
app.mount(root)
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
|
const err = new Error('errored out')
|
||||||
|
reject!(err)
|
||||||
|
await timeout()
|
||||||
|
// error handler will not be called if error component is present
|
||||||
|
expect(handler).not.toHaveBeenCalled()
|
||||||
|
expect(serializeInner(root)).toBe('errored out')
|
||||||
|
|
||||||
|
toggle.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
|
// errored out on previous load, toggle and mock success this time
|
||||||
|
toggle.value = true
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
|
// should render this time
|
||||||
|
resolve!(() => 'resolved')
|
||||||
|
await timeout()
|
||||||
|
expect(serializeInner(root)).toBe('resolved')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('error with error + loading components', async () => {
|
||||||
|
let resolve: (comp: Component) => void
|
||||||
|
let reject: (e: Error) => void
|
||||||
|
const Foo = createAsyncComponent({
|
||||||
|
loader: () =>
|
||||||
|
new Promise((_resolve, _reject) => {
|
||||||
|
resolve = _resolve as any
|
||||||
|
reject = _reject
|
||||||
|
}),
|
||||||
|
error: (props: { error: Error }) => props.error.message,
|
||||||
|
loading: () => 'loading',
|
||||||
|
delay: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggle = ref(true)
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
const app = createApp({
|
||||||
|
components: { Foo },
|
||||||
|
render: () => (toggle.value ? h(Foo) : null)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handler = (app.config.errorHandler = jest.fn())
|
||||||
|
|
||||||
|
app.mount(root)
|
||||||
|
|
||||||
|
// due to the delay, initial mount should be empty
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
|
// loading show up after delay
|
||||||
|
await timeout(1)
|
||||||
|
expect(serializeInner(root)).toBe('loading')
|
||||||
|
|
||||||
|
const err = new Error('errored out')
|
||||||
|
reject!(err)
|
||||||
|
await timeout()
|
||||||
|
// error handler will not be called if error component is present
|
||||||
|
expect(handler).not.toHaveBeenCalled()
|
||||||
|
expect(serializeInner(root)).toBe('errored out')
|
||||||
|
|
||||||
|
toggle.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
|
// errored out on previous load, toggle and mock success this time
|
||||||
|
toggle.value = true
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
|
// loading show up after delay
|
||||||
|
await timeout(1)
|
||||||
|
expect(serializeInner(root)).toBe('loading')
|
||||||
|
|
||||||
|
// should render this time
|
||||||
|
resolve!(() => 'resolved')
|
||||||
|
await timeout()
|
||||||
|
expect(serializeInner(root)).toBe('resolved')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('timeout without error component', async () => {
|
||||||
|
let resolve: (comp: Component) => void
|
||||||
|
const Foo = createAsyncComponent({
|
||||||
|
loader: () =>
|
||||||
|
new Promise(_resolve => {
|
||||||
|
resolve = _resolve as any
|
||||||
|
}),
|
||||||
|
timeout: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
const app = createApp({
|
||||||
|
components: { Foo },
|
||||||
|
render: () => h(Foo)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handler = (app.config.errorHandler = jest.fn())
|
||||||
|
|
||||||
|
app.mount(root)
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
|
await timeout(1)
|
||||||
|
expect(handler).toHaveBeenCalled()
|
||||||
|
expect(handler.mock.calls[0][0].message).toMatch(
|
||||||
|
`Async component timed out after 1ms.`
|
||||||
|
)
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
|
// if it resolved after timeout, should still work
|
||||||
|
resolve!(() => 'resolved')
|
||||||
|
await timeout()
|
||||||
|
expect(serializeInner(root)).toBe('resolved')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('timeout with error component', async () => {
|
||||||
|
let resolve: (comp: Component) => void
|
||||||
|
const Foo = createAsyncComponent({
|
||||||
|
loader: () =>
|
||||||
|
new Promise(_resolve => {
|
||||||
|
resolve = _resolve as any
|
||||||
|
}),
|
||||||
|
timeout: 1,
|
||||||
|
error: () => 'timed out'
|
||||||
|
})
|
||||||
|
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
const app = createApp({
|
||||||
|
components: { Foo },
|
||||||
|
render: () => h(Foo)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handler = (app.config.errorHandler = jest.fn())
|
||||||
|
|
||||||
|
app.mount(root)
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
|
await timeout(1)
|
||||||
|
expect(handler).not.toHaveBeenCalled()
|
||||||
|
expect(serializeInner(root)).toBe('timed out')
|
||||||
|
|
||||||
|
// if it resolved after timeout, should still work
|
||||||
|
resolve!(() => 'resolved')
|
||||||
|
await timeout()
|
||||||
|
expect(serializeInner(root)).toBe('resolved')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('timeout with error + loading components', async () => {
|
||||||
|
let resolve: (comp: Component) => void
|
||||||
|
const Foo = createAsyncComponent({
|
||||||
|
loader: () =>
|
||||||
|
new Promise(_resolve => {
|
||||||
|
resolve = _resolve as any
|
||||||
|
}),
|
||||||
|
delay: 1,
|
||||||
|
timeout: 16,
|
||||||
|
error: () => 'timed out',
|
||||||
|
loading: () => 'loading'
|
||||||
|
})
|
||||||
|
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
const app = createApp({
|
||||||
|
components: { Foo },
|
||||||
|
render: () => h(Foo)
|
||||||
|
})
|
||||||
|
app.mount(root)
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
await timeout(1)
|
||||||
|
expect(serializeInner(root)).toBe('loading')
|
||||||
|
|
||||||
|
await timeout(16)
|
||||||
|
expect(serializeInner(root)).toBe('timed out')
|
||||||
|
|
||||||
|
resolve!(() => 'resolved')
|
||||||
|
await timeout()
|
||||||
|
expect(serializeInner(root)).toBe('resolved')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('timeout without error component, but with loading component', async () => {
|
||||||
|
let resolve: (comp: Component) => void
|
||||||
|
const Foo = createAsyncComponent({
|
||||||
|
loader: () =>
|
||||||
|
new Promise(_resolve => {
|
||||||
|
resolve = _resolve as any
|
||||||
|
}),
|
||||||
|
delay: 1,
|
||||||
|
timeout: 16,
|
||||||
|
loading: () => 'loading'
|
||||||
|
})
|
||||||
|
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
const app = createApp({
|
||||||
|
components: { Foo },
|
||||||
|
render: () => h(Foo)
|
||||||
|
})
|
||||||
|
const handler = (app.config.errorHandler = jest.fn())
|
||||||
|
app.mount(root)
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
await timeout(1)
|
||||||
|
expect(serializeInner(root)).toBe('loading')
|
||||||
|
|
||||||
|
await timeout(16)
|
||||||
|
expect(handler).toHaveBeenCalled()
|
||||||
|
expect(handler.mock.calls[0][0].message).toMatch(
|
||||||
|
`Async component timed out after 16ms.`
|
||||||
|
)
|
||||||
|
// should still display loading
|
||||||
|
expect(serializeInner(root)).toBe('loading')
|
||||||
|
|
||||||
|
resolve!(() => 'resolved')
|
||||||
|
await timeout()
|
||||||
|
expect(serializeInner(root)).toBe('resolved')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('with suspense', async () => {
|
||||||
|
let resolve: (comp: Component) => void
|
||||||
|
const Foo = createAsyncComponent(
|
||||||
|
() =>
|
||||||
|
new Promise(_resolve => {
|
||||||
|
resolve = _resolve as any
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
const app = createApp({
|
||||||
|
components: { Foo },
|
||||||
|
render: () =>
|
||||||
|
h(Suspense, null, {
|
||||||
|
default: () => [h(Foo), ' & ', h(Foo)],
|
||||||
|
fallback: () => 'loading'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.mount(root)
|
||||||
|
expect(serializeInner(root)).toBe('loading')
|
||||||
|
|
||||||
|
resolve!(() => 'resolved')
|
||||||
|
await timeout()
|
||||||
|
expect(serializeInner(root)).toBe('resolved & resolved')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('suspensible: false', async () => {
|
||||||
|
let resolve: (comp: Component) => void
|
||||||
|
const Foo = createAsyncComponent({
|
||||||
|
loader: () =>
|
||||||
|
new Promise(_resolve => {
|
||||||
|
resolve = _resolve as any
|
||||||
|
}),
|
||||||
|
suspensible: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
const app = createApp({
|
||||||
|
components: { Foo },
|
||||||
|
render: () =>
|
||||||
|
h(Suspense, null, {
|
||||||
|
default: () => [h(Foo), ' & ', h(Foo)],
|
||||||
|
fallback: () => 'loading'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.mount(root)
|
||||||
|
// should not show suspense fallback
|
||||||
|
expect(serializeInner(root)).toBe('<!----> & <!---->')
|
||||||
|
|
||||||
|
resolve!(() => 'resolved')
|
||||||
|
await timeout()
|
||||||
|
expect(serializeInner(root)).toBe('resolved & resolved')
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
test.todo('suspense with error handling')
|
||||||
|
})
|
@ -379,6 +379,9 @@ describe('SSR hydration', () => {
|
|||||||
expect(container.innerHTML).toMatch(`<span>2</span><span>3</span>`)
|
expect(container.innerHTML).toMatch(`<span>2</span><span>3</span>`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
test.todo('async component')
|
||||||
|
|
||||||
describe('mismatch handling', () => {
|
describe('mismatch handling', () => {
|
||||||
test('text node', () => {
|
test('text node', () => {
|
||||||
const { container } = mountWithHydration(`foo`, () => 'bar')
|
const { container } = mountWithHydration(`foo`, () => 'bar')
|
||||||
|
155
packages/runtime-core/src/apiAsyncComponent.ts
Normal file
155
packages/runtime-core/src/apiAsyncComponent.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import {
|
||||||
|
PublicAPIComponent,
|
||||||
|
Component,
|
||||||
|
currentSuspense,
|
||||||
|
currentInstance,
|
||||||
|
ComponentInternalInstance
|
||||||
|
} from './component'
|
||||||
|
import { isFunction, isObject, EMPTY_OBJ } from '@vue/shared'
|
||||||
|
import { ComponentPublicInstance } from './componentProxy'
|
||||||
|
import { createVNode } from './vnode'
|
||||||
|
import { defineComponent } from './apiDefineComponent'
|
||||||
|
import { warn } from './warning'
|
||||||
|
import { ref } from '@vue/reactivity'
|
||||||
|
import { handleError, ErrorCodes } from './errorHandling'
|
||||||
|
|
||||||
|
export type AsyncComponentResolveResult<T = PublicAPIComponent> =
|
||||||
|
| T
|
||||||
|
| { default: T } // es modules
|
||||||
|
|
||||||
|
export type AsyncComponentLoader<T = any> = () => Promise<
|
||||||
|
AsyncComponentResolveResult<T>
|
||||||
|
>
|
||||||
|
|
||||||
|
export interface AsyncComponentOptions<T = any> {
|
||||||
|
loader: AsyncComponentLoader<T>
|
||||||
|
loading?: PublicAPIComponent
|
||||||
|
error?: PublicAPIComponent
|
||||||
|
delay?: number
|
||||||
|
timeout?: number
|
||||||
|
suspensible?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAsyncComponent<
|
||||||
|
T extends PublicAPIComponent = { new (): ComponentPublicInstance }
|
||||||
|
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
|
||||||
|
if (isFunction(source)) {
|
||||||
|
source = { loader: source }
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
suspensible = true,
|
||||||
|
loader,
|
||||||
|
loading: loadingComponent,
|
||||||
|
error: errorComponent,
|
||||||
|
delay = 200,
|
||||||
|
timeout // undefined = never times out
|
||||||
|
} = source
|
||||||
|
|
||||||
|
let pendingRequest: Promise<Component> | null = null
|
||||||
|
let resolvedComp: Component | undefined
|
||||||
|
|
||||||
|
const load = (): Promise<Component> => {
|
||||||
|
return (
|
||||||
|
pendingRequest ||
|
||||||
|
(pendingRequest = loader().then((comp: any) => {
|
||||||
|
// interop module default
|
||||||
|
if (comp.__esModule || comp[Symbol.toStringTag] === 'Module') {
|
||||||
|
comp = comp.default
|
||||||
|
}
|
||||||
|
if (__DEV__ && !isObject(comp) && !isFunction(comp)) {
|
||||||
|
warn(`Invalid async component load result: `, comp)
|
||||||
|
}
|
||||||
|
resolvedComp = comp
|
||||||
|
return comp
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return defineComponent({
|
||||||
|
name: 'AsyncComponentWrapper',
|
||||||
|
setup() {
|
||||||
|
const instance = currentInstance!
|
||||||
|
|
||||||
|
// already resolved
|
||||||
|
if (resolvedComp) {
|
||||||
|
return () => createInnerComp(resolvedComp!, instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// suspense-controlled
|
||||||
|
if (__FEATURE_SUSPENSE__ && suspensible && currentSuspense) {
|
||||||
|
return load().then(comp => {
|
||||||
|
return () => createInnerComp(comp, instance)
|
||||||
|
})
|
||||||
|
// TODO suspense error handling
|
||||||
|
}
|
||||||
|
|
||||||
|
// self-controlled
|
||||||
|
if (__NODE_JS__) {
|
||||||
|
// TODO SSR
|
||||||
|
}
|
||||||
|
// TODO hydration
|
||||||
|
|
||||||
|
const loaded = ref(false)
|
||||||
|
const error = ref()
|
||||||
|
const delayed = ref(!!delay)
|
||||||
|
|
||||||
|
if (delay) {
|
||||||
|
setTimeout(() => {
|
||||||
|
delayed.value = false
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeout != null) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!loaded.value) {
|
||||||
|
const err = new Error(
|
||||||
|
`Async component timed out after ${timeout}ms.`
|
||||||
|
)
|
||||||
|
if (errorComponent) {
|
||||||
|
error.value = err
|
||||||
|
} else {
|
||||||
|
handleError(err, instance, ErrorCodes.ASYNC_COMPONENT_LOADER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
load()
|
||||||
|
.then(() => {
|
||||||
|
loaded.value = true
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
pendingRequest = null
|
||||||
|
if (errorComponent) {
|
||||||
|
error.value = err
|
||||||
|
} else {
|
||||||
|
handleError(err, instance, ErrorCodes.ASYNC_COMPONENT_LOADER)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (loaded.value && resolvedComp) {
|
||||||
|
return createInnerComp(resolvedComp, instance)
|
||||||
|
} else if (error.value && errorComponent) {
|
||||||
|
return createVNode(errorComponent as Component, {
|
||||||
|
error: error.value
|
||||||
|
})
|
||||||
|
} else if (loadingComponent && !delayed.value) {
|
||||||
|
return createVNode(loadingComponent as Component)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) as any
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInnerComp(
|
||||||
|
comp: Component,
|
||||||
|
{ props, slots }: ComponentInternalInstance
|
||||||
|
) {
|
||||||
|
return createVNode(
|
||||||
|
comp,
|
||||||
|
props === EMPTY_OBJ ? null : props,
|
||||||
|
slots === EMPTY_OBJ ? null : slots
|
||||||
|
)
|
||||||
|
}
|
@ -19,6 +19,7 @@ export const enum ErrorCodes {
|
|||||||
APP_ERROR_HANDLER,
|
APP_ERROR_HANDLER,
|
||||||
APP_WARN_HANDLER,
|
APP_WARN_HANDLER,
|
||||||
FUNCTION_REF,
|
FUNCTION_REF,
|
||||||
|
ASYNC_COMPONENT_LOADER,
|
||||||
SCHEDULER
|
SCHEDULER
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,6 +50,7 @@ export const ErrorTypeStrings: Record<number | string, string> = {
|
|||||||
[ErrorCodes.APP_ERROR_HANDLER]: 'app errorHandler',
|
[ErrorCodes.APP_ERROR_HANDLER]: 'app errorHandler',
|
||||||
[ErrorCodes.APP_WARN_HANDLER]: 'app warnHandler',
|
[ErrorCodes.APP_WARN_HANDLER]: 'app warnHandler',
|
||||||
[ErrorCodes.FUNCTION_REF]: 'ref function',
|
[ErrorCodes.FUNCTION_REF]: 'ref function',
|
||||||
|
[ErrorCodes.ASYNC_COMPONENT_LOADER]: 'async component loader',
|
||||||
[ErrorCodes.SCHEDULER]:
|
[ErrorCodes.SCHEDULER]:
|
||||||
'scheduler flush. This is likely a Vue internals bug. ' +
|
'scheduler flush. This is likely a Vue internals bug. ' +
|
||||||
'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/vue-next'
|
'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/vue-next'
|
||||||
|
@ -34,6 +34,7 @@ export {
|
|||||||
export { provide, inject } from './apiInject'
|
export { provide, inject } from './apiInject'
|
||||||
export { nextTick } from './scheduler'
|
export { nextTick } from './scheduler'
|
||||||
export { defineComponent } from './apiDefineComponent'
|
export { defineComponent } from './apiDefineComponent'
|
||||||
|
export { createAsyncComponent } from './apiAsyncComponent'
|
||||||
|
|
||||||
// Advanced API ----------------------------------------------------------------
|
// Advanced API ----------------------------------------------------------------
|
||||||
|
|
||||||
@ -204,4 +205,8 @@ export {
|
|||||||
} from './directives'
|
} from './directives'
|
||||||
export { SuspenseBoundary } from './components/Suspense'
|
export { SuspenseBoundary } from './components/Suspense'
|
||||||
export { TransitionState, TransitionHooks } from './components/BaseTransition'
|
export { TransitionState, TransitionHooks } from './components/BaseTransition'
|
||||||
|
export {
|
||||||
|
AsyncComponentOptions,
|
||||||
|
AsyncComponentLoader
|
||||||
|
} from './apiAsyncComponent'
|
||||||
export { HMRRuntime } from './hmr'
|
export { HMRRuntime } from './hmr'
|
||||||
|
Loading…
Reference in New Issue
Block a user