From 41db49dfb7c520c4f743e522a03f06b33259a2eb Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 6 Jul 2020 21:23:29 -0400 Subject: [PATCH] fix(ssr): support dynamic components that resolve to element or vnode fix #1508 --- .../__tests__/ssrComponent.spec.ts | 12 ++-- packages/compiler-ssr/src/runtimeHelpers.ts | 2 + .../src/transforms/ssrTransformComponent.ts | 55 ++++++++++++---- .../__tests__/ssrDynamicComponent.spec.ts | 63 +++++++++++++++++++ packages/server-renderer/src/index.ts | 1 + packages/server-renderer/src/render.ts | 2 +- 6 files changed, 117 insertions(+), 18 deletions(-) create mode 100644 packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts index 407674932..860462a40 100644 --- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts @@ -20,21 +20,21 @@ describe('ssr: components', () => { test('dynamic component', () => { expect(compile(``).code) .toMatchInlineSnapshot(` - "const { resolveDynamicComponent: _resolveDynamicComponent, mergeProps: _mergeProps } = require(\\"vue\\") - const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\") + "const { resolveDynamicComponent: _resolveDynamicComponent, mergeProps: _mergeProps, createVNode: _createVNode } = require(\\"vue\\") + const { ssrRenderVNode: _ssrRenderVNode } = require(\\"@vue/server-renderer\\") return function ssrRender(_ctx, _push, _parent, _attrs) { - _push(_ssrRenderComponent(_resolveDynamicComponent(\\"foo\\"), _mergeProps({ prop: \\"b\\" }, _attrs), null, _parent)) + _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent(\\"foo\\"), _mergeProps({ prop: \\"b\\" }, _attrs), null), _parent) }" `) expect(compile(``).code) .toMatchInlineSnapshot(` - "const { resolveDynamicComponent: _resolveDynamicComponent, mergeProps: _mergeProps } = require(\\"vue\\") - const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\") + "const { resolveDynamicComponent: _resolveDynamicComponent, mergeProps: _mergeProps, createVNode: _createVNode } = require(\\"vue\\") + const { ssrRenderVNode: _ssrRenderVNode } = require(\\"@vue/server-renderer\\") return function ssrRender(_ctx, _push, _parent, _attrs) { - _push(_ssrRenderComponent(_resolveDynamicComponent(_ctx.foo), _mergeProps({ prop: \\"b\\" }, _attrs), null, _parent)) + _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent(_ctx.foo), _mergeProps({ prop: \\"b\\" }, _attrs), null), _parent) }" `) }) diff --git a/packages/compiler-ssr/src/runtimeHelpers.ts b/packages/compiler-ssr/src/runtimeHelpers.ts index c2732d19e..2aa93c0bf 100644 --- a/packages/compiler-ssr/src/runtimeHelpers.ts +++ b/packages/compiler-ssr/src/runtimeHelpers.ts @@ -1,6 +1,7 @@ import { registerRuntimeHelpers } from '@vue/compiler-dom' export const SSR_INTERPOLATE = Symbol(`ssrInterpolate`) +export const SSR_RENDER_VNODE = Symbol(`ssrRenderVNode`) export const SSR_RENDER_COMPONENT = Symbol(`ssrRenderComponent`) export const SSR_RENDER_SLOT = Symbol(`ssrRenderSlot`) export const SSR_RENDER_CLASS = Symbol(`ssrRenderClass`) @@ -18,6 +19,7 @@ export const SSR_RENDER_SUSPENSE = Symbol(`ssrRenderSuspense`) export const ssrHelpers = { [SSR_INTERPOLATE]: `ssrInterpolate`, + [SSR_RENDER_VNODE]: `ssrRenderVNode`, [SSR_RENDER_COMPONENT]: `ssrRenderComponent`, [SSR_RENDER_SLOT]: `ssrRenderSlot`, [SSR_RENDER_CLASS]: `ssrRenderClass`, diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts index 31ee943c4..4b3b10ff9 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts @@ -11,7 +11,6 @@ import { buildSlots, FunctionExpression, TemplateChildNode, - TELEPORT, createIfStatement, createSimpleExpression, getBaseTransformPreset, @@ -31,9 +30,12 @@ import { ExpressionNode, TemplateNode, SUSPENSE, - TRANSITION_GROUP + TELEPORT, + TRANSITION_GROUP, + CREATE_VNODE, + CallExpression } from '@vue/compiler-dom' -import { SSR_RENDER_COMPONENT } from '../runtimeHelpers' +import { SSR_RENDER_COMPONENT, SSR_RENDER_VNODE } from '../runtimeHelpers' import { SSRTransformContext, processChildren, @@ -58,7 +60,10 @@ interface WIPSlotEntry { vnodeBranch: ReturnStatement } -const componentTypeMap = new WeakMap() +const componentTypeMap = new WeakMap< + ComponentNode, + string | symbol | CallExpression +>() // ssr component transform is done in two phases: // In phase 1. we use `buildSlot` to analyze the children of the component into @@ -75,8 +80,9 @@ export const ssrTransformComponent: NodeTransform = (node, context) => { } const component = resolveComponentType(node, context, true /* ssr */) + componentTypeMap.set(node, component) + if (isSymbol(component)) { - componentTypeMap.set(node, component) if (component === SUSPENSE) { return ssrTransformSuspense(node, context) } @@ -134,10 +140,28 @@ export const ssrTransformComponent: NodeTransform = (node, context) => { ? buildSlots(node, context, buildSSRSlotFn).slots : `null` - node.ssrCodegenNode = createCallExpression( - context.helper(SSR_RENDER_COMPONENT), - [component, props, slots, `_parent`] - ) + if (typeof component !== 'string') { + // dynamic component that resolved to a `resolveDynamicComponent` call + // expression - since the reoslved result may be a plain element (string) + // or a VNode, handle it with `renderVNode`. + node.ssrCodegenNode = createCallExpression( + context.helper(SSR_RENDER_VNODE), + [ + `_push`, + createCallExpression(context.helper(CREATE_VNODE), [ + component, + props, + slots + ]), + `_parent` + ] + ) + } else { + node.ssrCodegenNode = createCallExpression( + context.helper(SSR_RENDER_COMPONENT), + [component, props, slots, `_parent`] + ) + } } } @@ -145,9 +169,9 @@ export function ssrProcessComponent( node: ComponentNode, context: SSRTransformContext ) { + const component = componentTypeMap.get(node)! if (!node.ssrCodegenNode) { // this is a built-in component that fell-through. - const component = componentTypeMap.get(node)! if (component === TELEPORT) { return ssrProcessTeleport(node, context) } else if (component === SUSPENSE) { @@ -176,7 +200,16 @@ export function ssrProcessComponent( vnodeBranch ) } - context.pushStatement(createCallExpression(`_push`, [node.ssrCodegenNode])) + if (typeof component === 'string') { + // static component + context.pushStatement( + createCallExpression(`_push`, [node.ssrCodegenNode]) + ) + } else { + // dynamic component (`resolveDynamicComponent` call) + // the codegen node is a `renderVNode` call + context.pushStatement(node.ssrCodegenNode) + } } } diff --git a/packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts b/packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts new file mode 100644 index 000000000..dff16c781 --- /dev/null +++ b/packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts @@ -0,0 +1,63 @@ +import { createApp, createVNode } from 'vue' +import { renderToString } from '../src/renderToString' + +describe('ssr: dynamic component', () => { + test('resolved to component', async () => { + expect( + await renderToString( + createApp({ + components: { + one: { + template: `
` + } + }, + template: `slot` + }) + ) + ).toBe(`
slot
`) + }) + + test('resolve to element', async () => { + expect( + await renderToString( + createApp({ + template: `slot` + }) + ) + ).toBe(`

