]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(ssr): support custom directive getSSRProps in optimized compilation
authorEvan You <yyx990803@gmail.com>
Fri, 4 Feb 2022 00:58:28 +0000 (08:58 +0800)
committerEvan You <yyx990803@gmail.com>
Fri, 4 Feb 2022 00:58:31 +0000 (08:58 +0800)
close #5304

14 files changed:
packages/compiler-core/__tests__/transforms/transformElement.spec.ts
packages/compiler-core/src/index.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/compiler-dom/__tests__/transforms/__snapshots__/vModel.spec.ts.snap
packages/compiler-ssr/__tests__/ssrComponent.spec.ts
packages/compiler-ssr/__tests__/ssrElement.spec.ts
packages/compiler-ssr/src/runtimeHelpers.ts
packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
packages/compiler-ssr/src/transforms/ssrTransformElement.ts
packages/runtime-core/src/directives.ts
packages/server-renderer/__tests__/ssrDirectives.spec.ts
packages/server-renderer/src/helpers/ssrGetDirectiveProps.ts [new file with mode: 0644]
packages/server-renderer/src/index.ts
packages/shared/src/index.ts

index 138fa1005c4630586ec837a2b61d3d130dab2394..e7a95622d1ab33d55ef7bf5d9b7ec4775cb650d2 100644 (file)
@@ -5,7 +5,8 @@ import {
   ErrorCodes,
   BindingTypes,
   NodeTransform,
-  transformExpression
+  transformExpression,
+  baseCompile
 } from '../../src'
 import {
   RESOLVE_COMPONENT,
@@ -66,6 +67,7 @@ function parseWithBind(template: string, options?: CompilerOptions) {
   return parseWithElementTransform(template, {
     ...options,
     directiveTransforms: {
+      ...options?.directiveTransforms,
       bind: transformBind
     }
   })
@@ -932,7 +934,11 @@ describe('compiler: element transform', () => {
     })
 
     test('NEED_PATCH (vnode hooks)', () => {
-      const { node } = parseWithBind(`<div @vnodeUpdated="foo" />`)
+      const root = baseCompile(`<div @vnodeUpdated="foo" />`, {
+        prefixIdentifiers: true,
+        cacheHandlers: true
+      }).ast
+      const node = (root as any).children[0].codegenNode
       expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
     })
 
index 1233e0ada591bc0a8eb3b6e8e39d84e485bb837c..a68d239581508e821839443fd3289b22a045c9a7 100644 (file)
@@ -54,7 +54,9 @@ export {
 export {
   transformElement,
   resolveComponentType,
-  buildProps
+  buildProps,
+  buildDirectiveArgs,
+  PropsExpression
 } from './transforms/transformElement'
 export { processSlotOutlet } from './transforms/transformSlotOutlet'
 export { generateCodeFrame } from '@vue/shared'
index 47cb3b0f48f005044cda9a80894fb1c550029ea5..7143963d1f5d0b1ec73b6da8f947ce062e171b11 100644 (file)
@@ -29,7 +29,8 @@ import {
   isObject,
   isReservedProp,
   capitalize,
-  camelize
+  camelize,
+  isBuiltInDirective
 } from '@vue/shared'
 import { createCompilerError, ErrorCodes } from '../errors'
 import {
@@ -665,7 +666,7 @@ export function buildProps(
             directiveImportMap.set(prop, needRuntime)
           }
         }
-      } else {
+      } else if (!isBuiltInDirective(name)) {
         // no built-in transform, this is a user custom directive.
         runtimeDirectives.push(prop)
         // custom dirs may use beforeUpdate so they need to force blocks
@@ -853,7 +854,7 @@ function mergeAsArray(existing: Property, incoming: Property) {
   }
 }
 
-function buildDirectiveArgs(
+export function buildDirectiveArgs(
   dir: DirectiveNode,
   context: TransformContext
 ): ArrayExpression {
index 1a4ce5e66c7a1abe4dcf02417e5dd17ccaabce85..cdaadc1d6302270f8a0b9c9ebfec01d6ad31326e 100644 (file)
@@ -37,14 +37,11 @@ exports[`compiler: transform v-model input w/ dynamic v-bind 2`] = `
 
 return function render(_ctx, _cache) {
   with (_ctx) {
-    const { vModelDynamic: _vModelDynamic, resolveDirective: _resolveDirective, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
-
-    const _directive_bind = _resolveDirective(\\"bind\\")
+    const { vModelDynamic: _vModelDynamic, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
 
     return _withDirectives((_openBlock(), _createElementBlock(\\"input\\", {
       \\"onUpdate:modelValue\\": $event => ((model) = $event)
     }, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
-      [_directive_bind, val, key],
       [_vModelDynamic, model]
     ])
   }
@@ -152,14 +149,11 @@ exports[`compiler: transform v-model simple expression for input (dynamic type)
 
 return function render(_ctx, _cache) {
   with (_ctx) {
-    const { vModelDynamic: _vModelDynamic, resolveDirective: _resolveDirective, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
-
-    const _directive_bind = _resolveDirective(\\"bind\\")
+    const { vModelDynamic: _vModelDynamic, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
 
     return _withDirectives((_openBlock(), _createElementBlock(\\"input\\", {
       \\"onUpdate:modelValue\\": $event => ((model) = $event)
     }, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
-      [_directive_bind, foo, \\"type\\"],
       [_vModelDynamic, model]
     ])
   }
index d7ceb0bcb3cf2c7cf4c7a3afe916e95a5e89855c..49eb9ff994172267eaecb8d8e5a4912bd8e830af 100644 (file)
@@ -377,4 +377,20 @@ describe('ssr: components', () => {
       })
     })
   })
+
+  describe('custom directive', () => {
+    test('basic', () => {
+      expect(compile(`<foo v-xxx:x.y="z" />`).code).toMatchInlineSnapshot(`
+        "const { resolveComponent: _resolveComponent, resolveDirective: _resolveDirective, mergeProps: _mergeProps } = require(\\"vue\\")
+        const { ssrGetDirectiveProps: _ssrGetDirectiveProps, ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\")
+
+        return function ssrRender(_ctx, _push, _parent, _attrs) {
+          const _component_foo = _resolveComponent(\\"foo\\")
+          const _directive_xxx = _resolveDirective(\\"xxx\\")
+
+          _push(_ssrRenderComponent(_component_foo, _mergeProps(_attrs, _ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.z, \\"x\\", { y: true })), null, _parent))
+        }"
+      `)
+    })
+  })
 })
index fee439755128b1f652bc5fec0ff12c3617284844..bf95065237d7fd889648c6f22a076f8b5fe193be 100644 (file)
@@ -288,5 +288,56 @@ describe('ssr: element', () => {
           }></div>\`"
       `)
     })
