]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(ssr): support dynamic components that resolve to element or vnode
authorEvan You <yyx990803@gmail.com>
Tue, 7 Jul 2020 01:23:29 +0000 (21:23 -0400)
committerEvan You <yyx990803@gmail.com>
Tue, 7 Jul 2020 01:23:35 +0000 (21:23 -0400)
fix #1508

packages/compiler-ssr/__tests__/ssrComponent.spec.ts
packages/compiler-ssr/src/runtimeHelpers.ts
packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts [new file with mode: 0644]
packages/server-renderer/src/index.ts
packages/server-renderer/src/render.ts

index 4076749321a64cdbf5fb6f8d0a27ff08129b7713..860462a4004f1bdb397c10e96ce9fd8d494a2df6 100644 (file)
@@ -20,21 +20,21 @@ describe('ssr: components', () => {
   test('dynamic component', () => {
     expect(compile(`<component is="foo" prop="b" />`).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(`<component :is="foo" prop="b" />`).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)
       }"
     `)
   })
index c2732d19e43884ac00d68962520cea7410615599..2aa93c0bf1589ba7d5c0b582a69b23573be7e12f 100644 (file)
@@ -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`,
index 31ee943c47263f7e4c17f215ea9407f1eb438817..4b3b10ff96486101b25210f8b5afcdbd9da749e3 100644 (file)
@@ -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<ComponentNode, symbol>()
+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 (file)
index 0000000..dff16c7
--- /dev/null
@@ -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: `<div><slot/></div>`
+            }
+          },
+          template: `<component :is="'one'"><span>slot</span></component>`
+        })
+      )
+    ).toBe(`<div><!--[--><span>slot</span><!--]--></div>`)
+  })
+
+  test('resolve to element', async () => {
+    expect(
+      await renderToString(
+        createApp({
+          template: `<component :is="'p'"><span>slot</span></component>`
+        })
+      )
+    ).toBe(`<p><span>slot</span></p>`)
+  })
+
+  test('resolve to component vnode', async () => {
+    const Child = {
+      props: ['id'],
+      template: `<div>{{ id }}<slot/></div>`
+    }
+    expect(
+      await renderToString(
+        createApp({
+          setup() {
+            return {
+              vnode: createVNode(Child, { id: 'test' })
+            }
+          },
+          template: `<component :is="vnode"><span>slot</span></component>`
+        })
+      )
+    ).toBe(`<div>test<!--[--><span>slot</span><!--]--></div>`)
+  })
+
+  test('resolve to element vnode', async () => {
+    expect(
+      await renderToString(
+        createApp({
+          setup() {
+            return {
+              vnode: createVNode('div', { id: 'test' })
+            }
+          },
+          template: `<component :is="vnode"><span>slot</span></component>`
+        })
+      )
+    ).toBe(`<div id="test"><span>slot</span></div>`)
+  })
+})
index 723f964ed1d67f369c5aebc9ea5485d877bd9b64..9c5066e851eca2bd60edbb6e481ee082e2a5f01f 100644 (file)
@@ -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'
index 55af4ff09bdc04bd9bdcb54026d7e5a80541b7bd..059c388f793a7b5ea88f86416adfcc89e27362ea 100644 (file)
@@ -142,7 +142,7 @@ function renderComponentSubTree(
   return getBuffer()
 }
 
-function renderVNode(
+export function renderVNode(
   push: PushFn,
   vnode: VNode,
   parentComponent: ComponentInternalInstance