feat(ssr): compiler-ssr support for Suspense

This commit is contained in:
Evan You
2020-03-10 16:52:08 -04:00
parent 47ead3b33a
commit 80c625dce3
10 changed files with 404 additions and 166 deletions

View File

@@ -30,17 +30,20 @@ import {
traverseNode,
ExpressionNode,
TemplateNode,
findProp,
JSChildNode
SUSPENSE
} from '@vue/compiler-dom'
import { SSR_RENDER_COMPONENT, SSR_RENDER_PORTAL } from '../runtimeHelpers'
import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
import {
SSRTransformContext,
processChildren,
processChildrenAsStatement
} from '../ssrCodegenTransform'
import { ssrProcessPortal } from './ssrTransformPortal'
import {
ssrProcessSuspense,
ssrTransformSuspense
} from './ssrTransformSuspense'
import { isSymbol, isObject, isArray } from '@vue/shared'
import { createSSRCompilerError, SSRErrorCodes } from '../errors'
// We need to construct the slot functions in the 1st pass to ensure proper
// scope tracking, but the children of each slot cannot be processed until
@@ -56,6 +59,12 @@ interface WIPSlotEntry {
const componentTypeMap = new WeakMap<ComponentNode, symbol>()
// ssr component transform is done in two phases:
// In phase 1. we use `buildSlot` to analyze the children of the component into
// WIP slot functions (it must be done in phase 1 because `buildSlot` relies on
// the core transform context).
// In phase 2. we convert the WIP slots from phase 1 into ssr-specific codegen
// nodes.
export const ssrTransformComponent: NodeTransform = (node, context) => {
if (
node.type !== NodeTypes.ELEMENT ||
@@ -67,6 +76,9 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
const component = resolveComponentType(node, context, true /* ssr */)
if (isSymbol(component)) {
componentTypeMap.set(node, component)
if (component === SUSPENSE) {
return ssrTransformSuspense(node, context)
}
return // built-in component: fallthrough
}
@@ -132,12 +144,15 @@ export function ssrProcessComponent(
) {
if (!node.ssrCodegenNode) {
// this is a built-in component that fell-through.
// just render its children.
const component = componentTypeMap.get(node)!
if (component === PORTAL) {
return ssrProcessPortal(node, context)
} else if (component === SUSPENSE) {
return ssrProcessSuspense(node, context)
} else {
// real fall-through (e.g. KeepAlive): just render its children.
processChildren(node.children, context)
}
processChildren(node.children, context)
} else {
// finish up slot function expressions from the 1st pass.
const wipEntries = wipMap.get(node) || []
@@ -161,47 +176,6 @@ export function ssrProcessComponent(
}
}
function ssrProcessPortal(node: ComponentNode, context: SSRTransformContext) {
const targetProp = findProp(node, 'target')
if (!targetProp) {
context.onError(
createSSRCompilerError(SSRErrorCodes.X_SSR_NO_PORTAL_TARGET, node.loc)
)
return
}
let target: JSChildNode
if (targetProp.type === NodeTypes.ATTRIBUTE && targetProp.value) {
target = createSimpleExpression(targetProp.value.content, true)
} else if (targetProp.type === NodeTypes.DIRECTIVE && targetProp.exp) {
target = targetProp.exp
} else {
context.onError(
createSSRCompilerError(
SSRErrorCodes.X_SSR_NO_PORTAL_TARGET,
targetProp.loc
)
)
return
}
const contentRenderFn = createFunctionExpression(
[`_push`],
undefined, // Body is added later
true, // newline
false, // isSlot
node.loc
)
contentRenderFn.body = processChildrenAsStatement(node.children, context)
context.pushStatement(
createCallExpression(context.helper(SSR_RENDER_PORTAL), [
contentRenderFn,
target,
`_parent`
])
)
}
export const rawOptionsMap = new WeakMap<RootNode, CompilerOptions>()
const [baseNodeTransforms, baseDirectiveTransforms] = getBaseTransformPreset(

View File

@@ -0,0 +1,60 @@
import {
ComponentNode,
findProp,
JSChildNode,
NodeTypes,
createSimpleExpression,
createFunctionExpression,
createCallExpression
} from '@vue/compiler-dom'
import {
SSRTransformContext,
processChildrenAsStatement
} from '../ssrCodegenTransform'
import { createSSRCompilerError, SSRErrorCodes } from '../errors'
import { SSR_RENDER_PORTAL } from '../runtimeHelpers'
// Note: this is a 2nd-pass codegen transform.
export function ssrProcessPortal(
node: ComponentNode,
context: SSRTransformContext
) {
const targetProp = findProp(node, 'target')
if (!targetProp) {
context.onError(
createSSRCompilerError(SSRErrorCodes.X_SSR_NO_PORTAL_TARGET, node.loc)
)
return
}
let target: JSChildNode
if (targetProp.type === NodeTypes.ATTRIBUTE && targetProp.value) {
target = createSimpleExpression(targetProp.value.content, true)
} else if (targetProp.type === NodeTypes.DIRECTIVE && targetProp.exp) {
target = targetProp.exp
} else {
context.onError(
createSSRCompilerError(
SSRErrorCodes.X_SSR_NO_PORTAL_TARGET,
targetProp.loc
)
)
return
}
const contentRenderFn = createFunctionExpression(
[`_push`],
undefined, // Body is added later
true, // newline
false, // isSlot
node.loc
)
contentRenderFn.body = processChildrenAsStatement(node.children, context)
context.pushStatement(
createCallExpression(context.helper(SSR_RENDER_PORTAL), [
contentRenderFn,
target,
`_parent`
])
)
}

View File

@@ -0,0 +1,78 @@
import {
ComponentNode,
TransformContext,
buildSlots,
createFunctionExpression,
FunctionExpression,
TemplateChildNode,
createCallExpression,
SlotsExpression
} from '@vue/compiler-dom'
import {
SSRTransformContext,
processChildrenAsStatement
} from '../ssrCodegenTransform'
import { SSR_RENDER_SUSPENSE } from '../runtimeHelpers'
const wipMap = new WeakMap<ComponentNode, WIPEntry>()
interface WIPEntry {
slotsExp: SlotsExpression
wipSlots: Array<{
fn: FunctionExpression
children: TemplateChildNode[]
}>
}
// phase 1
export function ssrTransformSuspense(
node: ComponentNode,
context: TransformContext
) {
return () => {
if (node.children.length) {
const wipEntry: WIPEntry = {
slotsExp: null as any,
wipSlots: []
}
wipMap.set(node, wipEntry)
wipEntry.slotsExp = buildSlots(node, context, (_props, children, loc) => {
const fn = createFunctionExpression(
[`_push`],
undefined, // no return, assign body later
true, // newline
false, // suspense slots are not treated as normal slots
loc
)
wipEntry.wipSlots.push({
fn,
children
})
return fn
}).slots
}
}
}
// phase 2
export function ssrProcessSuspense(
node: ComponentNode,
context: SSRTransformContext
) {
// complete wip slots with ssr code
const wipEntry = wipMap.get(node)
if (!wipEntry) {
return
}
const { slotsExp, wipSlots } = wipEntry
for (let i = 0; i < wipSlots.length; i++) {
const { fn, children } = wipSlots[i]
fn.body = processChildrenAsStatement(children, context)
}
// _push(ssrRenderSuspense(slots))
context.pushStatement(
createCallExpression(`_push`, [
createCallExpression(context.helper(SSR_RENDER_SUSPENSE), [slotsExp])
])
)
}