slot

`) + }) + + test('resolve to component vnode', async () => { + const Child = { + props: ['id'], + template: `
{{ id }}
` + } + expect( + await renderToString( + createApp({ + setup() { + return { + vnode: createVNode(Child, { id: 'test' }) + } + }, + template: `slot` + }) + ) + ).toBe(`
testslot
`) + }) + + test('resolve to element vnode', async () => { + expect( + await renderToString( + createApp({ + setup() { + return { + vnode: createVNode('div', { id: 'test' }) + } + }, + template: `slot` + }) + ) + ).toBe(`
slot
`) + }) +}) diff --git a/packages/server-renderer/src/index.ts b/packages/server-renderer/src/index.ts index 723f964ed..9c5066e85 100644 --- a/packages/server-renderer/src/index.ts +++ b/packages/server-renderer/src/index.ts @@ -4,6 +4,7 @@ export { renderToString } from './renderToString' export { renderToStream } from './renderToStream' // internal runtime helpers +export { renderVNode as ssrRenderVNode } from './render' export { ssrRenderComponent } from './helpers/ssrRenderComponent' export { ssrRenderSlot } from './helpers/ssrRenderSlot' export { ssrRenderTeleport } from './helpers/ssrRenderTeleport' diff --git a/packages/server-renderer/src/render.ts b/packages/server-renderer/src/render.ts index 55af4ff09..059c388f7 100644 --- a/packages/server-renderer/src/render.ts +++ b/packages/server-renderer/src/render.ts @@ -142,7 +142,7 @@ function renderComponentSubTree( return getBuffer() } -function renderVNode( +export function renderVNode( push: PushFn, vnode: VNode, parentComponent: ComponentInternalInstance