refactor(ssr): improve ssr async setup / suspense error handling
This commit is contained in:
parent
9c4de7b9ed
commit
47ead3b33a
@ -337,7 +337,7 @@ function setupStatefulComponent(
|
|||||||
// 2. create props proxy
|
// 2. create props proxy
|
||||||
// the propsProxy is a reactive AND readonly proxy to the actual props.
|
// the propsProxy is a reactive AND readonly proxy to the actual props.
|
||||||
// it will be updated in resolveProps() on updates before render
|
// it will be updated in resolveProps() on updates before render
|
||||||
const propsProxy = (instance.propsProxy = isInSSRComponentSetup
|
const propsProxy = (instance.propsProxy = isSSR
|
||||||
? instance.props
|
? instance.props
|
||||||
: shallowReadonly(instance.props))
|
: shallowReadonly(instance.props))
|
||||||
// 3. call setup()
|
// 3. call setup()
|
||||||
@ -360,7 +360,7 @@ function setupStatefulComponent(
|
|||||||
currentSuspense = null
|
currentSuspense = null
|
||||||
|
|
||||||
if (isPromise(setupResult)) {
|
if (isPromise(setupResult)) {
|
||||||
if (isInSSRComponentSetup) {
|
if (isSSR) {
|
||||||
// return the promise so server-renderer can wait on it
|
// return the promise so server-renderer can wait on it
|
||||||
return setupResult.then(resolvedResult => {
|
return setupResult.then(resolvedResult => {
|
||||||
handleSetupResult(instance, resolvedResult, parentSuspense, isSSR)
|
handleSetupResult(instance, resolvedResult, parentSuspense, isSSR)
|
||||||
|
@ -2,6 +2,16 @@ import { createApp, h, Suspense } from 'vue'
|
|||||||
import { renderToString } from '../src/renderToString'
|
import { renderToString } from '../src/renderToString'
|
||||||
|
|
||||||
describe('SSR Suspense', () => {
|
describe('SSR Suspense', () => {
|
||||||
|
let logError: jest.SpyInstance
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
logError = jest.spyOn(console, 'error').mockImplementation(() => {})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
logError.mockRestore()
|
||||||
|
})
|
||||||
|
|
||||||
const ResolvingAsync = {
|
const ResolvingAsync = {
|
||||||
async setup() {
|
async setup() {
|
||||||
return () => h('div', 'async')
|
return () => h('div', 'async')
|
||||||
@ -10,7 +20,7 @@ describe('SSR Suspense', () => {
|
|||||||
|
|
||||||
const RejectingAsync = {
|
const RejectingAsync = {
|
||||||
setup() {
|
setup() {
|
||||||
return new Promise((_, reject) => reject())
|
return new Promise((_, reject) => reject('foo'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,6 +35,7 @@ describe('SSR Suspense', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(await renderToString(createApp(Comp))).toBe(`<div>async</div>`)
|
expect(await renderToString(createApp(Comp))).toBe(`<div>async</div>`)
|
||||||
|
expect(logError).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('fallback', async () => {
|
test('fallback', async () => {
|
||||||
@ -38,6 +49,7 @@ describe('SSR Suspense', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(await renderToString(createApp(Comp))).toBe(`<div>fallback</div>`)
|
expect(await renderToString(createApp(Comp))).toBe(`<div>fallback</div>`)
|
||||||
|
expect(logError).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('2 components', async () => {
|
test('2 components', async () => {
|
||||||
@ -53,6 +65,7 @@ describe('SSR Suspense', () => {
|
|||||||
expect(await renderToString(createApp(Comp))).toBe(
|
expect(await renderToString(createApp(Comp))).toBe(
|
||||||
`<div><div>async</div><div>async</div></div>`
|
`<div><div>async</div><div>async</div></div>`
|
||||||
)
|
)
|
||||||
|
expect(logError).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('resolving component + rejecting component', async () => {
|
test('resolving component + rejecting component', async () => {
|
||||||
@ -66,6 +79,7 @@ describe('SSR Suspense', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(await renderToString(createApp(Comp))).toBe(`<div>fallback</div>`)
|
expect(await renderToString(createApp(Comp))).toBe(`<div>fallback</div>`)
|
||||||
|
expect(logError).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('failing suspense in passing suspense', async () => {
|
test('failing suspense in passing suspense', async () => {
|
||||||
@ -87,6 +101,7 @@ describe('SSR Suspense', () => {
|
|||||||
expect(await renderToString(createApp(Comp))).toBe(
|
expect(await renderToString(createApp(Comp))).toBe(
|
||||||
`<div><div>async</div><div>fallback 2</div></div>`
|
`<div><div>async</div><div>fallback 2</div></div>`
|
||||||
)
|
)
|
||||||
|
expect(logError).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('passing suspense in failing suspense', async () => {
|
test('passing suspense in failing suspense', async () => {
|
||||||
@ -106,5 +121,6 @@ describe('SSR Suspense', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(await renderToString(createApp(Comp))).toBe(`<div>fallback 1</div>`)
|
expect(await renderToString(createApp(Comp))).toBe(`<div>fallback 1</div>`)
|
||||||
|
expect(logError).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
Fragment,
|
Fragment,
|
||||||
ssrUtils,
|
ssrUtils,
|
||||||
Slots,
|
Slots,
|
||||||
warn,
|
|
||||||
createApp,
|
createApp,
|
||||||
ssrContextKey
|
ssrContextKey
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
@ -139,6 +138,8 @@ export function renderComponent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const AsyncSetupErrorMarker = Symbol('Vue async setup error')
|
||||||
|
|
||||||
function renderComponentVNode(
|
function renderComponentVNode(
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
parentComponent: ComponentInternalInstance | null = null
|
parentComponent: ComponentInternalInstance | null = null
|
||||||
@ -150,7 +151,21 @@ function renderComponentVNode(
|
|||||||
true /* isSSR */
|
true /* isSSR */
|
||||||
)
|
)
|
||||||
if (isPromise(res)) {
|
if (isPromise(res)) {
|
||||||
return res.then(() => renderComponentSubTree(instance))
|
return res
|
||||||
|
.catch(err => {
|
||||||
|
// normalize async setup rejection
|
||||||
|
if (!(err instanceof Error)) {
|
||||||
|
err = new Error(String(err))
|
||||||
|
}
|
||||||
|
err[AsyncSetupErrorMarker] = true
|
||||||
|
console.error(
|
||||||
|
`[@vue/server-renderer]: Uncaught error in async setup:\n`,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
// rethrow for suspense
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
.then(() => renderComponentSubTree(instance))
|
||||||
} else {
|
} else {
|
||||||
return renderComponentSubTree(instance)
|
return renderComponentSubTree(instance)
|
||||||
}
|
}
|
||||||
@ -208,7 +223,9 @@ function ssrCompile(
|
|||||||
isNativeTag: instance.appContext.config.isNativeTag || NO,
|
isNativeTag: instance.appContext.config.isNativeTag || NO,
|
||||||
onError(err: CompilerError) {
|
onError(err: CompilerError) {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
const message = `Template compilation error: ${err.message}`
|
const message = `[@vue/server-renderer] Template compilation error: ${
|
||||||
|
err.message
|
||||||
|
}`
|
||||||
const codeFrame =
|
const codeFrame =
|
||||||
err.loc &&
|
err.loc &&
|
||||||
generateCodeFrame(
|
generateCodeFrame(
|
||||||
@ -216,7 +233,7 @@ function ssrCompile(
|
|||||||
err.loc.start.offset,
|
err.loc.start.offset,
|
||||||
err.loc.end.offset
|
err.loc.end.offset
|
||||||
)
|
)
|
||||||
warn(codeFrame ? `${message}\n${codeFrame}` : message)
|
console.error(codeFrame ? `${message}\n${codeFrame}` : message)
|
||||||
} else {
|
} else {
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
@ -243,15 +260,15 @@ function renderVNode(
|
|||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
if (shapeFlag & ShapeFlags.ELEMENT) {
|
if (shapeFlag & ShapeFlags.ELEMENT) {
|
||||||
renderElement(push, vnode, parentComponent)
|
renderElementVNode(push, vnode, parentComponent)
|
||||||
} else if (shapeFlag & ShapeFlags.COMPONENT) {
|
} else if (shapeFlag & ShapeFlags.COMPONENT) {
|
||||||
push(renderComponentVNode(vnode, parentComponent))
|
push(renderComponentVNode(vnode, parentComponent))
|
||||||
} else if (shapeFlag & ShapeFlags.PORTAL) {
|
} else if (shapeFlag & ShapeFlags.PORTAL) {
|
||||||
renderPortal(vnode, parentComponent)
|
renderPortalVNode(vnode, parentComponent)
|
||||||
} else if (shapeFlag & ShapeFlags.SUSPENSE) {
|
} else if (shapeFlag & ShapeFlags.SUSPENSE) {
|
||||||
push(renderSuspense(vnode, parentComponent))
|
push(renderSuspenseVNode(vnode, parentComponent))
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.error(
|
||||||
'[@vue/server-renderer] Invalid VNode type:',
|
'[@vue/server-renderer] Invalid VNode type:',
|
||||||
type,
|
type,
|
||||||
`(${typeof type})`
|
`(${typeof type})`
|
||||||
@ -270,7 +287,7 @@ export function renderVNodeChildren(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderElement(
|
function renderElementVNode(
|
||||||
push: PushFn,
|
push: PushFn,
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
parentComponent: ComponentInternalInstance
|
parentComponent: ComponentInternalInstance
|
||||||
@ -325,17 +342,17 @@ function renderElement(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPortal(
|
function renderPortalVNode(
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
parentComponent: ComponentInternalInstance
|
parentComponent: ComponentInternalInstance
|
||||||
) {
|
) {
|
||||||
const target = vnode.props && vnode.props.target
|
const target = vnode.props && vnode.props.target
|
||||||
if (!target) {
|
if (!target) {
|
||||||
console.warn(`[@vue/server-renderer] Portal is missing target prop.`)
|
console.error(`[@vue/server-renderer] Portal is missing target prop.`)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
if (!isString(target)) {
|
if (!isString(target)) {
|
||||||
console.warn(
|
console.error(
|
||||||
`[@vue/server-renderer] Portal target must be a query selector string.`
|
`[@vue/server-renderer] Portal target must be a query selector string.`
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
@ -367,7 +384,7 @@ async function resolvePortals(context: SSRContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderSuspense(
|
async function renderSuspenseVNode(
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
parentComponent: ComponentInternalInstance
|
parentComponent: ComponentInternalInstance
|
||||||
): Promise<ResolvedSSRBuffer> {
|
): Promise<ResolvedSSRBuffer> {
|
||||||
@ -375,10 +392,15 @@ async function renderSuspense(
|
|||||||
try {
|
try {
|
||||||
const { push, getBuffer } = createBuffer()
|
const { push, getBuffer } = createBuffer()
|
||||||
renderVNode(push, content, parentComponent)
|
renderVNode(push, content, parentComponent)
|
||||||
|
// await here so error can be caught
|
||||||
return await getBuffer()
|
return await getBuffer()
|
||||||
} catch {
|
} catch (e) {
|
||||||
const { push, getBuffer } = createBuffer()
|
if (e[AsyncSetupErrorMarker]) {
|
||||||
renderVNode(push, fallback, parentComponent)
|
const { push, getBuffer } = createBuffer()
|
||||||
return getBuffer()
|
renderVNode(push, fallback, parentComponent)
|
||||||
|
return getBuffer()
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user