fix(compiler-dom): properly stringify class/style bindings when hoisting static strings

This commit is contained in:
Evan You 2020-02-21 13:10:13 +01:00
parent 189a0a3b19
commit 1b9b235663
11 changed files with 57 additions and 58 deletions

View File

@ -623,7 +623,7 @@ describe('compiler: element transform', () => {
test(`props merging: style`, () => { test(`props merging: style`, () => {
const { node } = parseWithElementTransform( const { node } = parseWithElementTransform(
`<div style="color: red" :style="{ color: 'red' }" />`, `<div style="color: green" :style="{ color: 'red' }" />`,
{ {
nodeTransforms: [transformStyle, transformElement], nodeTransforms: [transformStyle, transformElement],
directiveTransforms: { directiveTransforms: {
@ -646,7 +646,7 @@ describe('compiler: element transform', () => {
elements: [ elements: [
{ {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_1`, content: `{"color":"green"}`,
isStatic: false isStatic: false
}, },
{ {

View File

@ -2,9 +2,6 @@
exports[`compile should contain standard transforms 1`] = ` exports[`compile should contain standard transforms 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createVNode: _createVNode } = _Vue
const _hoisted_1 = {}
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
with (_ctx) { with (_ctx) {
@ -14,7 +11,7 @@ return function render(_ctx, _cache) {
_createVNode(\\"div\\", { textContent: text }, null, 8 /* PROPS */, [\\"textContent\\"]), _createVNode(\\"div\\", { textContent: text }, null, 8 /* PROPS */, [\\"textContent\\"]),
_createVNode(\\"div\\", { innerHTML: html }, null, 8 /* PROPS */, [\\"innerHTML\\"]), _createVNode(\\"div\\", { innerHTML: html }, null, 8 /* PROPS */, [\\"innerHTML\\"]),
_createVNode(\\"div\\", null, \\"test\\"), _createVNode(\\"div\\", null, \\"test\\"),
_createVNode(\\"div\\", { style: _hoisted_1 }, \\"red\\"), _createVNode(\\"div\\", { style: {\\"color\\":\\"red\\"} }, \\"red\\"),
_createVNode(\\"div\\", { style: {color: 'green'} }, null, 4 /* STYLE */) _createVNode(\\"div\\", { style: {color: 'green'} }, null, 4 /* STYLE */)
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
} }

View File

@ -5,7 +5,7 @@ describe('compile', () => {
const { code } = compile(`<div v-text="text"></div> const { code } = compile(`<div v-text="text"></div>
<div v-html="html"></div> <div v-html="html"></div>
<div v-cloak>test</div> <div v-cloak>test</div>
<div style="color=red">red</div> <div style="color:red">red</div>
<div :style="{color: 'green'}"></div>`) <div :style="{color: 'green'}"></div>`)
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()

View File

@ -77,8 +77,8 @@ describe('stringify static html', () => {
test('serliazing constant bindings', () => { test('serliazing constant bindings', () => {
const { ast } = compileWithStringify( const { ast } = compileWithStringify(
`<div><div>${repeat( `<div><div :style="{ color: 'red' }">${repeat(
`<span :class="'foo' + 'bar'">{{ 1 }} + {{ false }}</span>`, `<span :class="[{ foo: true }, { bar: true }]">{{ 1 }} + {{ false }}</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
)}</div></div>` )}</div></div>`
) )
@ -89,7 +89,7 @@ describe('stringify static html', () => {
callee: CREATE_STATIC, callee: CREATE_STATIC,
arguments: [ arguments: [
JSON.stringify( JSON.stringify(
`<div>${repeat( `<div style="color:red;">${repeat(
`<span class="foo bar">1 + false</span>`, `<span class="foo bar">1 + false</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
)}</div>` )}</div>`

View File

@ -26,17 +26,8 @@ function transformWithStyleTransform(
} }
describe('compiler: style transform', () => { describe('compiler: style transform', () => {
test('should transform into directive node and hoist value', () => { test('should transform into directive node', () => {
const { root, node } = transformWithStyleTransform( const { node } = transformWithStyleTransform(`<div style="color: red"/>`)
`<div style="color: red"/>`
)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: `{"color":"red"}`,
isStatic: false
}
])
expect(node.props[0]).toMatchObject({ expect(node.props[0]).toMatchObject({
type: NodeTypes.DIRECTIVE, type: NodeTypes.DIRECTIVE,
name: `bind`, name: `bind`,
@ -47,7 +38,7 @@ describe('compiler: style transform', () => {
}, },
exp: { exp: {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_1`, content: `{"color":"red"}`,
isStatic: false isStatic: false
} }
}) })
@ -71,7 +62,7 @@ describe('compiler: style transform', () => {
}, },
value: { value: {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_1`, content: `{"color":"red"}`,
isStatic: false isStatic: false
} }
} }

View File

@ -14,7 +14,10 @@ import {
isString, isString,
isSymbol, isSymbol,
escapeHtml, escapeHtml,
toDisplayString toDisplayString,
normalizeClass,
normalizeStyle,
stringifyStyle
} from '@vue/shared' } from '@vue/shared'
// Turn eligible hoisted static trees into stringied static nodes, e.g. // Turn eligible hoisted static trees into stringied static nodes, e.g.
@ -84,8 +87,15 @@ function stringifyElement(
} }
} else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') { } else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') {
// constant v-bind, e.g. :foo="1" // constant v-bind, e.g. :foo="1"
let evaluated = evaluateConstant(p.exp as SimpleExpressionNode)
const arg = p.arg && (p.arg as SimpleExpressionNode).content
if (arg === 'class') {
evaluated = normalizeClass(evaluated)
} else if (arg === 'style') {
evaluated = stringifyStyle(normalizeStyle(evaluated))
}
res += ` ${(p.arg as SimpleExpressionNode).content}="${escapeHtml( res += ` ${(p.arg as SimpleExpressionNode).content}="${escapeHtml(
evaluateConstant(p.exp as ExpressionNode) evaluated
)}"` )}"`
} }
} }
@ -151,7 +161,7 @@ function evaluateConstant(exp: ExpressionNode): string {
if (c.type === NodeTypes.TEXT) { if (c.type === NodeTypes.TEXT) {
res += c.content res += c.content
} else if (c.type === NodeTypes.INTERPOLATION) { } else if (c.type === NodeTypes.INTERPOLATION) {
res += evaluateConstant(c.content) res += toDisplayString(evaluateConstant(c.content))
} else { } else {
res += evaluateConstant(c) res += evaluateConstant(c)
} }

View File

@ -17,12 +17,11 @@ export const transformStyle: NodeTransform = (node, context) => {
node.props.forEach((p, i) => { node.props.forEach((p, i) => {
if (p.type === NodeTypes.ATTRIBUTE && p.name === 'style' && p.value) { if (p.type === NodeTypes.ATTRIBUTE && p.name === 'style' && p.value) {
// replace p with an expression node // replace p with an expression node
const exp = context.hoist(parseInlineCSS(p.value.content, p.loc))
node.props[i] = { node.props[i] = {
type: NodeTypes.DIRECTIVE, type: NodeTypes.DIRECTIVE,
name: `bind`, name: `bind`,
arg: createSimpleExpression(`style`, true, p.loc), arg: createSimpleExpression(`style`, true, p.loc),
exp, exp: parseInlineCSS(p.value.content, p.loc),
modifiers: [], modifiers: [],
loc: p.loc loc: p.loc
} }
@ -45,5 +44,5 @@ function parseInlineCSS(
tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim()) tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim())
} }
}) })
return createSimpleExpression(JSON.stringify(res), false, loc) return createSimpleExpression(JSON.stringify(res), false, loc, true)
} }

View File

@ -101,7 +101,7 @@ describe('ssr: element', () => {
expect( expect(
getCompiledString(`<div style="color:red;" :style="bar"></div>`) getCompiledString(`<div style="color:red;" :style="bar"></div>`)
).toMatchInlineSnapshot( ).toMatchInlineSnapshot(
`"\`<div style=\\"\${_ssrRenderStyle([_hoisted_1, _ctx.bar])}\\"></div>\`"` `"\`<div style=\\"\${_ssrRenderStyle([{\\"color\\":\\"red\\"}, _ctx.bar])}\\"></div>\`"`
) )
}) })
@ -184,7 +184,7 @@ describe('ssr: element', () => {
) )
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"\`<div\${_ssrRenderAttrs(_mergeProps({ "\`<div\${_ssrRenderAttrs(_mergeProps({
style: [_hoisted_1, _ctx.b] style: [{\\"color\\":\\"red\\"}, _ctx.b]
}, _ctx.obj))}></div>\`" }, _ctx.obj))}></div>\`"
`) `)
}) })

View File

@ -16,11 +16,9 @@ describe('ssr: v-show', () => {
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"const { ssrRenderStyle: _ssrRenderStyle } = require(\\"@vue/server-renderer\\") "const { ssrRenderStyle: _ssrRenderStyle } = require(\\"@vue/server-renderer\\")
const _hoisted_1 = {\\"color\\":\\"red\\"}
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<div style=\\"\${_ssrRenderStyle([ _push(\`<div style=\\"\${_ssrRenderStyle([
_hoisted_1, {\\"color\\":\\"red\\"},
(_ctx.foo) ? null : { display: \\"none\\" } (_ctx.foo) ? null : { display: \\"none\\" }
])}\\"></div>\`) ])}\\"></div>\`)
}" }"
@ -48,11 +46,9 @@ describe('ssr: v-show', () => {
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"const { ssrRenderStyle: _ssrRenderStyle } = require(\\"@vue/server-renderer\\") "const { ssrRenderStyle: _ssrRenderStyle } = require(\\"@vue/server-renderer\\")
const _hoisted_1 = {\\"color\\":\\"red\\"}
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<div style=\\"\${_ssrRenderStyle([ _push(\`<div style=\\"\${_ssrRenderStyle([
_hoisted_1, {\\"color\\":\\"red\\"},
{ fontSize: 14 }, { fontSize: 14 },
(_ctx.foo) ? null : { display: \\"none\\" } (_ctx.foo) ? null : { display: \\"none\\" }
])}\\"></div>\`) ])}\\"></div>\`)
@ -69,12 +65,10 @@ describe('ssr: v-show', () => {
"const { mergeProps: _mergeProps } = require(\\"vue\\") "const { mergeProps: _mergeProps } = require(\\"vue\\")
const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\") const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
const _hoisted_1 = {\\"color\\":\\"red\\"}
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<div\${_ssrRenderAttrs(_mergeProps(_ctx.baz, { _push(\`<div\${_ssrRenderAttrs(_mergeProps(_ctx.baz, {
style: [ style: [
_hoisted_1, {\\"color\\":\\"red\\"},
{ fontSize: 14 }, { fontSize: 14 },
(_ctx.foo) ? null : { display: \\"none\\" } (_ctx.foo) ? null : { display: \\"none\\" }
] ]

View File

@ -1,11 +1,9 @@
import { escapeHtml } from '@vue/shared' import { escapeHtml, stringifyStyle } from '@vue/shared'
import { import {
normalizeClass, normalizeClass,
normalizeStyle, normalizeStyle,
propsToAttrMap, propsToAttrMap,
hyphenate,
isString, isString,
isNoUnitNumericStyleProp,
isOn, isOn,
isSSRSafeAttrName, isSSRSafeAttrName,
isBooleanAttr, isBooleanAttr,
@ -93,17 +91,5 @@ export function ssrRenderStyle(raw: unknown): string {
return escapeHtml(raw) return escapeHtml(raw)
} }
const styles = normalizeStyle(raw) const styles = normalizeStyle(raw)
let ret = '' return escapeHtml(stringifyStyle(styles))
for (const key in styles) {
const value = styles[key]
const normalizedKey = key.indexOf(`--`) === 0 ? key : hyphenate(key)
if (
isString(value) ||
(typeof value === 'number' && isNoUnitNumericStyleProp(normalizedKey))
) {
// only render valid values
ret += `${normalizedKey}:${value};`
}
}
return escapeHtml(ret)
} }

View File

@ -1,4 +1,5 @@
import { isArray, isString, isObject } from './' import { isArray, isString, isObject, hyphenate } from './'
import { isNoUnitNumericStyleProp } from './domAttrConfig'
export function normalizeStyle( export function normalizeStyle(
value: unknown value: unknown
@ -19,6 +20,27 @@ export function normalizeStyle(
} }
} }
export function stringifyStyle(
styles: Record<string, string | number> | undefined
): string {
let ret = ''
if (!styles) {
return ret
}
for (const key in styles) {
const value = styles[key]
const normalizedKey = key.indexOf(`--`) === 0 ? key : hyphenate(key)
if (
isString(value) ||
(typeof value === 'number' && isNoUnitNumericStyleProp(normalizedKey))
) {
// only render valid values
ret += `${normalizedKey}:${value};`
}
}
return ret
}
export function normalizeClass(value: unknown): string { export function normalizeClass(value: unknown): string {
let res = '' let res = ''
if (isString(value)) { if (isString(value)) {