]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(compiler-ssr): component slots
authorEvan You <yyx990803@gmail.com>
Thu, 6 Feb 2020 17:05:53 +0000 (12:05 -0500)
committerEvan You <yyx990803@gmail.com>
Thu, 6 Feb 2020 17:05:53 +0000 (12:05 -0500)
packages/compiler-ssr/__tests__/ssrComponent.spec.ts
packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
packages/compiler-ssr/src/transforms/ssrVModel.ts
packages/compiler-ssr/src/transforms/ssrVShow.ts

index b47c4f945272a2557ff4d2a0ac649237b999e1f9..3edbb2e24d078bca584bae8fbde461ec60d0c9a4 100644 (file)
@@ -45,4 +45,121 @@ describe('ssr: components', () => {
       }"
     `)
   })
+
+  describe('slots', () => {
+    test('implicit default slot', () => {
+      expect(compile(`<foo>hello<div/></foo>`).code).toMatchInlineSnapshot(`
+        "const { resolveComponent } = require(\\"vue\\")
+        const { _renderComponent } = require(\\"@vue/server-renderer\\")
+
+        return function ssrRender(_ctx, _push, _parent) {
+          const _component_foo = resolveComponent(\\"foo\\")
+
+          _renderComponent(_component_foo, null, {
+            default: (_, _push, _parent) => {
+              _push(\`hello<div></div>\`)
+            },
+            _compiled: true
+          }, _parent)
+        }"
+      `)
+    })
+
+    test('explicit default slot', () => {
+      expect(compile(`<foo v-slot="{ msg }">{{ msg + outer }}</foo>`).code)
+        .toMatchInlineSnapshot(`
+        "const { resolveComponent } = require(\\"vue\\")
+        const { _renderComponent, _interpolate } = require(\\"@vue/server-renderer\\")
+
+        return function ssrRender(_ctx, _push, _parent) {
+          const _component_foo = resolveComponent(\\"foo\\")
+
+          _renderComponent(_component_foo, null, {
+            default: ({ msg }, _push, _parent) => {
+              _push(\`\${_interpolate(msg + _ctx.outer)}\`)
+            },
+            _compiled: true
+          }, _parent)
+        }"
+      `)
+    })
+
+    test('named slots', () => {
+      expect(
+        compile(`<foo>
+        <template v-slot>foo</template>
+        <template v-slot:named>bar</template>
+      </foo>`).code
+      ).toMatchInlineSnapshot(`
+        "const { resolveComponent } = require(\\"vue\\")
+        const { _renderComponent } = require(\\"@vue/server-renderer\\")
+
+        return function ssrRender(_ctx, _push, _parent) {
+          const _component_foo = resolveComponent(\\"foo\\")
+
+          _renderComponent(_component_foo, null, {
+            default: (_, _push, _parent) => {
+              _push(\`foo\`)
+            },
+            named: (_, _push, _parent) => {
+              _push(\`bar\`)
+            },
+            _compiled: true
+          }, _parent)
+        }"
+      `)
+    })
+
+    test('v-if slot', () => {
+      expect(
+        compile(`<foo>
+        <template v-slot:named v-if="ok">foo</template>
+      </foo>`).code
+      ).toMatchInlineSnapshot(`
+        "const { resolveComponent, createSlots } = require(\\"vue\\")
+        const { _renderComponent } = require(\\"@vue/server-renderer\\")
+
+        return function ssrRender(_ctx, _push, _parent) {
+          const _component_foo = resolveComponent(\\"foo\\")
+
+          _renderComponent(_component_foo, null, createSlots({ _compiled: true }, [
+            (_ctx.ok)
+              ? {
+                  name: \\"named\\",
+                  fn: (_, _push, _parent) => {
+                    _push(\`foo\`)
+                  }
+                }
+              : undefined
+          ]), _parent)
+        }"
+      `)
+    })
+
+    test('v-for slot', () => {
+      expect(
+        compile(`<foo>
+        <template v-for="key in names" v-slot:[key]="{ msg }">{{ msg + key + bar }}</template>
+      </foo>`).code
+      ).toMatchInlineSnapshot(`
+        "const { resolveComponent, renderList, createSlots } = require(\\"vue\\")
+        const { _renderComponent, _interpolate } = require(\\"@vue/server-renderer\\")
+
+        return function ssrRender(_ctx, _push, _parent) {
+          const _component_foo = resolveComponent(\\"foo\\")
+
+          _renderComponent(_component_foo, null, createSlots({ _compiled: true }, [
+            renderList(_ctx.names, (key) => {
+              return {
+                name: key,
+                fn: ({ msg }, _push, _parent) => {
+                  _push(\`\${_interpolate(msg + key + _ctx.bar)}\`)
+                }
+              }
+            })
+          ]), _parent)
+        }"
+      `)
+    })
+  })
 })
