wip(compiler-ssr): v-bind with static keys

This commit is contained in:
Evan You 2020-02-04 16:47:12 -05:00
parent e71781dcab
commit c059fc88b9
18 changed files with 189 additions and 92 deletions

View File

@ -301,9 +301,10 @@ export interface SequenceExpression extends Node {
export interface ConditionalExpression extends Node { export interface ConditionalExpression extends Node {
type: NodeTypes.JS_CONDITIONAL_EXPRESSION type: NodeTypes.JS_CONDITIONAL_EXPRESSION
test: ExpressionNode test: JSChildNode
consequent: JSChildNode consequent: JSChildNode
alternate: JSChildNode alternate: JSChildNode
newline: boolean
} }
export interface CacheExpression extends Node { export interface CacheExpression extends Node {
@ -648,13 +649,15 @@ export function createSequenceExpression(
export function createConditionalExpression( export function createConditionalExpression(
test: ConditionalExpression['test'], test: ConditionalExpression['test'],
consequent: ConditionalExpression['consequent'], consequent: ConditionalExpression['consequent'],
alternate: ConditionalExpression['alternate'] alternate: ConditionalExpression['alternate'],
newline = true
): ConditionalExpression { ): ConditionalExpression {
return { return {
type: NodeTypes.JS_CONDITIONAL_EXPRESSION, type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
test, test,
consequent, consequent,
alternate, alternate,
newline,
loc: locStub loc: locStub
} }
} }

View File

@ -685,7 +685,7 @@ function genConditionalExpression(
node: ConditionalExpression, node: ConditionalExpression,
context: CodegenContext context: CodegenContext
) { ) {
const { test, consequent, alternate } = node const { test, consequent, alternate, newline: needNewline } = node
const { push, indent, deindent, newline } = context const { push, indent, deindent, newline } = context
if (test.type === NodeTypes.SIMPLE_EXPRESSION) { if (test.type === NodeTypes.SIMPLE_EXPRESSION) {
const needsParens = !isSimpleIdentifier(test.content) const needsParens = !isSimpleIdentifier(test.content)
@ -694,15 +694,15 @@ function genConditionalExpression(
needsParens && push(`)`) needsParens && push(`)`)
} else { } else {
push(`(`) push(`(`)
genCompoundExpression(test, context) genNode(test, context)
push(`)`) push(`)`)
} }
indent() needNewline && indent()
context.indentLevel++ context.indentLevel++
push(`? `) push(`? `)
genNode(consequent, context) genNode(consequent, context)
context.indentLevel-- context.indentLevel--
newline() needNewline && newline()
push(`: `) push(`: `)
const isNested = alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION const isNested = alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION
if (!isNested) { if (!isNested) {
@ -712,7 +712,7 @@ function genConditionalExpression(
if (!isNested) { if (!isNested) {
context.indentLevel-- context.indentLevel--
} }
deindent(true /* without newline */) needNewline && deindent(true /* without newline */)
} }
function genSequenceExpression( function genSequenceExpression(
@ -748,15 +748,17 @@ function genCacheExpression(node: CacheExpression, context: CodegenContext) {
function genTemplateLiteral(node: TemplateLiteral, context: CodegenContext) { function genTemplateLiteral(node: TemplateLiteral, context: CodegenContext) {
const { push, indent, deindent } = context const { push, indent, deindent } = context
push('`') push('`')
for (let i = 0; i < node.elements.length; i++) { const l = node.elements.length
const multilines = l > 3
for (let i = 0; i < l; i++) {
const e = node.elements[i] const e = node.elements[i]
if (isString(e)) { if (isString(e)) {
push(e.replace(/`/g, '\\`')) push(e.replace(/`/g, '\\`'))
} else { } else {
push('${') push('${')
indent() if (multilines) indent()
genNode(e, context) genNode(e, context)
deindent() if (multilines) deindent()
push('}') push('}')
} }
} }

View File

@ -31,6 +31,7 @@ export { noopDirectiveTransform } from './transforms/noopDirectiveTransform'
// expose transforms so higher-order compilers can import and extend them // expose transforms so higher-order compilers can import and extend them
export { transformModel } from './transforms/vModel' export { transformModel } from './transforms/vModel'
export { transformOn } from './transforms/vOn' export { transformOn } from './transforms/vOn'
export { transformBind } from './transforms/vBind'
// exported for compiler-ssr // exported for compiler-ssr
export { processIfBranches } from './transforms/vIf' export { processIfBranches } from './transforms/vIf'

View File

@ -10,12 +10,6 @@ describe('ssr: element', () => {
) )
}) })
test('static attrs', () => {
expect(
getCompiledString(`<div id="foo" class="bar"></div>`)
).toMatchInlineSnapshot(`"\`<div id=\\"foo\\" class=\\"bar\\"></div>\`"`)
})
test('nested elements', () => { test('nested elements', () => {
expect( expect(
getCompiledString(`<div><span></span><span></span></div>`) getCompiledString(`<div><span></span><span></span></div>`)
@ -26,27 +20,71 @@ describe('ssr: element', () => {
expect(getCompiledString(`<input>`)).toMatchInlineSnapshot(`"\`<input>\`"`) expect(getCompiledString(`<input>`)).toMatchInlineSnapshot(`"\`<input>\`"`)
}) })
test('v-html', () => { describe('children override', () => {
expect(getCompiledString(`<div v-html="foo"/>`)).toMatchInlineSnapshot( test('v-html', () => {
`"\`<div>\${_ctx.foo}</div>\`"` expect(getCompiledString(`<div v-html="foo"/>`)).toMatchInlineSnapshot(
) `"\`<div>\${_ctx.foo}</div>\`"`
)
})
test('v-text', () => {
expect(getCompiledString(`<div v-text="foo"/>`)).toMatchInlineSnapshot(
`"\`<div>\${_interpolate(_ctx.foo)}</div>\`"`
)
})
test('<textarea> with dynamic value', () => {
expect(
getCompiledString(`<textarea :value="foo"/>`)
).toMatchInlineSnapshot(
`"\`<textarea>\${_interpolate(_ctx.foo)}</textarea>\`"`
)
})
test('<textarea> with static value', () => {
expect(
getCompiledString(`<textarea value="fo&gt;o"/>`)
).toMatchInlineSnapshot(`"\`<textarea>fo&gt;o</textarea>\`"`)
})
}) })
test('v-text', () => { describe('attrs', () => {
expect(getCompiledString(`<div v-text="foo"/>`)).toMatchInlineSnapshot( test('static attrs', () => {
`"\`<div>\${_interpolate(_ctx.foo)}</div>\`"` expect(
) getCompiledString(`<div id="foo" class="bar"></div>`)
}) ).toMatchInlineSnapshot(`"\`<div id=\\"foo\\" class=\\"bar\\"></div>\`"`)
})
test('<textarea> with dynamic value', () => { test('v-bind:class', () => {
expect(getCompiledString(`<textarea :value="foo"/>`)).toMatchInlineSnapshot( expect(
`"\`<textarea>\${_interpolate(_ctx.foo)}</textarea>\`"` getCompiledString(`<div id="foo" :class="bar"></div>`)
) ).toMatchInlineSnapshot(
}) `"\`<div id=\\"foo\\"\${_renderClass(_ctx.bar)}></div>\`"`
)
})
test('<textarea> with static value', () => { test('v-bind:style', () => {
expect( expect(
getCompiledString(`<textarea value="fo&gt;o"/>`) getCompiledString(`<div id="foo" :style="bar"></div>`)
).toMatchInlineSnapshot(`"\`<textarea>fo&gt;o</textarea>\`"`) ).toMatchInlineSnapshot(
`"\`<div id=\\"foo\\"\${_renderStyle(_ctx.bar)}></div>\`"`
)
})
test('v-bind:key (boolean)', () => {
expect(
getCompiledString(`<input type="checkbox" :checked="checked">`)
).toMatchInlineSnapshot(
`"\`<input type=\\"checkbox\\"\${(_ctx.checked)? \\" checked\\": \\"\\"}>\`"`
)
})
test('v-bind:key (non-boolean)', () => {
expect(
getCompiledString(`<div :id="id" class="bar"></div>`)
).toMatchInlineSnapshot(
`"\`<div\${_renderAttr(\\"id\\", _ctx.id)} class=\\"bar\\"></div>\`"`
)
})
}) })
}) })

View File

@ -38,7 +38,11 @@ describe('ssr: text', () => {
"const { _interpolate } = require(\\"@vue/server-renderer\\") "const { _interpolate } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<div><span>\${_interpolate(_ctx.foo)} bar</span><span>baz \${_interpolate(_ctx.qux)}</span></div>\`) _push(\`<div><span>\${
_interpolate(_ctx.foo)
} bar</span><span>baz \${
_interpolate(_ctx.qux)
}</span></div>\`)
}" }"
`) `)
}) })

View File

@ -1,13 +0,0 @@
import { compile } from '../src'
describe('ssr: v-bind', () => {
test('basic', () => {
expect(compile(`<div :id="id"/>`).code).toMatchInlineSnapshot(`
"const { _renderAttr } = require(\\"vue\\")
return function ssrRender(_ctx, _push, _parent) {
_push(\`<div\${_renderAttr(\\"id\\", _ctx.id)}></div>\`)
}"
`)
})
})

View File

@ -45,7 +45,11 @@ describe('ssr: v-for', () => {
_renderList(_ctx.list, (row, i) => { _renderList(_ctx.list, (row, i) => {
_push(\`<div><!---->\`) _push(\`<div><!---->\`)
_renderList(row, (j) => { _renderList(row, (j) => {
_push(\`<div>\${_interpolate(i)},\${_interpolate(j)}</div>\`) _push(\`<div>\${
_interpolate(i)
},\${
_interpolate(j)
}</div>\`)
}) })
_push(\`<!----></div>\`) _push(\`<!----></div>\`)
}) })
@ -97,7 +101,11 @@ describe('ssr: v-for', () => {
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<!---->\`) _push(\`<!---->\`)
_renderList(_ctx.list, (i) => { _renderList(_ctx.list, (i) => {
_push(\`<!----><span>\${_interpolate(i)}</span><span>\${_interpolate(i + 1)}</span><!---->\`) _push(\`<!----><span>\${
_interpolate(i)
}</span><span>\${
_interpolate(i + 1)
}</span><!---->\`)
}) })
_push(\`<!---->\`) _push(\`<!---->\`)
}" }"

View File

@ -1,5 +1,5 @@
import { compile } from '../src' import { compile } from '../src'
export function getCompiledString(src: string): string { export function getCompiledString(src: string): string {
return compile(src).code.match(/_push\((.*)\)/)![1] return compile(src).code.match(/_push\(([^]*)\)/)![1]
} }

View File

@ -17,9 +17,11 @@ export function createSSRCompilerError(
} }
export const enum SSRErrorCodes { export const enum SSRErrorCodes {
X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM = DOMErrorCodes.__EXTEND_POINT__ X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM = DOMErrorCodes.__EXTEND_POINT__,
X_SSR_UNSAFE_ATTR_NAME
} }
export const SSRErrorMessages: { [code: number]: string } = { export const SSRErrorMessages: { [code: number]: string } = {
[SSRErrorCodes.X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM]: `Custom directive is missing corresponding SSR transform and will be ignored.` [SSRErrorCodes.X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM]: `Custom directive is missing corresponding SSR transform and will be ignored.`,
[SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME]: `Unsafe attribute name for SSR.`
} }

View File

@ -8,7 +8,8 @@ import {
transformExpression, transformExpression,
trackVForSlotScopes, trackVForSlotScopes,
trackSlotScopes, trackSlotScopes,
noopDirectiveTransform noopDirectiveTransform,
transformBind
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { ssrCodegenTransform } from './ssrCodegenTransform' import { ssrCodegenTransform } from './ssrCodegenTransform'
import { ssrTransformElement } from './transforms/ssrTransformElement' import { ssrTransformElement } from './transforms/ssrTransformElement'
@ -16,9 +17,8 @@ import { ssrTransformComponent } from './transforms/ssrTransformComponent'
import { ssrTransformSlotOutlet } from './transforms/ssrTransformSlotOutlet' import { ssrTransformSlotOutlet } from './transforms/ssrTransformSlotOutlet'
import { ssrTransformIf } from './transforms/ssrVIf' import { ssrTransformIf } from './transforms/ssrVIf'
import { ssrTransformFor } from './transforms/ssrVFor' import { ssrTransformFor } from './transforms/ssrVFor'
import { ssrVBind } from './transforms/ssrVBind' import { ssrTransformModel } from './transforms/ssrVModel'
import { ssrVModel } from './transforms/ssrVModel' import { ssrTransformShow } from './transforms/ssrVShow'
import { ssrVShow } from './transforms/ssrVShow'
export function compile( export function compile(
template: string, template: string,
@ -54,9 +54,9 @@ export function compile(
ssrDirectiveTransforms: { ssrDirectiveTransforms: {
on: noopDirectiveTransform, on: noopDirectiveTransform,
cloak: noopDirectiveTransform, cloak: noopDirectiveTransform,
bind: ssrVBind, bind: transformBind, // reusing core v-bind
model: ssrVModel, model: ssrTransformModel,
show: ssrVShow, show: ssrTransformShow,
...(options.ssrDirectiveTransforms || {}) // user transforms ...(options.ssrDirectiveTransforms || {}) // user transforms
} }
}) })

View File

@ -7,6 +7,7 @@ export const SSR_RENDER_CLASS = Symbol(`renderClass`)
export const SSR_RENDER_STYLE = Symbol(`renderStyle`) export const SSR_RENDER_STYLE = Symbol(`renderStyle`)
export const SSR_RENDER_ATTRS = Symbol(`renderAttrs`) export const SSR_RENDER_ATTRS = Symbol(`renderAttrs`)
export const SSR_RENDER_ATTR = Symbol(`renderAttr`) export const SSR_RENDER_ATTR = Symbol(`renderAttr`)
export const SSR_RENDER_DYNAMIC_ATTR = Symbol(`renderDynamicAttr`)
export const SSR_RENDER_LIST = Symbol(`renderList`) export const SSR_RENDER_LIST = Symbol(`renderList`)
// Note: these are helpers imported from @vue/server-renderer // Note: these are helpers imported from @vue/server-renderer
@ -19,5 +20,6 @@ registerRuntimeHelpers({
[SSR_RENDER_STYLE]: `_renderStyle`, [SSR_RENDER_STYLE]: `_renderStyle`,
[SSR_RENDER_ATTRS]: `_renderAttrs`, [SSR_RENDER_ATTRS]: `_renderAttrs`,
[SSR_RENDER_ATTR]: `_renderAttr`, [SSR_RENDER_ATTR]: `_renderAttr`,
[SSR_RENDER_DYNAMIC_ATTR]: `_renderDynamicAttr`,
[SSR_RENDER_LIST]: `_renderList` [SSR_RENDER_LIST]: `_renderList`
}) })

View File

@ -5,11 +5,18 @@ import {
TemplateLiteral, TemplateLiteral,
createTemplateLiteral, createTemplateLiteral,
createInterpolation, createInterpolation,
createCallExpression createCallExpression,
createConditionalExpression,
createSimpleExpression
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { escapeHtml } from '@vue/shared' import { escapeHtml, isBooleanAttr, isSSRSafeAttrName } from '@vue/shared'
import { createSSRCompilerError, SSRErrorCodes } from '../errors' import { createSSRCompilerError, SSRErrorCodes } from '../errors'
import { SSR_RENDER_ATTR } from '../runtimeHelpers' import {
SSR_RENDER_ATTR,
SSR_RENDER_CLASS,
SSR_RENDER_STYLE,
SSR_RENDER_DYNAMIC_ATTR
} from '../runtimeHelpers'
export const ssrTransformElement: NodeTransform = (node, context) => { export const ssrTransformElement: NodeTransform = (node, context) => {
if ( if (
@ -66,12 +73,58 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
const { props } = directiveTransform(prop, node, context) const { props } = directiveTransform(prop, node, context)
for (let j = 0; j < props.length; j++) { for (let j = 0; j < props.length; j++) {
const { key, value } = props[j] const { key, value } = props[j]
openTag.push( if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) {
createCallExpression(context.helper(SSR_RENDER_ATTR), [ const attrName = key.content
key, // static key attr
value if (attrName === 'class') {
]) openTag.push(
) createCallExpression(context.helper(SSR_RENDER_CLASS), [
value
])
)
} else if (attrName === 'style') {
openTag.push(
createCallExpression(context.helper(SSR_RENDER_STYLE), [
value
])
)
} else if (isBooleanAttr(attrName)) {
openTag.push(
createConditionalExpression(
value,
createSimpleExpression(' ' + attrName, true),
createSimpleExpression('', true),
false /* no newline */
)
)
} else {
if (isSSRSafeAttrName(attrName)) {
openTag.push(
createCallExpression(context.helper(SSR_RENDER_ATTR), [
key,
value
])
)
} else {
context.onError(
createSSRCompilerError(
SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME,
key.loc
)
)
}
}
} else {
// dynamic key attr
// this branch is only encountered for custom directive
// transforms that returns properties with dynamic keys
openTag.push(
createCallExpression(
context.helper(SSR_RENDER_DYNAMIC_ATTR),
[key, value]
)
)
}
} }
} else { } else {
// no corresponding ssr directive transform found. // no corresponding ssr directive transform found.

View File

@ -1,18 +0,0 @@
import { DirectiveTransform, createObjectProperty } from '@vue/compiler-dom'
export const ssrVBind: DirectiveTransform = (dir, node, context) => {
if (!dir.exp) {
// error
return { props: [] }
} else {
// TODO modifiers
return {
props: [
createObjectProperty(
dir.arg!, // v-bind="obj" is handled separately
dir.exp
)
]
}
}
}

View File

@ -1,6 +1,6 @@
import { DirectiveTransform } from '@vue/compiler-dom' import { DirectiveTransform } from '@vue/compiler-dom'
export const ssrVModel: DirectiveTransform = (dir, node, context) => { export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
return { return {
props: [] props: []
} }

View File

@ -1,6 +1,6 @@
import { DirectiveTransform } from '@vue/compiler-dom' import { DirectiveTransform } from '@vue/compiler-dom'
export const ssrVShow: DirectiveTransform = (dir, node, context) => { export const ssrTransformShow: DirectiveTransform = (dir, node, context) => {
return { return {
props: [] props: []
} }

View File

@ -33,13 +33,18 @@ export function renderAttrs(
} else if (key === 'style') { } else if (key === 'style') {
ret += ` style="${renderStyle(value)}"` ret += ` style="${renderStyle(value)}"`
} else { } else {
ret += renderAttr(key, value, tag) ret += renderDynamicAttr(key, value, tag)
} }
} }
return ret return ret
} }
export function renderAttr(key: string, value: unknown, tag?: string): string { // render an attr with dynamic (unknown) key.
export function renderDynamicAttr(
key: string,
value: unknown,
tag?: string
): string {
if (value == null) { if (value == null) {
return `` return ``
} }
@ -56,6 +61,15 @@ export function renderAttr(key: string, value: unknown, tag?: string): string {
} }
} }
// Render a v-bind attr with static key. The key is pre-processed at compile
// time and we only need to check and escape value.
export function renderAttr(key: string, value: unknown): string {
if (value == null) {
return ``
}
return ` ${key}="${escapeHtml(value)}"`
}
export function renderClass(raw: unknown): string { export function renderClass(raw: unknown): string {
return escapeHtml(normalizeClass(raw)) return escapeHtml(normalizeClass(raw))
} }

View File

@ -10,7 +10,8 @@ export {
renderClass as _renderClass, renderClass as _renderClass,
renderStyle as _renderStyle, renderStyle as _renderStyle,
renderAttrs as _renderAttrs, renderAttrs as _renderAttrs,
renderAttr as _renderAttr renderAttr as _renderAttr,
renderDynamicAttr as _renderDynamicAttr
} from './helpers/renderAttrs' } from './helpers/renderAttrs'
export { interpolate as _interpolate } from './helpers/interpolate' export { interpolate as _interpolate } from './helpers/interpolate'
export { renderList as _renderList } from './helpers/renderList' export { renderList as _renderList } from './helpers/renderList'