Skip to content

Commit 60cf175

Browse files
committed
feat(ssr): support custom directive getSSRProps in optimized compilation
close #5304
1 parent a51f935 commit 60cf175

File tree

14 files changed

+228
-45
lines changed

14 files changed

+228
-45
lines changed

packages/compiler-core/__tests__/transforms/transformElement.spec.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
ErrorCodes,
66
BindingTypes,
77
NodeTransform,
8-
transformExpression
8+
transformExpression,
9+
baseCompile
910
} from '../../src'
1011
import {
1112
RESOLVE_COMPONENT,
@@ -66,6 +67,7 @@ function parseWithBind(template: string, options?: CompilerOptions) {
6667
return parseWithElementTransform(template, {
6768
...options,
6869
directiveTransforms: {
70+
...options?.directiveTransforms,
6971
bind: transformBind
7072
}
7173
})
@@ -932,7 +934,11 @@ describe('compiler: element transform', () => {
932934
})
933935

934936
test('NEED_PATCH (vnode hooks)', () => {
935-
const { node } = parseWithBind(`<div @vnodeUpdated="foo" />`)
937+
const root = baseCompile(`<div @vnodeUpdated="foo" />`, {
938+
prefixIdentifiers: true,
939+
cacheHandlers: true
940+
}).ast
941+
const node = (root as any).children[0].codegenNode
936942
expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
937943
})
938944

packages/compiler-core/src/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ export {
5454
export {
5555
transformElement,
5656
resolveComponentType,
57-
buildProps
57+
buildProps,
58+
buildDirectiveArgs,
59+
PropsExpression
5860
} from './transforms/transformElement'
5961
export { processSlotOutlet } from './transforms/transformSlotOutlet'
6062
export { generateCodeFrame } from '@vue/shared'

packages/compiler-core/src/transforms/transformElement.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import {
2929
isObject,
3030
isReservedProp,
3131
capitalize,
32-
camelize
32+
camelize,
33+
isBuiltInDirective
3334
} from '@vue/shared'
3435
import { createCompilerError, ErrorCodes } from '../errors'
3536
import {
@@ -665,7 +666,7 @@ export function buildProps(
665666
directiveImportMap.set(prop, needRuntime)
666667
}
667668
}
668-
} else {
669+
} else if (!isBuiltInDirective(name)) {
669670
// no built-in transform, this is a user custom directive.
670671
runtimeDirectives.push(prop)
671672
// custom dirs may use beforeUpdate so they need to force blocks
@@ -853,7 +854,7 @@ function mergeAsArray(existing: Property, incoming: Property) {
853854
}
854855
}
855856

856-
function buildDirectiveArgs(
857+
export function buildDirectiveArgs(
857858
dir: DirectiveNode,
858859
context: TransformContext
859860
): ArrayExpression {

packages/compiler-dom/__tests__/transforms/__snapshots__/vModel.spec.ts.snap

+2-8
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,11 @@ exports[`compiler: transform v-model input w/ dynamic v-bind 2`] = `
3737
3838
return function render(_ctx, _cache) {
3939
with (_ctx) {
40-
const { vModelDynamic: _vModelDynamic, resolveDirective: _resolveDirective, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
41-
42-
const _directive_bind = _resolveDirective(\\"bind\\")
40+
const { vModelDynamic: _vModelDynamic, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
4341
4442
return _withDirectives((_openBlock(), _createElementBlock(\\"input\\", {
4543
\\"onUpdate:modelValue\\": $event => ((model) = $event)
4644
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
47-
[_directive_bind, val, key],
4845
[_vModelDynamic, model]
4946
])
5047
}
@@ -152,14 +149,11 @@ exports[`compiler: transform v-model simple expression for input (dynamic type)
152149
153150
return function render(_ctx, _cache) {
154151
with (_ctx) {
155-
const { vModelDynamic: _vModelDynamic, resolveDirective: _resolveDirective, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
156-
157-
const _directive_bind = _resolveDirective(\\"bind\\")
152+
const { vModelDynamic: _vModelDynamic, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
158153
159154
return _withDirectives((_openBlock(), _createElementBlock(\\"input\\", {
160155
\\"onUpdate:modelValue\\": $event => ((model) = $event)
161156
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
162-
[_directive_bind, foo, \\"type\\"],
163157
[_vModelDynamic, model]
164158
])
165159
}

packages/compiler-ssr/__tests__/ssrComponent.spec.ts

+16
Original file line numberDiff line numberDiff line change
@@ -377,4 +377,20 @@ describe('ssr: components', () => {
377377
})
378378
})
379379
})
380+
381+
describe('custom directive', () => {
382+
test('basic', () => {
383+
expect(compile(`<foo v-xxx:x.y="z" />`).code).toMatchInlineSnapshot(`
384+
"const { resolveComponent: _resolveComponent, resolveDirective: _resolveDirective, mergeProps: _mergeProps } = require(\\"vue\\")
385+
const { ssrGetDirectiveProps: _ssrGetDirectiveProps, ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\")
386+
387+
return function ssrRender(_ctx, _push, _parent, _attrs) {
388+
const _component_foo = _resolveComponent(\\"foo\\")
389+
const _directive_xxx = _resolveDirective(\\"xxx\\")
390+
391+
_push(_ssrRenderComponent(_component_foo, _mergeProps(_attrs, _ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.z, \\"x\\", { y: true })), null, _parent))
392+
}"
393+
`)
394+
})
395+
})
380396
})

packages/compiler-ssr/__tests__/ssrElement.spec.ts

+51
Original file line numberDiff line numberDiff line change
@@ -288,5 +288,56 @@ describe('ssr: element', () => {
288288
}></div>\`"
289289
`)
290290
})
291+
292+
test('custom dir', () => {
293+
expect(getCompiledString(`<div v-xxx:x.y="z" />`)).toMatchInlineSnapshot(`
294+
"\`<div\${
295+
_ssrRenderAttrs(_ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.z, \\"x\\", { y: true }))
296+
}></div>\`"
297+
`)
298+
})
299+
300+
test('custom dir with normal attrs', () => {
301+
expect(getCompiledString(`<div class="foo" v-xxx />`))
302+
.toMatchInlineSnapshot(`
303+
"\`<div\${
304+
_ssrRenderAttrs(_mergeProps({ class: \\"foo\\" }, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
305+
}></div>\`"
306+
`)
307+
})
308+
309+
test('custom dir with v-bind', () => {
310+
expect(getCompiledString(`<div :title="foo" :class="bar" v-xxx />`))
311+
.toMatchInlineSnapshot(`
312+
"\`<div\${
313+
_ssrRenderAttrs(_mergeProps({
314+
title: _ctx.foo,
315+
class: _ctx.bar
316+
}, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
317+
}></div>\`"
318+
`)
319+
})
320+
321+
test('custom dir with object v-bind', () => {
322+
expect(getCompiledString(`<div v-bind="x" v-xxx />`))
323+
.toMatchInlineSnapshot(`
324+
"\`<div\${
325+
_ssrRenderAttrs(_mergeProps(_ctx.x, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
326+
}></div>\`"
327+
`)
328+
})
329+
330+
test('custom dir with object v-bind + normal bindings', () => {
331+
expect(
332+
getCompiledString(`<div v-bind="x" class="foo" v-xxx title="bar" />`)
333+
).toMatchInlineSnapshot(`
334+
"\`<div\${
335+
_ssrRenderAttrs(_mergeProps(_ctx.x, {
336+
class: \\"foo\\",
337+
title: \\"bar\\"
338+
}, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
339+
}></div>\`"
340+
`)
341+
})
291342
})
292343
})

