]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(ssr): scopeId in slots
authorEvan You <yyx990803@gmail.com>
Thu, 6 Feb 2020 22:45:34 +0000 (17:45 -0500)
committerEvan You <yyx990803@gmail.com>
Thu, 6 Feb 2020 22:45:46 +0000 (17:45 -0500)
packages/compiler-ssr/__tests__/ssrComponent.spec.ts
packages/compiler-ssr/__tests__/ssrScopeId.spec.ts [new file with mode: 0644]
packages/compiler-ssr/src/ssrCodegenTransform.ts
packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
packages/compiler-ssr/src/transforms/ssrTransformElement.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/src/helpers/ssrRenderSlot.ts

index 3fd0629f3d9ea125d1876857f8adb95698ebca9f..b957fd7b6a546697e9a986b5e89fe81d1b592702 100644 (file)
@@ -56,8 +56,12 @@ describe('ssr: components', () => {
           const _component_foo = resolveComponent(\\"foo\\")
 
           _ssrRenderComponent(_component_foo, null, {
-            default: (_, _push, _parent) => {
-              _push(\`hello<div></div>\`)
+            default: (_, _push, _parent, _scopeId) => {
+              if (_scopeId) {
+                _push(\`hello<div \${_scopeId}></div>\`)
+              } else {
+                _push(\`hello<div></div>\`)
+              }
             },
             _compiled: true
           }, _parent)
@@ -75,7 +79,7 @@ describe('ssr: components', () => {
           const _component_foo = resolveComponent(\\"foo\\")
 
           _ssrRenderComponent(_component_foo, null, {
-            default: ({ msg }, _push, _parent) => {
+            default: ({ msg }, _push, _parent, _scopeId) => {
               _push(\`\${_ssrInterpolate(msg + _ctx.outer)}\`)
             },
             _compiled: true
@@ -98,10 +102,10 @@ describe('ssr: components', () => {
           const _component_foo = resolveComponent(\\"foo\\")
 
           _ssrRenderComponent(_component_foo, null, {
-            default: (_, _push, _parent) => {
+            default: (_, _push, _parent, _scopeId) => {
               _push(\`foo\`)
             },
-            named: (_, _push, _parent) => {
+            named: (_, _push, _parent, _scopeId) => {
               _push(\`bar\`)
             },
             _compiled: true
@@ -126,7 +130,7 @@ describe('ssr: components', () => {
             (_ctx.ok)
               ? {
                   name: \\"named\\",
-                  fn: (_, _push, _parent) => {
+                  fn: (_, _push, _parent, _scopeId) => {
                     _push(\`foo\`)
                   }
                 }
@@ -152,7 +156,7 @@ describe('ssr: components', () => {
             renderList(_ctx.names, (key) => {
               return {
                 name: key,
-                fn: ({ msg }, _push, _parent) => {
+                fn: ({ msg }, _push, _parent, _scopeId) => {
                   _push(\`\${_ssrInterpolate(msg + key + _ctx.bar)}\`)
                 }
               }
diff --git a/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts b/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts
new file mode 100644 (file)
index 0000000..eb1aede
--- /dev/null
@@ -0,0 +1,114 @@
+import { compile } from '../src'
+
+const scopeId = 'data-v-xxxxxxx'
+
+describe('ssr: scopeId', () => {
+  test('basic', () => {
+    expect(
+      compile(`<div><span>hello</span></div>`, {
+        scopeId
+      }).code
+    ).toMatchInlineSnapshot(`
+      "
+      return function ssrRender(_ctx, _push, _parent) {
+        _push(\`<div data-v-xxxxxxx><span data-v-xxxxxxx>hello</span></div>\`)
+      }"
+    `)
+  })
+
+  test('inside slots (only text)', () => {
+    // should have no branching inside slot
+    expect(
+      compile(`<foo>foo</foo>`, {
+        scopeId
+      }).code
+    ).toMatchInlineSnapshot(`
+      "const { resolveComponent } = require(\\"vue\\")
+      const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent) {
+        const _component_foo = resolveComponent(\\"foo\\")
+
+        _ssrRenderComponent(_component_foo, null, {
+          default: (_, _push, _parent, _scopeId) => {
+            _push(\`foo\`)
+          },
+          _compiled: true
+        }, _parent)
+      }"
+    `)
+  })
+
+  test('inside slots (with elements)', () => {
+    expect(
+      compile(`<foo><span>hello</span></foo>`, {
+        scopeId
+      }).code
+    ).toMatchInlineSnapshot(`
+      "const { resolveComponent } = require(\\"vue\\")
+      const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent) {
+        const _component_foo = resolveComponent(\\"foo\\")
+
+        _ssrRenderComponent(_component_foo, null, {
+          default: (_, _push, _parent, _scopeId) => {
+            if (_scopeId) {
+              _push(\`<span data-v-xxxxxxx \${_scopeId}>hello</span>\`)
+            } else {
+              _push(\`<span data-v-xxxxxxx>hello</span>\`)
+            }
+          },
+          _compiled: true
+        }, _parent)
+      }"
+    `)
+  })
+
+  test('nested slots', () => {
+    expect(
+      compile(`<foo><span>hello</span><bar><span/></bar></foo>`, {
+        scopeId
+      }).code
+    ).toMatchInlineSnapshot(`
+      "const { resolveComponent } = require(\\"vue\\")
+      const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent) {
+        const _component_bar = resolveComponent(\\"bar\\")
+        const _component_foo = resolveComponent(\\"foo\\")
+
+        _ssrRenderComponent(_component_foo, null, {
+          default: (_, _push, _parent, _scopeId) => {
+            if (_scopeId) {
+              _push(\`<span data-v-xxxxxxx \${_scopeId}>hello</span>\`)
+              _ssrRenderComponent(_component_bar, null, {
+                default: (_, _push, _parent, _scopeId) => {
+                  if (_scopeId) {
+                    _push(\`<span data-v-xxxxxxx \${_scopeId}></span>\`)
+                  } else {
+                    _push(\`<span data-v-xxxxxxx></span>\`)
+                  }
+                },
+                _compiled: true
+              }, _parent)
+            } else {
+              _push(\`<span data-v-xxxxxxx>hello</span>\`)
+              _ssrRenderComponent(_component_bar, null, {
+                default: (_, _push, _parent, _scopeId) => {
+                  if (_scopeId) {
+                    _push(\`<span data-v-xxxxxxx \${_scopeId}></span>\`)
+                  } else {
+                    _push(\`<span data-v-xxxxxxx></span>\`)
+                  }
+                },
+                _compiled: true
+              }, _parent)
+            }
+          },
+          _compiled: true
+        }, _parent)
+      }"
+    `)
+  })
+})
index d9efb3b01f849defde915565108303634ac8924b..c5012e8693b4ce50dc091114a28bd6d62e19e6e2 100644 (file)
@@ -13,12 +13,13 @@ import {
   IfStatement,
   CallExpression
 } from '@vue/compiler-dom'
-import { isString, escapeHtml, NO } from '@vue/shared'
+import { isString, escapeHtml } from '@vue/shared'
 import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
 import { ssrProcessIf } from './transforms/ssrVIf'
 import { ssrProcessFor } from './transforms/ssrVFor'
 import { ssrProcessSlotOutlet } from './transforms/ssrTransformSlotOutlet'
 import { ssrProcessComponent } from './transforms/ssrTransformComponent'
+import { ssrProcessElement } from './transforms/ssrTransformElement'
 
 // Because SSR codegen output is completely different from client-side output
 // (e.g. multiple elements can be concatenated into a single template literal
@@ -29,7 +30,7 @@ import { ssrProcessComponent } from './transforms/ssrTransformComponent'
 export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
   const context = createSSRTransformContext(options)
   const isFragment =
-    ast.children.length > 1 && !ast.children.every(c => isText(c))
+    ast.children.length > 1 && ast.children.some(c => !isText(c))
   processChildren(ast.children, context, isFragment)
   ast.codegenNode = createBlockStatement(context.body)
 
@@ -46,7 +47,8 @@ export type SSRTransformContext = ReturnType<typeof createSSRTransformContext>
 
 function createSSRTransformContext(
   options: CompilerOptions,
-  helpers: Set<symbol> = new Set()
+  helpers: Set<symbol> = new Set(),
+  withSlotScopeId = false
 ) {
   const body: BlockStatement['body'] = []
   let currentString: TemplateLiteral | null = null
@@ -55,6 +57,7 @@ function createSSRTransformContext(
     options,
     body,
     helpers,
+    withSlotScopeId,
     helper<T extends symbol>(name: T): T {
       helpers.add(name)
       return name
@@ -82,11 +85,16 @@ function createSSRTransformContext(
   }
 }
 
-export function createChildContext(
-  parent: SSRTransformContext
+function createChildContext(
+  parent: SSRTransformContext,
+  withSlotScopeId = parent.withSlotScopeId
 ): SSRTransformContext {
   // ensure child inherits parent helpers
-  return createSSRTransformContext(parent.options, parent.helpers)
+  return createSSRTransformContext(
+    parent.options,
+    parent.helpers,
+    withSlotScopeId
+  )
 }
 
 export function processChildren(
@@ -97,23 +105,11 @@ export function processChildren(
   if (asFragment) {
     context.pushStringPart(`<!---->`)
   }
-  const isVoidTag = context.options.isVoidTag || NO
   for (let i = 0; i < children.length; i++) {
     const child = children[i]
     if (child.type === NodeTypes.ELEMENT) {
       if (child.tagType === ElementTypes.ELEMENT) {
-        const elementsToAdd = child.ssrCodegenNode!.elements
-        for (let j = 0; j < elementsToAdd.length; j++) {
-          context.pushStringPart(elementsToAdd[j])
-        }
-        if (child.children.length) {
-          processChildren(child.children, context)
-        }
-
-        if (!isVoidTag(child.tag)) {
-          // push closing tag
-          context.pushStringPart(`</${child.tag}>`)
-        }
+        ssrProcessElement(child, context)
       } else if (child.tagType === ElementTypes.COMPONENT) {
         ssrProcessComponent(child, context)
       } else if (child.tagType === ElementTypes.SLOT) {
@@ -135,3 +131,14 @@ export function processChildren(
     context.pushStringPart(`<!---->`)
   }
 }
+
+export function processChildrenAsStatement(
+  children: TemplateChildNode[],
+  parentContext: SSRTransformContext,
+  asFragment = false,
+  withSlotScopeId = parentContext.withSlotScopeId
+): BlockStatement {
+  const childContext = createChildContext(parentContext, withSlotScopeId)
+  processChildren(children, childContext, asFragment)
+  return createBlockStatement(childContext.body)
+}
index de055f3da99996609cb8d0cebed0415534825cc9..7cc0d49ba8858f9e6e29ef52a8df0768cb7a9c39 100644 (file)
@@ -14,13 +14,16 @@ import {
   TemplateChildNode,
   PORTAL,
   SUSPENSE,
-  TRANSITION_GROUP
+  TRANSITION_GROUP,
+  createIfStatement,
+  createSimpleExpression,
+  isText
 } from '@vue/compiler-dom'
 import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
 import {
   SSRTransformContext,
-  createChildContext,
-  processChildren
+  processChildren,
+  processChildrenAsStatement
 } from '../ssrCodegenTransform'
 import { isSymbol } from '@vue/shared'
 
@@ -62,10 +65,10 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
 
     const buildSSRSlotFn: SlotFnBuilder = (props, children, loc) => {
       // An SSR slot function has the signature of
-      //   (props, _push, _parent) => void
+      //   (props, _push, _parent, _scopeId) => void
       // See server-renderer/src/helpers/renderSlot.ts
       const fn = createFunctionExpression(
-        [props || `_`, `_push`, `_parent`],
+        [props || `_`, `_push`, `_parent`, `_scopeId`],
         undefined, // no return, assign body later
         true, // newline
         false, // isSlot: pass false since we don't need client scopeId codegen
@@ -111,9 +114,24 @@ export function ssrProcessComponent(
     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)
+      const hasNonTextChild = children.some(c => !isText(c))
+      if (hasNonTextChild) {
+        // SSR slots need to handled potential presence of scopeId of the child
+        // component. To avoid the cost of concatenation when it's unnecessary,
+        // we split the code into two paths, one with slot scopeId and one without.
+        fn.body = createBlockStatement([
+          createIfStatement(
+            createSimpleExpression(`_scopeId`, false),
+            // branch with scopeId concatenation
+            processChildrenAsStatement(children, context, false, true),
+            // branch without scopeId concatenation
+            processChildrenAsStatement(children, context, false, false)
+          )
+        ])
+      } else {
+        // only text, no need for scopeId branching.
+        fn.body = processChildrenAsStatement(children, context)
+      }
     }
     context.pushStatement(node.ssrCodegenNode)
   }
index cb35c331652401652118eaf2c4f1e857b118e5a0..000266b095e767856a8c30a9f1957bfa04b3c386 100644 (file)
@@ -24,7 +24,7 @@ import {
   MERGE_PROPS,
   isBindKey
 } from '@vue/compiler-dom'
-import { escapeHtml, isBooleanAttr, isSSRSafeAttrName } from '@vue/shared'
+import { escapeHtml, isBooleanAttr, isSSRSafeAttrName, NO } from '@vue/shared'
 import { createSSRCompilerError, SSRErrorCodes } from '../errors'
 import {
   SSR_RENDER_ATTR,
@@ -35,6 +35,14 @@ import {
   SSR_INTERPOLATE,
   SSR_GET_DYNAMIC_MODEL_PROPS
 } from '../runtimeHelpers'
+import { SSRTransformContext, processChildren } from '../ssrCodegenTransform'
+
+// for directives with children overwrite (e.g. v-html & v-text), we need to
+// store the raw children so that they can be added in the 2nd pass.
+const rawChildrenMap = new WeakMap<
+  PlainElementNode,
+  TemplateLiteral['elements'][0]
+>()
 
 export const ssrTransformElement: NodeTransform = (node, context) => {
   if (
@@ -45,7 +53,6 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
       // element
       // generate the template literal representing the open tag.
       const openTag: TemplateLiteral['elements'] = [`<${node.tag}`]
-      let rawChildren
 
       // v-bind="obj" or v-bind:[key] can potentially overwrite other static
       // attrs and can affect final rendering result, so when they are present
@@ -70,10 +77,9 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
               props
             )
             const existingText = node.children[0] as TextNode | undefined
-            node.children = []
-            rawChildren = createCallExpression(
-              context.helper(SSR_INTERPOLATE),
-              [
+            rawChildrenMap.set(
+              node,
+              createCallExpression(context.helper(SSR_INTERPOLATE), [
                 createConditionalExpression(
                   createSimpleExpression(`"value" in ${tempId}`, false),
                   createSimpleExpression(`${tempId}.value`, false),
@@ -83,7 +89,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
                   ),
                   false
                 )
-              ]
+              ])
             )
           } else if (node.tag === 'input') {
             // <input v-bind="obj" v-model>
@@ -126,8 +132,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
         // special cases with children override
         if (prop.type === NodeTypes.DIRECTIVE) {
           if (prop.name === 'html' && prop.exp) {
-            node.children = []
-            rawChildren = prop.exp
+            rawChildrenMap.set(node, prop.exp)
           } else if (prop.name === 'text' && prop.exp) {
             node.children = [createInterpolation(prop.exp, prop.loc)]
           } else if (prop.name === 'slot') {
@@ -225,8 +230,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
         } else {
           // special case: value on <textarea>
           if (node.tag === 'textarea' && prop.name === 'value' && prop.value) {
-            node.children = []
-            rawChildren = escapeHtml(prop.value.content)
+            rawChildrenMap.set(node, escapeHtml(prop.value.content))
           } else if (!hasDynamicVBind) {
             // static prop
             if (prop.name === 'class' && prop.value) {
@@ -250,10 +254,6 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
         openTag.push(` ${context.scopeId}`)
       }
 
-      openTag.push(`>`)
-      if (rawChildren) {
-        openTag.push(rawChildren)
-      }
       node.ssrCodegenNode = createTemplateLiteral(openTag)
     }
   }
@@ -296,3 +296,35 @@ function findVModel(node: PlainElementNode): DirectiveNode | undefined {
     p => p.type === NodeTypes.DIRECTIVE && p.name === 'model' && p.exp
   ) as DirectiveNode | undefined
 }
+
+export function ssrProcessElement(
+  node: PlainElementNode,
+  context: SSRTransformContext
+) {
+  const isVoidTag = context.options.isVoidTag || NO
+  const elementsToAdd = node.ssrCodegenNode!.elements
+  for (let j = 0; j < elementsToAdd.length; j++) {
+    context.pushStringPart(elementsToAdd[j])
+  }
+
+  // Handle slot scopeId
+  if (context.withSlotScopeId) {
+    context.pushStringPart(` `)
+    context.pushStringPart(createSimpleExpression(`_scopeId`, false))
+  }
+
+  // close open tag
+  context.pushStringPart(`>`)
+
+  const rawChildren = rawChildrenMap.get(node)
+  if (rawChildren) {
+    context.pushStringPart(rawChildren)
+  } else if (node.children.length) {
+    processChildren(node.children, context)
+  }
+
+  if (!isVoidTag(node.tag)) {
+    // push closing tag
+    context.pushStringPart(`</${node.tag}>`)
+  }
+}
index bef9c661fc360debd09693b35e2cd0c809fc242c..b40d17ab4787c2035f96b67690bd1743a7987793 100644 (file)
@@ -4,14 +4,12 @@ import {
   processSlotOutlet,
   createCallExpression,
   SlotOutletNode,
-  createFunctionExpression,
-  createBlockStatement
+  createFunctionExpression
 } from '@vue/compiler-dom'
 import { SSR_RENDER_SLOT } from '../runtimeHelpers'
 import {
   SSRTransformContext,
-  createChildContext,
-  processChildren
+  processChildrenAsStatement
 } from '../ssrCodegenTransform'
 
 export const ssrTransformSlotOutlet: NodeTransform = (node, context) => {
@@ -38,10 +36,8 @@ export function ssrProcessSlotOutlet(
   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)
+    fallbackRenderFn.body = processChildrenAsStatement(node.children, context)
     // _renderSlot(slots, name, props, fallback, ...)
     renderCall.arguments[3] = fallbackRenderFn
   }
index ec9f251ed987a5f9c344681890ec1de98715c7de..1b6034ba8aca8aa5205c87c291c93064716c495d 100644 (file)
@@ -5,13 +5,11 @@ import {
   createCallExpression,
   createFunctionExpression,
   createForLoopParams,
-  createBlockStatement,
   NodeTypes
 } from '@vue/compiler-dom'
 import {
   SSRTransformContext,
-  createChildContext,
-  processChildren
+  processChildrenAsStatement
 } from '../ssrCodegenTransform'
 import { SSR_RENDER_LIST } from '../runtimeHelpers'
 
@@ -24,14 +22,16 @@ export const ssrTransformFor = createStructuralDirectiveTransform(
 // This is called during the 2nd transform pass to construct the SSR-sepcific
 // codegen nodes.
 export function ssrProcessFor(node: ForNode, context: SSRTransformContext) {
-  const childContext = createChildContext(context)
   const needFragmentWrapper =
     node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT
-  processChildren(node.children, childContext, needFragmentWrapper)
   const renderLoop = createFunctionExpression(
     createForLoopParams(node.parseResult)
   )
-  renderLoop.body = createBlockStatement(childContext.body)
+  renderLoop.body = processChildrenAsStatement(
+    node.children,
+    context,
+    needFragmentWrapper
+  )
 
   // v-for always renders a fragment
   context.pushStringPart(`<!---->`)
index a642b9fd42ece33162a10081a8870f76cbe45e78..aad7ad14d98b00579251a9d15ef452faf40ce575 100644 (file)
@@ -11,8 +11,7 @@ import {
 } from '@vue/compiler-dom'
 import {
   SSRTransformContext,
-  createChildContext,
-  processChildren
+  processChildrenAsStatement
 } from '../ssrCodegenTransform'
 
 // Plugin for the first transform pass, which simply constructs the AST node
@@ -63,7 +62,5 @@ function processIfBranch(
     (children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
     // optimize away nested fragments when the only child is a ForNode
     !(children.length === 1 && children[0].type === NodeTypes.FOR)
-  const childContext = createChildContext(context)
-  processChildren(children, childContext, needFragmentWrapper)
-  return createBlockStatement(childContext.body)
+  return processChildrenAsStatement(children, context, needFragmentWrapper)
 }
index 07e9c36882ffe562b6c3393fad5a41fc44559194..0ffc8176aee0a72077da0e266bbff484233bf513 100644 (file)
@@ -6,7 +6,8 @@ export type SSRSlots = Record<string, SSRSlot>
 export type SSRSlot = (
   props: Props,
   push: PushFn,
-  parentComponent: ComponentInternalInstance | null
+  parentComponent: ComponentInternalInstance | null,
+  scopeId: string | null
 ) => void
 
 export function ssrRenderSlot(
@@ -23,7 +24,8 @@ export function ssrRenderSlot(
   if (slotFn) {
     if (slotFn.length > 1) {
       // only ssr-optimized slot fns accept more than 1 arguments
-      slotFn(slotProps, push, parentComponent)
+      const scopeId = parentComponent && parentComponent.type.__scopeId
+      slotFn(slotProps, push, parentComponent, scopeId ? scopeId + `-s` : null)
     } else {
       // normal slot
       renderVNodeChildren(push, (slotFn as Slot)(slotProps), parentComponent)