]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(srr): slot outlet
authorEvan You <yyx990803@gmail.com>
Thu, 6 Feb 2020 02:04:40 +0000 (21:04 -0500)
committerEvan You <yyx990803@gmail.com>
Thu, 6 Feb 2020 02:04:40 +0000 (21:04 -0500)
14 files changed:
packages/compiler-core/src/ast.ts
packages/compiler-core/src/index.ts
packages/compiler-core/src/transforms/transformSlotOutlet.ts
packages/compiler-core/src/transforms/vFor.ts
packages/compiler-core/src/transforms/vIf.ts
packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts [new file with mode: 0644]
packages/compiler-ssr/src/ssrCodegenTransform.ts
packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts
packages/compiler-ssr/src/transforms/ssrVFor.ts
packages/compiler-ssr/src/transforms/ssrVIf.ts
packages/server-renderer/__tests__/renderToString.spec.ts
packages/server-renderer/src/helpers/renderSlot.ts [new file with mode: 0644]
packages/server-renderer/src/index.ts
packages/server-renderer/src/renderToString.ts

index cd605832e1f5a4d7a996abea97fe49dd923f19de..dca1989a5f3b7613bcc11729e82aa0fe3de0056a 100644 (file)
@@ -147,11 +147,13 @@ export interface ComponentNode extends BaseElementNode {
     | ComponentCodegenNode
     | CacheExpression // when cached by v-once
     | undefined
+  ssrCodegenNode?: CallExpression
 }
 
 export interface SlotOutletNode extends BaseElementNode {
   tagType: ElementTypes.SLOT
   codegenNode: SlotOutletCodegenNode | undefined | CacheExpression // when cached by v-once
+  ssrCodegenNode?: CallExpression
 }
 
 export interface TemplateNode extends BaseElementNode {
index e807b0e50d555a56484389715205aa3ecbc1cd75..65f3884f4e3fe1c1492eba7b3995863dc316b6a7 100644 (file)
@@ -35,14 +35,15 @@ export { transformBind } from './transforms/vBind'
 
 // exported for compiler-ssr
 export { MERGE_PROPS } from './runtimeHelpers'
-export { processIfBranches } from './transforms/vIf'
-export { processForNode, createForLoopParams } from './transforms/vFor'
+export { processIf } from './transforms/vIf'
+export { processFor, createForLoopParams } from './transforms/vFor'
 export {
   transformExpression,
   processExpression
 } from './transforms/transformExpression'
 export { trackVForSlotScopes, trackSlotScopes } from './transforms/vSlot'
 export { buildProps } from './transforms/transformElement'
+export { processSlotOutlet } from './transforms/transformSlotOutlet'
 
 // utility, but need to rewrite typing to avoid dts relying on @vue/shared
 import { generateCodeFrame as _genCodeFrame } from '@vue/shared'
index f8862cbbfc3af3d50f44f4b5600724abf498e17f..da8ab81321007d9f778989be89c97245f224ab61 100644 (file)
@@ -1,78 +1,32 @@
-import { NodeTransform } from '../transform'
+import { NodeTransform, TransformContext } from '../transform'
 import {
   NodeTypes,
   CallExpression,
   createCallExpression,
-  ExpressionNode
+  ExpressionNode,
+  SlotOutletNode
 } from '../ast'
-import { isSlotOutlet } from '../utils'
-import { buildProps } from './transformElement'
+import { isSlotOutlet, findProp } from '../utils'
+import { buildProps, PropsExpression } from './transformElement'
 import { createCompilerError, ErrorCodes } from '../errors'
 import { RENDER_SLOT } from '../runtimeHelpers'
 
 export const transformSlotOutlet: NodeTransform = (node, context) => {
   if (isSlotOutlet(node)) {
-    const { props, children, loc } = node
-    const $slots = context.prefixIdentifiers ? `_ctx.$slots` : `$slots`
-    let slotName: string | ExpressionNode = `"default"`
+    const { children, loc } = node
+    const { slotName, slotProps } = processSlotOutlet(node, context)
 
-    // check for <slot name="xxx" OR :name="xxx" />
-    let nameIndex: number = -1
-    for (let i = 0; i < props.length; i++) {
-      const prop = props[i]
-      if (prop.type === NodeTypes.ATTRIBUTE) {
-        if (prop.name === `name` && prop.value) {
-          // static name="xxx"
-          slotName = JSON.stringify(prop.value.content)
-          nameIndex = i
-          break
-        }
-      } else if (prop.name === `bind`) {
-        const { arg, exp } = prop
-        if (
-          arg &&
-          exp &&
-          arg.type === NodeTypes.SIMPLE_EXPRESSION &&
-          arg.isStatic &&
-          arg.content === `name`
-        ) {
-          // dynamic :name="xxx"
-          slotName = exp
-          nameIndex = i
-          break
-        }
-      }
-    }
+    const slotArgs: CallExpression['arguments'] = [
+      context.prefixIdentifiers ? `_ctx.$slots` : `$slots`,
+      slotName
+    ]
 
-    const slotArgs: CallExpression['arguments'] = [$slots, slotName]
-    const propsWithoutName =
-      nameIndex > -1
-        ? props.slice(0, nameIndex).concat(props.slice(nameIndex + 1))
-        : props
-    let hasProps = propsWithoutName.length > 0
-    if (hasProps) {
-      const { props: propsExpression, directives } = buildProps(
-        node,
-        context,
-        propsWithoutName
-      )
-      if (directives.length) {
-        context.onError(
-          createCompilerError(
-            ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
-            directives[0].loc
-          )
-        )
-      }
-      if (propsExpression) {
-        slotArgs.push(propsExpression)
-      } else {
-        hasProps = false
-      }
+    if (slotProps) {
+      slotArgs.push(slotProps)
     }
 
     if (children.length) {
-      if (!hasProps) {
+      if (!slotProps) {
         slotArgs.push(`{}`)
       }
       slotArgs.push(children)
@@ -85,3 +39,49 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
     )
   }
 }
+
+interface SlotOutletProcessResult {
+  slotName: string | ExpressionNode
+  slotProps: PropsExpression | undefined
+}
+
+export function processSlotOutlet(
+  node: SlotOutletNode,
+  context: TransformContext
+): SlotOutletProcessResult {
+  let slotName: string | ExpressionNode = `"default"`
+  let slotProps: PropsExpression | undefined = undefined
+
+  // check for <slot name="xxx" OR :name="xxx" />
+  const name = findProp(node, 'name')
+  if (name) {
+    if (name.type === NodeTypes.ATTRIBUTE && name.value) {
+      // static name
+      slotName = JSON.stringify(name.value.content)
+    } else if (name.type === NodeTypes.DIRECTIVE && name.exp) {
+      // dynamic name
+      slotName = name.exp
+    }
+  }
+
+  const propsWithoutName = name
+    ? node.props.filter(p => p !== name)
+    : node.props
+  if (propsWithoutName.length > 0) {
+    const { props, directives } = buildProps(node, context, propsWithoutName)
+    slotProps = props
+    if (directives.length) {
+      context.onError(
+        createCompilerError(
+          ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
+          directives[0].loc
+        )
+      )
+    }
+  }
+
+  return {
+    slotName,
+    slotProps
+  }
+}
index d8a8fe1b0040d908de3af8a8ea4b70e20ccd7e32..1032c0a564372c81fe9030e78a721932162c6715 100644 (file)
@@ -46,7 +46,7 @@ export const transformFor = createStructuralDirectiveTransform(
   'for',
   (node, dir, context) => {
     const { helper } = context
-    return processForNode(node, dir, context, forNode => {
+    return processFor(node, dir, context, forNode => {
       // create the loop render function expression now, and add the
       // iterator on exit after all children have been traversed
       const renderExp = createCallExpression(helper(RENDER_LIST), [
@@ -138,7 +138,7 @@ export const transformFor = createStructuralDirectiveTransform(
 )
 
 // target-agnostic transform used for both Client and SSR
-export function processForNode(
+export function processFor(
   node: ElementNode,
   dir: DirectiveNode,
   context: TransformContext,
index 69cfb3f1ba86a8d154029b44419f35d42df8ac0f..5ddf8b176ea3185799871ae2a5057015aa5e26bc 100644 (file)
@@ -41,7 +41,7 @@ import { injectProp } from '../utils'
 export const transformIf = createStructuralDirectiveTransform(
   /^(if|else|else-if)$/,
   (node, dir, context) => {
-    return processIfBranches(node, dir, context, (ifNode, branch, isRoot) => {
+    return processIf(node, dir, context, (ifNode, branch, isRoot) => {
       // Exit callback. Complete the codegenNode when all children have been
       // transformed.
       return () => {
@@ -72,7 +72,7 @@ export const transformIf = createStructuralDirectiveTransform(
 )
 
 // target-agnostic transform used for both Client and SSR
-export function processIfBranches(
+export function processIf(
   node: ElementNode,
   dir: DirectiveNode,
   context: TransformContext,
diff --git a/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts b/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts
new file mode 100644 (file)
index 0000000..1069fa3
--- /dev/null
@@ -0,0 +1,60 @@
+import { compile } from '../src'
+
+describe('ssr: <slot>', () => {
+  test('basic', () => {
+    expect(compile(`<slot/>`).code).toMatchInlineSnapshot(`
+      "const { _renderSlot } = require(\\"@vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent) {
+        _renderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent)
+      }"
+    `)
+  })
+
+  test('with name', () => {
+    expect(compile(`<slot name="foo" />`).code).toMatchInlineSnapshot(`
+      "const { _renderSlot } = require(\\"@vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent) {
+        _renderSlot(_ctx.$slots, \\"foo\\", {}, null, _push, _parent)
+      }"
+    `)
+  })
+
+  test('with dynamic name', () => {
+    expect(compile(`<slot :name="bar.baz" />`).code).toMatchInlineSnapshot(`
+      "const { _renderSlot } = require(\\"@vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent) {
+        _renderSlot(_ctx.$slots, _ctx.bar.baz, {}, null, _push, _parent)
+      }"
+    `)
+  })
+
+  test('with props', () => {
+    expect(compile(`<slot name="foo" :p="1" bar="2" />`).code)
+      .toMatchInlineSnapshot(`
+      "const { _renderSlot } = require(\\"@vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent) {
+        _renderSlot(_ctx.$slots, \\"foo\\", {
+          p: 1,
+          bar: \\"2\\"
+        }, null, _push, _parent)
+      }"
+    `)
+  })
+
+  test('with fallback', () => {
+    expect(compile(`<slot>some {{ fallback }} content</slot>`).code)
+      .toMatchInlineSnapshot(`
+      "const { _renderSlot, _interpolate } = require(\\"@vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent) {
+        _renderSlot(_ctx.$slots, \\"default\\", {}, () => {
+          _push(\`some \${_interpolate(_ctx.fallback)} content\`)
+        }, _push, _parent)
+      }"
+    `)
+  })
+})
index 8a0758aed0947e9bc85bbb70a2c3b151025b6009..c0b960e628eb290353ef015c6011a34ec83f17aa 100644 (file)
@@ -15,8 +15,9 @@ import {
 } from '@vue/compiler-dom'
 import { isString, escapeHtml, NO } from '@vue/shared'
 import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
-import { processIf } from './transforms/ssrVIf'
-import { processFor } from './transforms/ssrVFor'
+import { ssrProcessIf } from './transforms/ssrVIf'
+import { ssrProcessFor } from './transforms/ssrVFor'
+import { ssrProcessSlotOutlet } from './transforms/ssrTransformSlotOutlet'
 
 // Because SSR codegen output is completely different from client-side output
 // (e.g. multiple elements can be concatenated into a single template literal
@@ -119,7 +120,7 @@ export function processChildren(
       } else if (child.tagType === ElementTypes.COMPONENT) {
         // TODO
       } else if (child.tagType === ElementTypes.SLOT) {
-        // TODO
+        ssrProcessSlotOutlet(child, context)
       }
     } else if (child.type === NodeTypes.TEXT) {
       context.pushStringPart(escapeHtml(child.content))
@@ -128,9 +129,9 @@ export function processChildren(
         createCallExpression(context.helper(SSR_INTERPOLATE), [child.content])
       )
     } else if (child.type === NodeTypes.IF) {
-      processIf(child, context)
+      ssrProcessIf(child, context)
     } else if (child.type === NodeTypes.FOR) {
-      processFor(child, context)
+      ssrProcessFor(child, context)
     }
   }
 }
index 4e734dbc127170a6e84390c443e5916be56609a6..bef9c661fc360debd09693b35e2cd0c809fc242c 100644 (file)
@@ -1,3 +1,49 @@
-import { NodeTransform } from '@vue/compiler-dom'
+import {
+  NodeTransform,
+  isSlotOutlet,
+  processSlotOutlet,
+  createCallExpression,
+  SlotOutletNode,
+  createFunctionExpression,
+  createBlockStatement
+} from '@vue/compiler-dom'
+import { SSR_RENDER_SLOT } from '../runtimeHelpers'
+import {
+  SSRTransformContext,
+  createChildContext,
+  processChildren
+} from '../ssrCodegenTransform'
 
-export const ssrTransformSlotOutlet: NodeTransform = () => {}
+export const ssrTransformSlotOutlet: NodeTransform = (node, context) => {
+  if (isSlotOutlet(node)) {
+    const { slotName, slotProps } = processSlotOutlet(node, context)
+    node.ssrCodegenNode = createCallExpression(
+      context.helper(SSR_RENDER_SLOT),
+      [
+        `_ctx.$slots`,
+        slotName,
+        slotProps || `{}`,
+        `null`, // fallback content placeholder.
+        `_push`,
+        `_parent`
+      ]
+    )
+  }
+}
+
+export function ssrProcessSlotOutlet(
+  node: SlotOutletNode,
+  context: SSRTransformContext
+) {
+  const renderCall = node.ssrCodegenNode!
+  // has fallback content
+  if (node.children.length) {
+    const childContext = createChildContext(context)
+    processChildren(node.children, childContext)
+    const fallbackRenderFn = createFunctionExpression([])
+    fallbackRenderFn.body = createBlockStatement(childContext.body)
+    // _renderSlot(slots, name, props, fallback, ...)
+    renderCall.arguments[3] = fallbackRenderFn
+  }
+  context.pushStatement(node.ssrCodegenNode!)
+}
index 06e687f31bd347aeaf68daff619d90eb6a4ccff0..effa8d8802af4f794e33e3ecffa9da84bf96a0eb 100644 (file)
@@ -1,7 +1,7 @@
 import {
   createStructuralDirectiveTransform,
   ForNode,
-  processForNode,
+  processFor,
   createCallExpression,
   createFunctionExpression,
   createForLoopParams,
@@ -18,12 +18,12 @@ import { SSR_RENDER_LIST } from '../runtimeHelpers'
 // Plugin for the first transform pass, which simply constructs the AST node
 export const ssrTransformFor = createStructuralDirectiveTransform(
   'for',
-  processForNode
+  processFor
 )
 
 // This is called during the 2nd transform pass to construct the SSR-sepcific
 // codegen nodes.
-export function processFor(node: ForNode, context: SSRTransformContext) {
+export function ssrProcessFor(node: ForNode, context: SSRTransformContext) {
   const childContext = createChildContext(context)
   const needFragmentWrapper =
     node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT
index 905dd5391fe941a6b73d33916aec7c993b4b1b2a..23e06db2a86d9afae960114ca3f68ffce23174d2 100644 (file)
@@ -1,6 +1,6 @@
 import {
   createStructuralDirectiveTransform,
-  processIfBranches,
+  processIf,
   IfNode,
   createIfStatement,
   createBlockStatement,
@@ -18,12 +18,12 @@ import {
 // Plugin for the first transform pass, which simply constructs the AST node
 export const ssrTransformIf = createStructuralDirectiveTransform(
   /^(if|else|else-if)$/,
-  processIfBranches
+  processIf
 )
 
 // This is called during the 2nd transform pass to construct the SSR-sepcific
 // codegen nodes.
-export function processIf(node: IfNode, context: SSRTransformContext) {
+export function ssrProcessIf(node: IfNode, context: SSRTransformContext) {
   const [rootBranch] = node.branches
   const ifStatement = createIfStatement(
     rootBranch.condition!,
index 36a7f2736479e066c7e15add160969b547f71056..97d02db6f75379132cabeeb84144ee32782e9b02 100644 (file)
@@ -7,11 +7,8 @@ import {
   ComponentOptions
 } from 'vue'
 import { escapeHtml } from '@vue/shared'
-import {
-  renderToString,
-  renderComponent,
-  renderSlot
-} from '../src/renderToString'
+import { renderToString, renderComponent } from '../src/renderToString'
+import { renderSlot } from '../src/helpers/renderSlot'
 
 describe('ssr: renderToString', () => {
   test('should apply app context', async () => {
@@ -135,7 +132,16 @@ describe('ssr: renderToString', () => {
         props: ['msg'],
         ssrRender(ctx: any, push: any, parent: any) {
           push(`<div class="child">`)
-          renderSlot(ctx.$slots.default, { msg: 'from slot' }, push, parent)
+          renderSlot(
+            ctx.$slots,
+            'default',
+            { msg: 'from slot' },
+            () => {
+              push(`fallback`)
+            },
+            push,
+            parent
+          )
           push(`</div>`)
         }
       }
@@ -169,6 +175,19 @@ describe('ssr: renderToString', () => {
           `<!----><span>from slot</span><!---->` +
           `</div></div>`
       )
+
+      // test fallback
+      expect(
+        await renderToString(
+          createApp({
+            ssrRender(_ctx, push, parent) {
+              push(`<div>parent`)
+              push(renderComponent(Child, { msg: 'hello' }, null, parent))
+              push(`</div>`)
+            }
+          })
+        )
+      ).toBe(`<div>parent<div class="child"><!---->fallback<!----></div></div>`)
     })
 
     test('nested components with vnode slots', async () => {
@@ -176,7 +195,14 @@ describe('ssr: renderToString', () => {
         props: ['msg'],
         ssrRender(ctx: any, push: any, parent: any) {
           push(`<div class="child">`)
-          renderSlot(ctx.$slots.default, { msg: 'from slot' }, push, parent)
+          renderSlot(
+            ctx.$slots,
+            'default',
+            { msg: 'from slot' },
+            null,
+            push,
+            parent
+          )
           push(`</div>`)
         }
       }
diff --git a/packages/server-renderer/src/helpers/renderSlot.ts b/packages/server-renderer/src/helpers/renderSlot.ts
new file mode 100644 (file)
index 0000000..e202cda
--- /dev/null
@@ -0,0 +1,35 @@
+import { Props, PushFn, renderVNodeChildren } from '../renderToString'
+import { ComponentInternalInstance, Slot, Slots } from 'vue'
+
+export type SSRSlots = Record<string, SSRSlot>
+
+export type SSRSlot = (
+  props: Props,
+  push: PushFn,
+  parentComponent: ComponentInternalInstance | null
+) => void
+
+export function renderSlot(
+  slots: Slots | SSRSlots,
+  slotName: string,
+  slotProps: Props,
+  fallbackRenderFn: (() => void) | null,
+  push: PushFn,
+  parentComponent: ComponentInternalInstance | null = null
+) {
+  const slotFn = slots[slotName]
+  // template-compiled slots are always rendered as fragments
+  push(`<!---->`)
+  if (slotFn) {
+    if (slotFn.length > 1) {
+      // only ssr-optimized slot fns accept more than 1 arguments
+      slotFn(slotProps, push, parentComponent)
+    } else {
+      // normal slot
+      renderVNodeChildren(push, (slotFn as Slot)(slotProps), parentComponent)
+    }
+  } else if (fallbackRenderFn) {
+    fallbackRenderFn()
+  }
+  push(`<!---->`)
+}
index 3f190b95d57e37adee2c3175da42efc81b8b4957..6d7a7c41a859588a22acfd4f98b2144abbbdb391 100644 (file)
@@ -2,10 +2,8 @@
 export { renderToString } from './renderToString'
 
 // internal runtime helpers
-export {
-  renderComponent as _renderComponent,
-  renderSlot as _renderSlot
-} from './renderToString'
+export { renderComponent as _renderComponent } from './renderToString'
+export { renderSlot as _renderSlot } from './helpers/renderSlot'
 export {
   renderClass as _renderClass,
   renderStyle as _renderStyle,
index 3d0f01244b16c9570c135688544a58e970a62bf5..d7b6626bb4e2d307602cb846dd55e73a80f18b73 100644 (file)
@@ -11,7 +11,6 @@ import {
   Portal,
   ShapeFlags,
   ssrUtils,
-  Slot,
   Slots
 } from 'vue'
 import {
@@ -23,6 +22,7 @@ import {
   escapeHtml
 } from '@vue/shared'
 import { renderAttrs } from './helpers/renderAttrs'
+import { SSRSlots } from './helpers/renderSlot'
 
 const {
   isVNode,
@@ -41,8 +41,8 @@ const {
 type SSRBuffer = SSRBufferItem[]
 type SSRBufferItem = string | ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
 type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
-type PushFn = (item: SSRBufferItem) => void
-type Props = Record<string, unknown>
+export type PushFn = (item: SSRBufferItem) => void
+export type Props = Record<string, unknown>
 
 function createBuffer() {
   let appendable = false
@@ -191,7 +191,7 @@ function renderVNode(
   }
 }
 
-function renderVNodeChildren(
+export function renderVNodeChildren(
   push: PushFn,
   children: VNodeArrayChildren,
   parentComponent: ComponentInternalInstance | null = null
@@ -255,29 +255,3 @@ function renderElement(
     push(`</${tag}>`)
   }
 }
-
-export type SSRSlots = Record<string, SSRSlot>
-
-export type SSRSlot = (
-  props: Props,
-  push: PushFn,
-  parentComponent: ComponentInternalInstance | null
-) => void
-
-export function renderSlot(
-  slotFn: Slot | SSRSlot,
-  slotProps: Props,
-  push: PushFn,
-  parentComponent: ComponentInternalInstance | null = null
-) {
-  // template-compiled slots are always rendered as fragments
-  push(`<!---->`)
-  if (slotFn.length > 1) {
-    // only ssr-optimized slot fns accept more than 1 arguments
-    slotFn(slotProps, push, parentComponent)
-  } else {
-    // normal slot
-    renderVNodeChildren(push, (slotFn as Slot)(slotProps), parentComponent)
-  }
-  push(`<!---->`)
-}