packages/compiler-ssr/src/runtimeHelpers.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const SSR_RENDER_DYNAMIC_MODEL = Symbol(`ssrRenderDynamicModel`)
1717
export const SSR_GET_DYNAMIC_MODEL_PROPS = Symbol(`ssrGetDynamicModelProps`)
1818
export const SSR_RENDER_TELEPORT = Symbol(`ssrRenderTeleport`)
1919
export const SSR_RENDER_SUSPENSE = Symbol(`ssrRenderSuspense`)
20+
export const SSR_GET_DIRECTIVE_PROPS = Symbol(`ssrGetDirectiveProps`)
2021

2122
export const ssrHelpers = {
2223
[SSR_INTERPOLATE]: `ssrInterpolate`,
@@ -35,7 +36,8 @@ export const ssrHelpers = {
3536
[SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`,
3637
[SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps`,
3738
[SSR_RENDER_TELEPORT]: `ssrRenderTeleport`,
38-
[SSR_RENDER_SUSPENSE]: `ssrRenderSuspense`
39+
[SSR_RENDER_SUSPENSE]: `ssrRenderSuspense`,
40+
[SSR_GET_DIRECTIVE_PROPS]: `ssrGetDirectiveProps`
3941
}
4042

4143
// Note: these are helpers imported from @vue/server-renderer

packages/compiler-ssr/src/transforms/ssrTransformComponent.ts

+14-9
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ import {
3333
TELEPORT,
3434
TRANSITION_GROUP,
3535
CREATE_VNODE,
36-
CallExpression
36+
CallExpression,
37+
JSChildNode
3738
} from '@vue/compiler-dom'
3839
import { SSR_RENDER_COMPONENT, SSR_RENDER_VNODE } from '../runtimeHelpers'
3940
import {
@@ -48,6 +49,7 @@ import {
4849
} from './ssrTransformSuspense'
4950
import { ssrProcessTransitionGroup } from './ssrTransformTransitionGroup'
5051
import { isSymbol, isObject, isArray } from '@vue/shared'
52+
import { buildSSRProps } from './ssrTransformElement'
5153

5254
// We need to construct the slot functions in the 1st pass to ensure proper
5355
// scope tracking, but the children of each slot cannot be processed until
@@ -110,12 +112,15 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
110112
})
111113
}
112114