index 034f1efd420271557327efd5f2aef14db9c05c01..6916515d80998325288a07c03a9a4c53dd3350e8 100644 (file)
@@ -7,12 +7,33 @@ import {
   buildProps,
   ComponentNode,
   PORTAL,
-  SUSPENSE
+  SUSPENSE,
+  SlotFnBuilder,
+  createFunctionExpression,
+  createBlockStatement,
+  buildSlots,
+  FunctionExpression,
+  TemplateChildNode
 } from '@vue/compiler-dom'
 import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
-import { SSRTransformContext } from '../ssrCodegenTransform'
+import {
+  SSRTransformContext,
+  createChildContext,
+  processChildren
+} from '../ssrCodegenTransform'
 import { isSymbol } from '@vue/shared'
 
+// 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
+// the 2nd pass, so we store the WIP slot functions in a weakmap during the 1st
+// pass and complete them in the 2nd pass.
+const wipMap = new WeakMap<ComponentNode, WIPSlotEntry[]>()
+
+interface WIPSlotEntry {
+  fn: FunctionExpression
+  children: TemplateChildNode[]
+}
+
 export const ssrTransformComponent: NodeTransform = (node, context) => {
   if (
     node.type !== NodeTypes.ELEMENT ||
@@ -38,20 +59,37 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
 
     // note we are not passing ssr: true here because for components, v-on
     // handlers should still be passed
-    const { props } = buildProps(node, context)
+    const props =
+      node.props.length > 0 ? buildProps(node, context).props || `null` : `null`
+
+    const wipEntries: WIPSlotEntry[] = []
+    wipMap.set(node, wipEntries)
+
+    const buildSSRSlotFn: SlotFnBuilder = (props, children, loc) => {
+      // An SSR slot function has the signature of
+      //   (props, _push, _parent) => void
+      // See server-renderer/src/helpers/renderSlot.ts
+      const fn = createFunctionExpression(
+        [props || `_`, `_push`, `_parent`],
+        undefined, // no return, assign body later
+        true, // newline
+        false, // isSlot: pass false since we don't need client scopeId codegen
+        loc
+      )
+      wipEntries.push({ fn, children })
+      return fn
+    }
+
+    const slots = node.children.length
+      ? buildSlots(node, context, buildSSRSlotFn).slots
+      : `null`
 
-    // TODO slots
     // TODO option for slots bail out
     // TODO scopeId
 
     node.ssrCodegenNode = createCallExpression(
       context.helper(SSR_RENDER_COMPONENT),
-      [
-        component,
-        props || `null`,
-        `null`, // TODO slots
-        `_parent`
-      ]
+      [component, props, slots, `_parent`]
     )
   }
 }
@@ -60,5 +98,13 @@ export function ssrProcessComponent(
   node: ComponentNode,
   context: SSRTransformContext
 ) {
+  // finish up slot function expressions from the 1st pass.
+  const wipEntries = wipMap.get(node) || []
+  for (let i = 0; i < wipEntries.length; i++) {
+    const { fn, children } = wipEntries[i]
+    const childContext = createChildContext(context)
+    processChildren(children, childContext)
+    fn.body = createBlockStatement(childContext.body)
+  }
   context.pushStatement(node.ssrCodegenNode!)
 }
index 1af92879c0670b38d6b011a48a8ea25adddbe3df..f23c62fedb49433efd5c6c8e28f93b0a80b4ab1f 100644 (file)
@@ -41,7 +41,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
     const res: DirectiveTransformResult = { props: [] }
     const defaultProps = [
       // default value binding for text type inputs
-      createObjectProperty(createSimpleExpression(`value`, true), model)
+      createObjectProperty(`value`, model)
     ]
     if (node.tag === 'input') {
       const type = findProp(node, 'type')
@@ -62,7 +62,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
             case 'radio':
               res.props = [
                 createObjectProperty(
-                  createSimpleExpression(`checked`, true),
+                  `checked`,
                   createCallExpression(context.helper(SSR_LOOSE_EQUAL), [
                     model,
                     value
@@ -73,7 +73,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
             case 'checkbox':
               res.props = [
                 createObjectProperty(
-                  createSimpleExpression(`checked`, true),
+                  `checked`,
                   createConditionalExpression(
                     createCallExpression(`Array.isArray`, [model]),
                     createCallExpression(context.helper(SSR_LOOSE_CONTAIN), [
index 314d8a234c2da70088a6eb6bcf45cab685e87766..ecd1c0d60b60c8a9edf088002134a1434458be56 100644 (file)
@@ -17,13 +17,13 @@ export const ssrTransformShow: DirectiveTransform = (dir, node, context) => {
   return {
     props: [
       createObjectProperty(
-        createSimpleExpression(`style`, true),
+        `style`,
         createConditionalExpression(
           dir.exp!,
           createSimpleExpression(`null`, false),
           createObjectExpression([
             createObjectProperty(
-              createSimpleExpression(`display`, true),
+              `display`,
               createSimpleExpression(`none`, true)
             )
           ]),