+
+    test('custom dir', () => {
+      expect(getCompiledString(`<div v-xxx:x.y="z" />`)).toMatchInlineSnapshot(`
+        "\`<div\${
+            _ssrRenderAttrs(_ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.z, \\"x\\", { y: true }))
+          }></div>\`"
+      `)
+    })
+
+    test('custom dir with normal attrs', () => {
+      expect(getCompiledString(`<div class="foo" v-xxx />`))
+        .toMatchInlineSnapshot(`
+        "\`<div\${
+            _ssrRenderAttrs(_mergeProps({ class: \\"foo\\" }, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
+          }></div>\`"
+      `)
+    })
+
+    test('custom dir with v-bind', () => {
+      expect(getCompiledString(`<div :title="foo" :class="bar" v-xxx />`))
+        .toMatchInlineSnapshot(`
+        "\`<div\${
+            _ssrRenderAttrs(_mergeProps({
+              title: _ctx.foo,
+              class: _ctx.bar
+            }, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
+          }></div>\`"
+      `)
+    })
+
+    test('custom dir with object v-bind', () => {
+      expect(getCompiledString(`<div v-bind="x" v-xxx />`))
+        .toMatchInlineSnapshot(`
+        "\`<div\${
+            _ssrRenderAttrs(_mergeProps(_ctx.x, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
+          }></div>\`"
+      `)
+    })
+
+    test('custom dir with object v-bind + normal bindings', () => {
+      expect(
+        getCompiledString(`<div v-bind="x" class="foo" v-xxx title="bar" />`)
+      ).toMatchInlineSnapshot(`
+        "\`<div\${
+            _ssrRenderAttrs(_mergeProps(_ctx.x, {
+              class: \\"foo\\",
+              title: \\"bar\\"
+            }, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
+          }></div>\`"
+      `)
+    })
   })
 })