113-
const props =
114-
node.props.length > 0
115-
? // note we are not passing ssr: true here because for components, v-on
116-
// handlers should still be passed
117-
buildProps(node, context).props || `null`
118-
: `null`
115+
let propsExp: string | JSChildNode = `null`
116+
if (node.props.length) {
117+
// note we are not passing ssr: true here because for components, v-on
118+
// handlers should still be passed
119+
const { props, directives } = buildProps(node, context)
120+
if (props || directives.length) {
121+
propsExp = buildSSRProps(props, directives, context)
122+
}
123+
}
119124

120125
const wipEntries: WIPSlotEntry[] = []
121126
wipMap.set(node, wipEntries)
@@ -151,7 +156,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
151156
`_push`,
152157
createCallExpression(context.helper(CREATE_VNODE), [
153158
component,
154-
props,
159+
propsExp,
155160
slots
156161
]),
157162
`_parent`
@@ -160,7 +165,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
160165
} else {
161166
node.ssrCodegenNode = createCallExpression(
162167
context.helper(SSR_RENDER_COMPONENT),
163-
[component, props, slots, `_parent`]
168+
[component, propsExp, slots, `_parent`]
164169
)
165170
}
166171
}

packages/compiler-ssr/src/transforms/ssrTransformElement.ts

