refactor(compiler): prefix all imported helpers to avoid scope collision

This commit is contained in:
Evan You 2020-02-07 18:53:39 -05:00
parent c44d9fbe3d
commit 51317af6e8
6 changed files with 219 additions and 132 deletions

View File

@ -80,6 +80,7 @@ function createCodegenContext(
sourceMap = false, sourceMap = false,
filename = `template.vue.html`, filename = `template.vue.html`,
scopeId = null, scopeId = null,
optimizeBindings = false,
runtimeGlobalName = `Vue`, runtimeGlobalName = `Vue`,
runtimeModuleName = `vue`, runtimeModuleName = `vue`,
ssr = false ssr = false
@ -91,6 +92,7 @@ function createCodegenContext(
sourceMap, sourceMap,
filename, filename,
scopeId, scopeId,
optimizeBindings,
runtimeGlobalName, runtimeGlobalName,
runtimeModuleName, runtimeModuleName,
ssr, ssr,
@ -102,8 +104,7 @@ function createCodegenContext(
indentLevel: 0, indentLevel: 0,
map: undefined, map: undefined,
helper(key) { helper(key) {
const name = helperNameMap[key] return `_${helperNameMap[key]}`
return prefixIdentifiers ? name : `_${name}`
}, },
push(code, node) { push(code, node) {
context.code += code context.code += code
@ -282,7 +283,6 @@ export function generate(
function genFunctionPreamble(ast: RootNode, context: CodegenContext) { function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
const { const {
ssr, ssr,
helper,
prefixIdentifiers, prefixIdentifiers,
push, push,
newline, newline,
@ -293,13 +293,16 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
!__BROWSER__ && ssr !__BROWSER__ && ssr
? `require(${JSON.stringify(runtimeModuleName)})` ? `require(${JSON.stringify(runtimeModuleName)})`
: runtimeGlobalName : runtimeGlobalName
const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
// Generate const declaration for helpers // Generate const declaration for helpers
// In prefix mode, we place the const declaration at top so it's done // In prefix mode, we place the const declaration at top so it's done
// only once; But if we not prefixing, we place the declaration inside the // only once; But if we not prefixing, we place the declaration inside the
// with block so it doesn't incur the `in` check cost for every helper access. // with block so it doesn't incur the `in` check cost for every helper access.
if (ast.helpers.length > 0) { if (ast.helpers.length > 0) {
if (!__BROWSER__ && prefixIdentifiers) { if (!__BROWSER__ && prefixIdentifiers) {
push(`const { ${ast.helpers.map(helper).join(', ')} } = ${VueBinding}\n`) push(
`const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n`
)
} else { } else {
// "with" mode. // "with" mode.
// save Vue in a separate variable to avoid collision // save Vue in a separate variable to avoid collision
@ -310,7 +313,7 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
if (ast.hoists.length) { if (ast.hoists.length) {
const staticHelpers = [CREATE_VNODE, CREATE_COMMENT, CREATE_TEXT] const staticHelpers = [CREATE_VNODE, CREATE_COMMENT, CREATE_TEXT]
.filter(helper => ast.helpers.includes(helper)) .filter(helper => ast.helpers.includes(helper))
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`) .map(aliasHelper)
.join(', ') .join(', ')
push(`const { ${staticHelpers} } = _Vue\n`) push(`const { ${staticHelpers} } = _Vue\n`)
} }
@ -321,7 +324,7 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
// ssr guaruntees prefixIdentifier: true // ssr guaruntees prefixIdentifier: true
push( push(
`const { ${ast.ssrHelpers `const { ${ast.ssrHelpers
.map(helper) .map(aliasHelper)
.join(', ')} } = require("@vue/server-renderer")\n` .join(', ')} } = require("@vue/server-renderer")\n`
) )
} }
@ -335,7 +338,14 @@ function genModulePreamble(
context: CodegenContext, context: CodegenContext,
genScopeId: boolean genScopeId: boolean
) { ) {
const { push, helper, newline, scopeId, runtimeModuleName } = context const {
push,
helper,
newline,
scopeId,
optimizeBindings,
runtimeModuleName
} = context
if (genScopeId) { if (genScopeId) {
ast.helpers.push(WITH_SCOPE_ID) ast.helpers.push(WITH_SCOPE_ID)
@ -346,17 +356,35 @@ function genModulePreamble(
// generate import statements for helpers // generate import statements for helpers
if (ast.helpers.length) { if (ast.helpers.length) {
push( if (optimizeBindings) {
`import { ${ast.helpers.map(helper).join(', ')} } from ${JSON.stringify( // when bundled with webpack with code-split, calling an import binding
runtimeModuleName // as a function leads to it being wrapped with `Object(a.b)` or `(0,a.b)`,
)}\n` // incurring both payload size increase and potential perf overhead.
) // therefore we assign the imports to vairables (which is a constant ~50b
// cost per-component instead of scaling with template size)
push(
`import { ${ast.helpers
.map(s => helperNameMap[s])
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
)
push(
`\n// Binding optimization for webpack code-split\nconst ${ast.helpers
.map(s => `_${helperNameMap[s]} = ${helperNameMap[s]}`)
.join(', ')}\n`
)
} else {
push(
`import { ${ast.helpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
)
}
} }
if (ast.ssrHelpers && ast.ssrHelpers.length) { if (ast.ssrHelpers && ast.ssrHelpers.length) {
push( push(
`import { ${ast.ssrHelpers `import { ${ast.ssrHelpers
.map(helper) .map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from "@vue/server-renderer"\n` .join(', ')} } from "@vue/server-renderer"\n`
) )
} }

View File

@ -73,6 +73,9 @@ export interface CodegenOptions {
scopeId?: string | null scopeId?: string | null
// we need to know about this to generate proper preambles // we need to know about this to generate proper preambles
prefixIdentifiers?: boolean prefixIdentifiers?: boolean
// option to optimize helper import bindings via variable assignment
// (only used for webpack code-split)
optimizeBindings?: boolean
// for specifying where to import helpers // for specifying where to import helpers
runtimeModuleName?: string runtimeModuleName?: string
runtimeGlobalName?: string runtimeGlobalName?: string

View File

@ -15,19 +15,19 @@ export const SSR_RENDER_DYNAMIC_MODEL = Symbol(`ssrRenderDynamicModel`)
export const SSR_GET_DYNAMIC_MODEL_PROPS = Symbol(`ssrGetDynamicModelProps`) export const SSR_GET_DYNAMIC_MODEL_PROPS = Symbol(`ssrGetDynamicModelProps`)
export const ssrHelpers = { export const ssrHelpers = {
[SSR_INTERPOLATE]: `_ssrInterpolate`, [SSR_INTERPOLATE]: `ssrInterpolate`,
[SSR_RENDER_COMPONENT]: `_ssrRenderComponent`, [SSR_RENDER_COMPONENT]: `ssrRenderComponent`,
[SSR_RENDER_SLOT]: `_ssrRenderSlot`, [SSR_RENDER_SLOT]: `ssrRenderSlot`,
[SSR_RENDER_CLASS]: `_ssrRenderClass`, [SSR_RENDER_CLASS]: `ssrRenderClass`,
[SSR_RENDER_STYLE]: `_ssrRenderStyle`, [SSR_RENDER_STYLE]: `ssrRenderStyle`,
[SSR_RENDER_ATTRS]: `_ssrRenderAttrs`, [SSR_RENDER_ATTRS]: `ssrRenderAttrs`,
[SSR_RENDER_ATTR]: `_ssrRenderAttr`, [SSR_RENDER_ATTR]: `ssrRenderAttr`,
[SSR_RENDER_DYNAMIC_ATTR]: `_ssrRenderDynamicAttr`, [SSR_RENDER_DYNAMIC_ATTR]: `ssrRenderDynamicAttr`,
[SSR_RENDER_LIST]: `_ssrRenderList`, [SSR_RENDER_LIST]: `ssrRenderList`,
[SSR_LOOSE_EQUAL]: `_ssrLooseEqual`, [SSR_LOOSE_EQUAL]: `ssrLooseEqual`,
[SSR_LOOSE_CONTAIN]: `_ssrLooseContain`, [SSR_LOOSE_CONTAIN]: `ssrLooseContain`,
[SSR_RENDER_DYNAMIC_MODEL]: `_ssrRenderDynamicModel`, [SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`,
[SSR_GET_DYNAMIC_MODEL_PROPS]: `_ssrGetDynamicModelProps` [SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps`
} }
// Note: these are helpers imported from @vue/server-renderer // Note: these are helpers imported from @vue/server-renderer

View File

@ -2,22 +2,22 @@
export { renderToString } from './renderToString' export { renderToString } from './renderToString'
// internal runtime helpers // internal runtime helpers
export { renderComponent as _ssrRenderComponent } from './renderToString' export { renderComponent } from './renderToString'
export { ssrRenderSlot as _ssrRenderSlot } from './helpers/ssrRenderSlot' export { ssrRenderSlot } from './helpers/ssrRenderSlot'
export { export {
ssrRenderClass as _ssrRenderClass, ssrRenderClass,
ssrRenderStyle as _ssrRenderStyle, ssrRenderStyle,
ssrRenderAttrs as _ssrRenderAttrs, ssrRenderAttrs,
ssrRenderAttr as _ssrRenderAttr, ssrRenderAttr,
ssrRenderDynamicAttr as _ssrRenderDynamicAttr ssrRenderDynamicAttr
} from './helpers/ssrRenderAttrs' } from './helpers/ssrRenderAttrs'
export { ssrInterpolate as _ssrInterpolate } from './helpers/ssrInterpolate' export { ssrInterpolate } from './helpers/ssrInterpolate'
export { ssrRenderList as _ssrRenderList } from './helpers/ssrRenderList' export { ssrRenderList } from './helpers/ssrRenderList'
// v-model helpers // v-model helpers
export { export {
ssrLooseEqual as _ssrLooseEqual, ssrLooseEqual,
ssrLooseContain as _ssrLooseContain, ssrLooseContain,
ssrRenderDynamicModel as _ssrRenderDynamicModel, ssrRenderDynamicModel,
ssrGetDynamicModelProps as _ssrGetDynamicModelProps ssrGetDynamicModelProps
} from './helpers/ssrVModelHelpers' } from './helpers/ssrVModelHelpers'

View File

@ -6,6 +6,7 @@ export const ssrMode = ref(false)
export const compilerOptions: CompilerOptions = reactive({ export const compilerOptions: CompilerOptions = reactive({
mode: 'module', mode: 'module',
prefixIdentifiers: false, prefixIdentifiers: false,
optimizeBindings: false,
hoistStatic: false, hoistStatic: false,
cacheHandlers: false, cacheHandlers: false,
scopeId: null scopeId: null
@ -29,96 +30,134 @@ const App = {
}, },
`@${__COMMIT__}` `@${__COMMIT__}`
), ),
' | ',
h(
'a',
{
href:
'https://app.netlify.com/sites/vue-next-template-explorer/deploys',
target: `_blank`
},
'History'
),
h('div', { id: 'options' }, [ h('div', { id: 'options-wrapper' }, [
// mode selection h('div', { id: 'options-label' }, 'Options ↘'),
h('span', { class: 'options-group' }, [ h('ul', { id: 'options' }, [
h('span', { class: 'label' }, 'Mode:'), // mode selection
h('input', { h('li', { id: 'mode' }, [
type: 'radio', h('span', { class: 'label' }, 'Mode: '),
id: 'mode-module', h('input', {
name: 'mode', type: 'radio',
checked: isModule, id: 'mode-module',
onChange() { name: 'mode',
compilerOptions.mode = 'module' checked: isModule,
} onChange() {
}), compilerOptions.mode = 'module'
h('label', { for: 'mode-module' }, 'module'), }
h('input', { }),
type: 'radio', h('label', { for: 'mode-module' }, 'module'),
id: 'mode-function', ' ',
name: 'mode', h('input', {
checked: !isModule, type: 'radio',
onChange() { id: 'mode-function',
compilerOptions.mode = 'function' name: 'mode',
} checked: !isModule,
}), onChange() {
h('label', { for: 'mode-function' }, 'function') compilerOptions.mode = 'function'
]), }
}),
h('label', { for: 'mode-function' }, 'function')
]),
// SSR // SSR
h('input', { h('li', [
type: 'checkbox', h('input', {
id: 'ssr', type: 'checkbox',
name: 'ssr', id: 'ssr',
checked: ssrMode.value, name: 'ssr',
onChange(e: Event) { checked: ssrMode.value,
ssrMode.value = (e.target as HTMLInputElement).checked onChange(e: Event) {
} ssrMode.value = (e.target as HTMLInputElement).checked
}), }
h('label', { for: 'ssr' }, 'SSR'), }),
h('label', { for: 'ssr' }, 'SSR')
]),
// toggle prefixIdentifiers // toggle prefixIdentifiers
h('input', { h('li', [
type: 'checkbox', h('input', {
id: 'prefix', type: 'checkbox',
disabled: isModule || isSSR, id: 'prefix',
checked: usePrefix || isSSR, disabled: isModule || isSSR,
onChange(e: Event) { checked: usePrefix || isSSR,
compilerOptions.prefixIdentifiers = onChange(e: Event) {
(e.target as HTMLInputElement).checked || isModule compilerOptions.prefixIdentifiers =
} (e.target as HTMLInputElement).checked || isModule
}), }
h('label', { for: 'prefix' }, 'prefixIdentifiers'), }),
h('label', { for: 'prefix' }, 'prefixIdentifiers')
]),
// toggle hoistStatic // toggle hoistStatic
h('input', { h('li', [
type: 'checkbox', h('input', {
id: 'hoist', type: 'checkbox',
checked: compilerOptions.hoistStatic && !isSSR, id: 'hoist',
disabled: isSSR, checked: compilerOptions.hoistStatic && !isSSR,
onChange(e: Event) { disabled: isSSR,
compilerOptions.hoistStatic = (e.target as HTMLInputElement).checked onChange(e: Event) {
} compilerOptions.hoistStatic = (e.target as HTMLInputElement).checked
}), }
h('label', { for: 'hoist' }, 'hoistStatic'), }),
h('label', { for: 'hoist' }, 'hoistStatic')
]),
// toggle cacheHandlers // toggle cacheHandlers
h('input', { h('li', [
type: 'checkbox', h('input', {
id: 'cache', type: 'checkbox',
checked: usePrefix && compilerOptions.cacheHandlers && !isSSR, id: 'cache',
disabled: !usePrefix || isSSR, checked: usePrefix && compilerOptions.cacheHandlers && !isSSR,
onChange(e: Event) { disabled: !usePrefix || isSSR,
compilerOptions.cacheHandlers = (e.target as HTMLInputElement).checked onChange(e: Event) {
} compilerOptions.cacheHandlers = (e.target as HTMLInputElement).checked
}), }
h('label', { for: 'cache' }, 'cacheHandlers'), }),
h('label', { for: 'cache' }, 'cacheHandlers')
]),
// toggle scopeId // toggle scopeId
h('input', { h('li', [
type: 'checkbox', h('input', {
id: 'scope-id', type: 'checkbox',
disabled: !isModule, id: 'scope-id',
checked: isModule && compilerOptions.scopeId, disabled: !isModule,
onChange(e: Event) { checked: isModule && compilerOptions.scopeId,
compilerOptions.scopeId = onChange(e: Event) {
isModule && (e.target as HTMLInputElement).checked compilerOptions.scopeId =
? 'scope-id' isModule && (e.target as HTMLInputElement).checked
: null ? 'scope-id'
} : null
}), }
h('label', { for: 'scope-id' }, 'scopeId') }),
h('label', { for: 'scope-id' }, 'scopeId')
]),
// toggle optimizeBindings
h('li', [
h('input', {
type: 'checkbox',
id: 'optimize-bindings',
disabled: !isModule || isSSR,
checked: isModule && !isSSR && compilerOptions.optimizeBindings,
onChange(e: Event) {
compilerOptions.optimizeBindings = (e.target as HTMLInputElement).checked
}
}),
h('label', { for: 'optimize-bindings' }, 'optimizeBindings')
])
])
]) ])
] ]
} }

View File

@ -14,6 +14,7 @@ body {
border-bottom: 1px solid #333; border-bottom: 1px solid #333;
padding: 0.3em 1.6em; padding: 0.3em 1.6em;
color: #fff; color: #fff;
z-index: 1;
} }
h1 { h1 {
@ -22,17 +23,34 @@ h1 {
margin-right: 15px; margin-right: 15px;
} }
#options-wrapper {
position: absolute;
top: 20px;
right: 10px;
}
#options-wrapper:hover #options {
display: block;
}
#options-label {
cursor: pointer;
text-align: right;
padding-right: 10px;
font-weight: bold;
}
#options { #options {
float: right; display: none;
margin-top: 1em; margin-top: 15px;
list-style-type: none;
background-color: #1e1e1e;
border: 1px solid #333;
padding: 15px 30px;
} }
.options-group { #options li {
margin-right: 30px; margin: 8px 0;
}
#header span, #header label, #header input, #header a {
display: inline-block;
} }
#header a { #header a {
@ -45,7 +63,6 @@ h1 {
} }
#header input { #header input {
margin-left: 12px;
margin-right: 6px; margin-right: 6px;
} }