index 9be6c610a93c4c36c557a1a6c97552868bd3ae78..f0a6a2f290cdc922b91a0c3b65934b69f8ae3845 100644 (file)
@@ -17,6 +17,7 @@ export const SSR_RENDER_DYNAMIC_MODEL = Symbol(`ssrRenderDynamicModel`)
 export const SSR_GET_DYNAMIC_MODEL_PROPS = Symbol(`ssrGetDynamicModelProps`)
 export const SSR_RENDER_TELEPORT = Symbol(`ssrRenderTeleport`)
 export const SSR_RENDER_SUSPENSE = Symbol(`ssrRenderSuspense`)
+export const SSR_GET_DIRECTIVE_PROPS = Symbol(`ssrGetDirectiveProps`)
 
 export const ssrHelpers = {
   [SSR_INTERPOLATE]: `ssrInterpolate`,
@@ -35,7 +36,8 @@ export const ssrHelpers = {
   [SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`,
   [SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps`,
   [SSR_RENDER_TELEPORT]: `ssrRenderTeleport`,
-  [SSR_RENDER_SUSPENSE]: `ssrRenderSuspense`
+  [SSR_RENDER_SUSPENSE]: `ssrRenderSuspense`,
+  [SSR_GET_DIRECTIVE_PROPS]: `ssrGetDirectiveProps`
 }
 
 // Note: these are helpers imported from @vue/server-renderer
index e4cd2698d1b8b3382777c133ace81a20e324a629..b02d3afddb5aaed972846c30258068db56458a6a 100644 (file)
@@ -33,7 +33,8 @@ import {
   TELEPORT,
   TRANSITION_GROUP,
   CREATE_VNODE,
-  CallExpression
+  CallExpression,
+  JSChildNode
 } from '@vue/compiler-dom'
 import { SSR_RENDER_COMPONENT, SSR_RENDER_VNODE } from '../runtimeHelpers'
 import {
@@ -48,6 +49,7 @@ import {
 } from './ssrTransformSuspense'
 import { ssrProcessTransitionGroup } from './ssrTransformTransitionGroup'
 import { isSymbol, isObject, isArray } from '@vue/shared'
+import { buildSSRProps } from './ssrTransformElement'
 
 // 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
@@ -110,12 +112,15 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
       })
     }
 
-    const props =
-      node.props.length > 0
-        ? // note we are not passing ssr: true here because for components, v-on
-          // handlers should still be passed
-          buildProps(node, context).props || `null`
-        : `null`
+    let propsExp: string | JSChildNode = `null`
+    if (node.props.length) {
+      // note we are not passing ssr: true here because for components, v-on
+      // handlers should still be passed
+      const { props, directives } = buildProps(node, context)
+      if (props || directives.length) {
+        propsExp = buildSSRProps(props, directives, context)
+      }
+    }
 
     const wipEntries: WIPSlotEntry[] = []
     wipMap.set(node, wipEntries)
@@ -151,7 +156,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
           `_push`,
           createCallExpression(context.helper(CREATE_VNODE), [
             component,
-            props,
+            propsExp,
             slots
           ]),
           `_parent`
@@ -160,7 +165,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
     } else {
       node.ssrCodegenNode = createCallExpression(
         context.helper(SSR_RENDER_COMPONENT),
-        [component, props, slots, `_parent`]
+        [component, propsExp, slots, `_parent`]
       )
     }
   }