+60-14
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,15 @@ import {
2626
createSequenceExpression,
2727
InterpolationNode,
2828
isStaticExp,
29-
AttributeNode
29+
AttributeNode,
30+
buildDirectiveArgs,
31+
TransformContext,
32+
PropsExpression
3033
} from '@vue/compiler-dom'
3134
import {
3235
escapeHtml,
3336
isBooleanAttr,
37+
isBuiltInDirective,
3438
isSSRSafeAttrName,
3539
NO,
3640
propsToAttrMap
@@ -44,7 +48,8 @@ import {
4448
SSR_RENDER_ATTRS,
4549
SSR_INTERPOLATE,
4650
SSR_GET_DYNAMIC_MODEL_PROPS,
47-
SSR_INCLUDE_BOOLEAN_ATTR
51+
SSR_INCLUDE_BOOLEAN_ATTR,
52+
SSR_GET_DIRECTIVE_PROPS
4853
} from '../runtimeHelpers'
4954
import { SSRTransformContext, processChildren } from '../ssrCodegenTransform'
5055

@@ -71,16 +76,26 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
7176
const needTagForRuntime =
7277
node.tag === 'textarea' || node.tag.indexOf('-') > 0
7378

74-
// v-bind="obj" or v-bind:[key] can potentially overwrite other static
75-
// attrs and can affect final rendering result, so when they are present
76-
// we need to bail out to full `renderAttrs`
79+
// v-bind="obj", v-bind:[key] and custom directives can potentially
80+
// overwrite other static attrs and can affect final rendering result,
81+
// so when they are present we need to bail out to full `renderAttrs`
7782
const hasDynamicVBind = hasDynamicKeyVBind(node)
78-
if (hasDynamicVBind) {
79-
const { props } = buildProps(node, context, node.props, true /* ssr */)
80-
if (props) {
83+
const hasCustomDir = node.props.some(
84+
p => p.type === NodeTypes.DIRECTIVE && !isBuiltInDirective(p.name)
85+
)
86+
const needMergeProps = hasDynamicVBind || hasCustomDir
87+
if (needMergeProps) {
88+
const { props, directives } = buildProps(
89+
node,
90+
context,
91+
node.props,
92+
true /* ssr */
93+
)
94+
if (props || directives.length) {
95+
const mergedProps = buildSSRProps(props, directives, context)
8196
const propsExp = createCallExpression(
8297
context.helper(SSR_RENDER_ATTRS),
83-
[props]
98+
[mergedProps]
8499
)
85100

86101
if (node.tag === 'textarea') {
@@ -99,7 +114,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
99114
propsExp.arguments = [
100115
createAssignmentExpression(
101116
createSimpleExpression(tempId, false),
102-
props
117+
mergedProps
103118
)
104119
]
105120
rawChildrenMap.set(
@@ -128,7 +143,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
128143
const tempExp = createSimpleExpression(tempId, false)
129144
propsExp.arguments = [
130145
createSequenceExpression([
131-
createAssignmentExpression(tempExp, props),
146+
createAssignmentExpression(tempExp, mergedProps),
132147
createCallExpression(context.helper(MERGE_PROPS), [
133148
tempExp,
134149
createCallExpression(
@@ -176,10 +191,10 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
176191
createCompilerError(ErrorCodes.X_V_SLOT_MISPLACED, prop.loc)
177192
)
178193
} else if (isTextareaWithValue(node, prop) && prop.exp) {
179-
if (!hasDynamicVBind) {
194+
if (!needMergeProps) {
180195
node.children = [createInterpolation(prop.exp, prop.loc)]
181196
}
182-
} else if (!hasDynamicVBind) {
197+
} else if (!needMergeProps) {
183198
// Directive transforms.
184199
const directiveTransform = context.directiveTransforms[prop.name]
185200
if (directiveTransform) {
@@ -277,7 +292,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
277292
// special case: value on <textarea>
278293
if (node.tag === 'textarea' && prop.name === 'value' && prop.value) {
279294
rawChildrenMap.set(node, escapeHtml(prop.value.content))
280-
} else if (!hasDynamicVBind) {
295+
} else if (!needMergeProps) {
281296
if (prop.name === 'key' || prop.name === 'ref') {
282297
continue
283298
}
@@ -307,6 +322,37 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
307322
}
308323
}
309324

325+
export function buildSSRProps(
326+
props: PropsExpression | undefined,
327+
directives: DirectiveNode[],
328+
context: TransformContext
329+
): JSChildNode {
330+
let mergePropsArgs: JSChildNode[] = []
331+
if (props) {
332+
if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
333+
// already a mergeProps call
334+
mergePropsArgs = props.arguments as JSChildNode[]
335+
} else {
336+
mergePropsArgs.push(props)
337+
}
338+
}
339+
if (directives.length) {
340+
for (const dir of directives) {
341+
context.directives.add(dir.name)
342+
mergePropsArgs.push(
343+
createCallExpression(context.helper(SSR_GET_DIRECTIVE_PROPS), [
344+
`_ctx`,
345+
...buildDirectiveArgs(dir, context).elements
346+
] as JSChildNode[])
347+
)
348+
}
349+
}
350+
351+
return mergePropsArgs.length > 1
352+
? createCallExpression(context.helper(MERGE_PROPS), mergePropsArgs)
353+
: mergePropsArgs[0]
354+
}
355+
310356
function isTrueFalseValue(prop: DirectiveNode | AttributeNode) {
311357
if (prop.type === NodeTypes.DIRECTIVE) {
312358
return (

0 commit comments

Comments
 (0)