Skip to content

Commit

Permalink
feat(ssr): compiler-ssr support for Suspense
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Mar 10, 2020
1 parent 47ead3b commit 80c625d
Show file tree
Hide file tree
Showing 10 changed files with 396 additions and 158 deletions.
27 changes: 1 addition & 26 deletions packages/compiler-ssr/__tests__/ssrComponent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('ssr: components', () => {
.toMatchInlineSnapshot(`
"const { resolveDynamicComponent: _resolveDynamicComponent } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
_push(_ssrRenderComponent(_resolveDynamicComponent(_ctx.foo, _ctx.$), { prop: \\"b\\" }, null, _parent))
}"
Expand Down Expand Up @@ -269,7 +269,6 @@ describe('ssr: components', () => {
})

test('built-in fallthroughs', () => {
// no fragment
expect(compile(`<transition><div/></transition>`).code)
.toMatchInlineSnapshot(`
"
Expand All @@ -278,7 +277,6 @@ describe('ssr: components', () => {
}"
`)

// wrap with fragment
expect(compile(`<transition-group><div/></transition-group>`).code)
.toMatchInlineSnapshot(`
"
Expand All @@ -287,7 +285,6 @@ describe('ssr: components', () => {
}"
`)

// no fragment
expect(compile(`<keep-alive><foo/></keep-alive>`).code)
.toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
Expand All @@ -299,28 +296,6 @@ describe('ssr: components', () => {
_push(_ssrRenderComponent(_component_foo, null, null, _parent))
}"
`)

// wrap with fragment
expect(compile(`<suspense><div/></suspense>`).code)
.toMatchInlineSnapshot(`
"
return function ssrRender(_ctx, _push, _parent) {
_push(\`<div></div>\`)
}"
`)
})

test('portal rendering', () => {
expect(compile(`<portal :target="target"><div/></portal>`).code)
.toMatchInlineSnapshot(`
"const { ssrRenderPortal: _ssrRenderPortal } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
_ssrRenderPortal((_push) => {
_push(\`<div></div>\`)
}, _ctx.target, _parent)
}"
`)
})
})
})
16 changes: 16 additions & 0 deletions packages/compiler-ssr/__tests__/ssrPortal.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { compile } from '../src'

describe('ssr compile: portal', () => {
test('should work', () => {
expect(compile(`<portal :target="target"><div/></portal>`).code)
.toMatchInlineSnapshot(`
"const { ssrRenderPortal: _ssrRenderPortal } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
_ssrRenderPortal((_push) => {
_push(\`<div></div>\`)
}, _ctx.target, _parent)
}"
`)
})
})
51 changes: 51 additions & 0 deletions packages/compiler-ssr/__tests__/ssrSuspense.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { compile } from '../src'

describe('ssr compile: suspense', () => {
test('implicit default', () => {
expect(compile(`<suspense><foo/></suspense>`).code).toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent, ssrRenderSuspense: _ssrRenderSuspense } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
const _component_foo = _resolveComponent(\\"foo\\")
_push(_ssrRenderSuspense({
default: (_push) => {
_push(_ssrRenderComponent(_component_foo, null, null, _parent))
},
_: 1
}))
}"
`)
})

test('explicit slots', () => {
expect(
compile(`<suspense>
<template #default>
<foo/>
</template>
<template #fallback>
loading...
</template>
</suspense>`).code
).toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent, ssrRenderSuspense: _ssrRenderSuspense } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
const _component_foo = _resolveComponent(\\"foo\\")
_push(_ssrRenderSuspense({
default: (_push) => {
_push(_ssrRenderComponent(_component_foo, null, null, _parent))
},
fallback: (_push) => {
_push(\` loading... \`)
},
_: 1
}))
}"
`)
})
})
4 changes: 3 additions & 1 deletion packages/compiler-ssr/src/runtimeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const SSR_LOOSE_CONTAIN = Symbol(`ssrLooseContain`)
export const SSR_RENDER_DYNAMIC_MODEL = Symbol(`ssrRenderDynamicModel`)
export const SSR_GET_DYNAMIC_MODEL_PROPS = Symbol(`ssrGetDynamicModelProps`)
export const SSR_RENDER_PORTAL = Symbol(`ssrRenderPortal`)
export const SSR_RENDER_SUSPENSE = Symbol(`ssrRenderSuspense`)

export const ssrHelpers = {
[SSR_INTERPOLATE]: `ssrInterpolate`,
Expand All @@ -29,7 +30,8 @@ export const ssrHelpers = {
[SSR_LOOSE_CONTAIN]: `ssrLooseContain`,
[SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`,
[SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps`,
[SSR_RENDER_PORTAL]: `ssrRenderPortal`
[SSR_RENDER_PORTAL]: `ssrRenderPortal`,
[SSR_RENDER_SUSPENSE]: `ssrRenderSuspense`
}

// Note: these are helpers imported from @vue/server-renderer
Expand Down
68 changes: 21 additions & 47 deletions packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ||
Expand All @@ -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
}

Expand Down Expand Up @@ -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) || []
Expand All @@ -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(
Expand Down
60 changes: 60 additions & 0 deletions packages/compiler-ssr/src/transforms/ssrTransformPortal.ts
Original file line number Diff line number Diff line change
@@ -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`
])
)
}
Loading

0 comments on commit 80c625d

Please sign in to comment.