index 13d8c04f4ce6bbf1d690caef29d265433134501d..08b7f4ad00b72a2e05e25539eb99dcc97bd2a3e3 100644 (file)
@@ -26,11 +26,15 @@ import {
   createSequenceExpression,
   InterpolationNode,
   isStaticExp,
-  AttributeNode
+  AttributeNode,
+  buildDirectiveArgs,
+  TransformContext,
+  PropsExpression
 } from '@vue/compiler-dom'
 import {
   escapeHtml,
   isBooleanAttr,
+  isBuiltInDirective,
   isSSRSafeAttrName,
   NO,
   propsToAttrMap
@@ -44,7 +48,8 @@ import {
   SSR_RENDER_ATTRS,
   SSR_INTERPOLATE,
   SSR_GET_DYNAMIC_MODEL_PROPS,
-  SSR_INCLUDE_BOOLEAN_ATTR
+  SSR_INCLUDE_BOOLEAN_ATTR,
+  SSR_GET_DIRECTIVE_PROPS
 } from '../runtimeHelpers'
 import { SSRTransformContext, processChildren } from '../ssrCodegenTransform'
 
@@ -71,16 +76,26 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
     const needTagForRuntime =
       node.tag === 'textarea' || node.tag.indexOf('-') > 0
 
-    // v-bind="obj" or v-bind:[key] can potentially overwrite other static
-    // attrs and can affect final rendering result, so when they are present
-    // we need to bail out to full `renderAttrs`
+    // v-bind="obj", v-bind:[key] and custom directives can potentially
+    // overwrite other static attrs and can affect final rendering result,
+    // so when they are present we need to bail out to full `renderAttrs`
     const hasDynamicVBind = hasDynamicKeyVBind(node)
-    if (hasDynamicVBind) {
-      const { props } = buildProps(node, context, node.props, true /* ssr */)
-      if (props) {
+    const hasCustomDir = node.props.some(
+      p => p.type === NodeTypes.DIRECTIVE && !isBuiltInDirective(p.name)
+    )
+    const needMergeProps = hasDynamicVBind || hasCustomDir
+    if (needMergeProps) {
+      const { props, directives } = buildProps(
+        node,
+        context,
+        node.props,
+        true /* ssr */
+      )
+      if (props || directives.length) {
+        const mergedProps = buildSSRProps(props, directives, context)
         const propsExp = createCallExpression(
           context.helper(SSR_RENDER_ATTRS),
-          [props]
+          [mergedProps]
         )
 
         if (node.tag === 'textarea') {
@@ -99,7 +114,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
             propsExp.arguments = [
               createAssignmentExpression(
                 createSimpleExpression(tempId, false),
-                props
+                mergedProps
               )
             ]
             rawChildrenMap.set(
@@ -128,7 +143,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
             const tempExp = createSimpleExpression(tempId, false)
             propsExp.arguments = [
               createSequenceExpression([
-                createAssignmentExpression(tempExp, props),
+                createAssignmentExpression(tempExp, mergedProps),
                 createCallExpression(context.helper(MERGE_PROPS), [
                   tempExp,
                   createCallExpression(
@@ -176,10 +191,10 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
             createCompilerError(ErrorCodes.X_V_SLOT_MISPLACED, prop.loc)
           )
         } else if (isTextareaWithValue(node, prop) && prop.exp) {
-          if (!hasDynamicVBind) {
+          if (!needMergeProps) {
             node.children = [createInterpolation(prop.exp, prop.loc)]
           }
-        } else if (!hasDynamicVBind) {
+        } else if (!needMergeProps) {
           // Directive transforms.
           const directiveTransform = context.directiveTransforms[prop.name]
           if (directiveTransform) {
@@ -277,7 +292,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
         // special case: value on <textarea>
         if (node.tag === 'textarea' && prop.name === 'value' && prop.value) {
           rawChildrenMap.set(node, escapeHtml(prop.value.content))
-        } else if (!hasDynamicVBind) {
+        } else if (!needMergeProps) {
           if (prop.name === 'key' || prop.name === 'ref') {
             continue
           }
@@ -307,6 +322,37 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
   }
 }
 
+export function buildSSRProps(
+  props: PropsExpression | undefined,
+  directives: DirectiveNode[],
+  context: TransformContext
+): JSChildNode {
+  let mergePropsArgs: JSChildNode[] = []
+  if (props) {
+    if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
+      // already a mergeProps call
+      mergePropsArgs = props.arguments as JSChildNode[]
+    } else {
+      mergePropsArgs.push(props)
+    }
+  }
+  if (directives.length) {
+    for (const dir of directives) {
+      context.directives.add(dir.name)
+      mergePropsArgs.push(
+        createCallExpression(context.helper(SSR_GET_DIRECTIVE_PROPS), [
+          `_ctx`,
+          ...buildDirectiveArgs(dir, context).elements
+        ] as JSChildNode[])
+      )
+    }
+  }
+
+  return mergePropsArgs.length > 1
+    ? createCallExpression(context.helper(MERGE_PROPS), mergePropsArgs)
+    : mergePropsArgs[0]
+}
+
 function isTrueFalseValue(prop: DirectiveNode | AttributeNode) {
   if (prop.type === NodeTypes.DIRECTIVE) {
     return (
index ec13e951bccb8ba1f7ab1fa95ebea29bb4a1b71b..a3cf81dd0af37520b3f0d4d197c9901ea8a0a942 100644 (file)
@@ -12,7 +12,7 @@ return withDirectives(h(comp), [
 */
 
 import { VNode } from './vnode'
-import { isFunction, EMPTY_OBJ, makeMap } from '@vue/shared'
+import { isFunction, EMPTY_OBJ, isBuiltInDirective } from '@vue/shared'
 import { warn } from './warning'
 import { ComponentInternalInstance, Data } from './component'
 import { currentRenderingInstance } from './componentRenderContext'
@@ -63,10 +63,6 @@ export type Directive<T = any, V = any> =
 
 export type DirectiveModifiers = Record<string, boolean>
 
-const isBuiltInDirective = /*#__PURE__*/ makeMap(
-  'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo'
-)
-
 export function validateDirectiveName(name: string) {
   if (isBuiltInDirective(name)) {
     warn('Do not use built-in directive ids as custom directive id: ' + name)
index 788ba092500e69dd5c1a6c022aca91e03a9510c8..3e8bd2e0f67905989bc941e992f266d68c208c9a 100644 (file)
@@ -10,8 +10,10 @@ import {
   vShow,
   vModelText,
   vModelRadio,
-  vModelCheckbox
+  vModelCheckbox,
+  resolveDirective
 } from 'vue'
+import { ssrGetDirectiveProps, ssrRenderAttrs } from '../src'
 
 describe('ssr: directives', () => {
   describe('template v-show', () => {
@@ -374,7 +376,7 @@ describe('ssr: directives', () => {
     })
   })
 
-  test('custom directive w/ getSSRProps', async () => {
+  test('custom directive w/ getSSRProps (vdom)', async () => {
     expect(
       await renderToString(
         createApp({
@@ -394,4 +396,35 @@ describe('ssr: directives', () => {
       )
     ).toBe(`<div id="foo"></div>`)
   })
+
+  test('custom directive w/ getSSRProps (optimized)', async () => {
+    expect(
+      await renderToString(
+        createApp({
+          data() {
+            return {
+              x: 'foo'
+            }
+          },
+          directives: {
+            xxx: {
+              getSSRProps({ value, arg, modifiers }) {
+                return { id: [value, arg, modifiers.ok].join('-') }
+              }
+            }
+          },
+          ssrRender(_ctx, _push, _parent, _attrs) {
+            const _directive_xxx = resolveDirective('xxx')!
+            _push(
+              `<div${ssrRenderAttrs(
+                ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.x, 'arg', {
+                  ok: true
+                })
+              )}></div>`
+            )
+          }
+        })
+      )
+    ).toBe(`<div id="foo-arg-true"></div>`)
+  })
 })
diff --git a/packages/server-renderer/src/helpers/ssrGetDirectiveProps.ts b/packages/server-renderer/src/helpers/ssrGetDirectiveProps.ts
new file mode 100644 (file)
index 0000000..9a1eb31
--- /dev/null
@@ -0,0 +1,26 @@
+import { ComponentPublicInstance, Directive } from '@vue/runtime-core'
+
+export function ssrGetDirectiveProps(
+  instance: ComponentPublicInstance,
+  dir: Directive,
+  value?: any,
+  arg?: string,
+  modifiers: Record<string, boolean> = {}
+): Record<string, any> {
+  if (typeof dir !== 'function' && dir.getSSRProps) {
+    return (
+      dir.getSSRProps(
+        {
+          dir,
+          instance,
+          value,
+          oldValue: undefined,
+          arg,
+          modifiers
+        },
+        null as any
+      ) || {}
+    )
+  }
+  return {}
+}
index a029305af4c3f9c1ffc57f218928e13cd0f3561c..e8b716a865792b7f7c1b11dc73f8d17375d308ca 100644 (file)
@@ -30,6 +30,7 @@ export {
 export { ssrInterpolate } from './helpers/ssrInterpolate'
 export { ssrRenderList } from './helpers/ssrRenderList'
 export { ssrRenderSuspense } from './helpers/ssrRenderSuspense'
+export { ssrGetDirectiveProps } from './helpers/ssrGetDirectiveProps'
 export { includeBooleanAttr as ssrIncludeBooleanAttr } from '@vue/shared'
 
 // v-model helpers
index fd4f5c9ddcec821a3ccab59b9d4662f87d1666e6..e47cb5cd673d28256b06b5fbb0b242f961df7264 100644 (file)
@@ -90,6 +90,10 @@ export const isReservedProp = /*#__PURE__*/ makeMap(
     'onVnodeBeforeUnmount,onVnodeUnmounted'
 )
 
+export const isBuiltInDirective = /*#__PURE__*/ makeMap(
+  'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo'
+)
+
 const cacheStringFunction = <T extends (str: string) => string>(fn: T): T => {
   const cache: Record<string, string> = Object.create(null)
   return ((str: string) => {