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,
filename = `template.vue.html`,
scopeId = null,
optimizeBindings = false,
runtimeGlobalName = `Vue`,
runtimeModuleName = `vue`,
ssr = false
@ -91,6 +92,7 @@ function createCodegenContext(
sourceMap,
filename,
scopeId,
optimizeBindings,
runtimeGlobalName,
runtimeModuleName,
ssr,
@ -102,8 +104,7 @@ function createCodegenContext(
indentLevel: 0,
map: undefined,
helper(key) {
const name = helperNameMap[key]
return prefixIdentifiers ? name : `_${name}`
return `_${helperNameMap[key]}`
},
push(code, node) {
context.code += code
@ -282,7 +283,6 @@ export function generate(
function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
const {
ssr,
helper,
prefixIdentifiers,
push,
newline,
@ -293,13 +293,16 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
!__BROWSER__ && ssr
? `require(${JSON.stringify(runtimeModuleName)})`
: runtimeGlobalName
const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
// Generate const declaration for helpers
// 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
// with block so it doesn't incur the `in` check cost for every helper access.
if (ast.helpers.length > 0) {
if (!__BROWSER__ && prefixIdentifiers) {
push(`const { ${ast.helpers.map(helper).join(', ')} } = ${VueBinding}\n`)
push(
`const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n`
)
} else {
// "with" mode.
// save Vue in a separate variable to avoid collision
@ -310,7 +313,7 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
if (ast.hoists.length) {
const staticHelpers = [CREATE_VNODE, CREATE_COMMENT, CREATE_TEXT]
.filter(helper => ast.helpers.includes(helper))
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
.map(aliasHelper)
.join(', ')
push(`const { ${staticHelpers} } = _Vue\n`)
}
@ -321,7 +324,7 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
// ssr guaruntees prefixIdentifier: true
push(
`const { ${ast.ssrHelpers
.map(helper)
.map(aliasHelper)
.join(', ')} } = require("@vue/server-renderer")\n`
)
}
@ -335,7 +338,14 @@ function genModulePreamble(
context: CodegenContext,
genScopeId: boolean
) {
const { push, helper, newline, scopeId, runtimeModuleName } = context
const {
push,
helper,
newline,
scopeId,
optimizeBindings,
runtimeModuleName
} = context
if (genScopeId) {
ast.helpers.push(WITH_SCOPE_ID)
@ -346,17 +356,35 @@ function genModulePreamble(
// generate import statements for helpers
if (ast.helpers.length) {
push(
`import { ${ast.helpers.map(helper).join(', ')} } from ${JSON.stringify(
runtimeModuleName
)}\n`
)
if (optimizeBindings) {
// when bundled with webpack with code-split, calling an import binding
// as a function leads to it being wrapped with `Object(a.b)` or `(0,a.b)`,
// 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) {
push(
`import { ${ast.ssrHelpers
.map(helper)
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from "@vue/server-renderer"\n`
)
}

View File

@ -73,6 +73,9 @@ export interface CodegenOptions {
scopeId?: string | null
// we need to know about this to generate proper preambles
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
runtimeModuleName?: 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 ssrHelpers = {
[SSR_INTERPOLATE]: `_ssrInterpolate`,
[SSR_RENDER_COMPONENT]: `_ssrRenderComponent`,
[SSR_RENDER_SLOT]: `_ssrRenderSlot`,
[SSR_RENDER_CLASS]: `_ssrRenderClass`,
[SSR_RENDER_STYLE]: `_ssrRenderStyle`,
[SSR_RENDER_ATTRS]: `_ssrRenderAttrs`,
[SSR_RENDER_ATTR]: `_ssrRenderAttr`,
[SSR_RENDER_DYNAMIC_ATTR]: `_ssrRenderDynamicAttr`,
[SSR_RENDER_LIST]: `_ssrRenderList`,
[SSR_LOOSE_EQUAL]: `_ssrLooseEqual`,
[SSR_LOOSE_CONTAIN]: `_ssrLooseContain`,
[SSR_RENDER_DYNAMIC_MODEL]: `_ssrRenderDynamicModel`,
[SSR_GET_DYNAMIC_MODEL_PROPS]: `_ssrGetDynamicModelProps`
[SSR_INTERPOLATE]: `ssrInterpolate`,
[SSR_RENDER_COMPONENT]: `ssrRenderComponent`,
[SSR_RENDER_SLOT]: `ssrRenderSlot`,
[SSR_RENDER_CLASS]: `ssrRenderClass`,
[SSR_RENDER_STYLE]: `ssrRenderStyle`,
[SSR_RENDER_ATTRS]: `ssrRenderAttrs`,
[SSR_RENDER_ATTR]: `ssrRenderAttr`,
[SSR_RENDER_DYNAMIC_ATTR]: `ssrRenderDynamicAttr`,
[SSR_RENDER_LIST]: `ssrRenderList`,
[SSR_LOOSE_EQUAL]: `ssrLooseEqual`,
[SSR_LOOSE_CONTAIN]: `ssrLooseContain`,
[SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`,
[SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps`
}
// Note: these are helpers imported from @vue/server-renderer

View File

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

View File

@ -6,6 +6,7 @@ export const ssrMode = ref(false)
export const compilerOptions: CompilerOptions = reactive({
mode: 'module',
prefixIdentifiers: false,
optimizeBindings: false,
hoistStatic: false,
cacheHandlers: false,
scopeId: null
@ -29,96 +30,134 @@ const App = {
},
`@${__COMMIT__}`
),
' | ',
h(
'a',
{
href:
'https://app.netlify.com/sites/vue-next-template-explorer/deploys',
target: `_blank`
},
'History'
),
h('div', { id: 'options' }, [
// mode selection
h('span', { class: 'options-group' }, [
h('span', { class: 'label' }, 'Mode:'),
h('input', {
type: 'radio',
id: 'mode-module',
name: 'mode',
checked: isModule,
onChange() {
compilerOptions.mode = 'module'
}
}),
h('label', { for: 'mode-module' }, 'module'),
h('input', {
type: 'radio',
id: 'mode-function',
name: 'mode',
checked: !isModule,
onChange() {
compilerOptions.mode = 'function'
}
}),
h('label', { for: 'mode-function' }, 'function')
]),
h('div', { id: 'options-wrapper' }, [
h('div', { id: 'options-label' }, 'Options ↘'),
h('ul', { id: 'options' }, [
// mode selection
h('li', { id: 'mode' }, [
h('span', { class: 'label' }, 'Mode: '),
h('input', {
type: 'radio',
id: 'mode-module',
name: 'mode',
checked: isModule,
onChange() {
compilerOptions.mode = 'module'
}
}),
h('label', { for: 'mode-module' }, 'module'),
' ',
h('input', {
type: 'radio',
id: 'mode-function',
name: 'mode',
checked: !isModule,
onChange() {
compilerOptions.mode = 'function'
}
}),
h('label', { for: 'mode-function' }, 'function')
]),
// SSR
h('input', {
type: 'checkbox',
id: 'ssr',
name: 'ssr',
checked: ssrMode.value,
onChange(e: Event) {
ssrMode.value = (e.target as HTMLInputElement).checked
}
}),
h('label', { for: 'ssr' }, 'SSR'),
// SSR
h('li', [
h('input', {
type: 'checkbox',
id: 'ssr',
name: 'ssr',
checked: ssrMode.value,
onChange(e: Event) {
ssrMode.value = (e.target as HTMLInputElement).checked
}
}),
h('label', { for: 'ssr' }, 'SSR')
]),
// toggle prefixIdentifiers
h('input', {
type: 'checkbox',
id: 'prefix',
disabled: isModule || isSSR,
checked: usePrefix || isSSR,
onChange(e: Event) {
compilerOptions.prefixIdentifiers =
(e.target as HTMLInputElement).checked || isModule
}
}),
h('label', { for: 'prefix' }, 'prefixIdentifiers'),
// toggle prefixIdentifiers
h('li', [
h('input', {
type: 'checkbox',
id: 'prefix',
disabled: isModule || isSSR,
checked: usePrefix || isSSR,
onChange(e: Event) {
compilerOptions.prefixIdentifiers =
(e.target as HTMLInputElement).checked || isModule
}
}),
h('label', { for: 'prefix' }, 'prefixIdentifiers')
]),
// toggle hoistStatic
h('input', {
type: 'checkbox',
id: 'hoist',
checked: compilerOptions.hoistStatic && !isSSR,
disabled: isSSR,
onChange(e: Event) {
compilerOptions.hoistStatic = (e.target as HTMLInputElement).checked
}
}),
h('label', { for: 'hoist' }, 'hoistStatic'),
// toggle hoistStatic
h('li', [
h('input', {
type: 'checkbox',
id: 'hoist',
checked: compilerOptions.hoistStatic && !isSSR,
disabled: isSSR,
onChange(e: Event) {
compilerOptions.hoistStatic = (e.target as HTMLInputElement).checked
}
}),
h('label', { for: 'hoist' }, 'hoistStatic')
]),
// toggle cacheHandlers
h('input', {
type: 'checkbox',
id: 'cache',
checked: usePrefix && compilerOptions.cacheHandlers && !isSSR,
disabled: !usePrefix || isSSR,
onChange(e: Event) {
compilerOptions.cacheHandlers = (e.target as HTMLInputElement).checked
}
}),
h('label', { for: 'cache' }, 'cacheHandlers'),
// toggle cacheHandlers
h('li', [
h('input', {
type: 'checkbox',
id: 'cache',
checked: usePrefix && compilerOptions.cacheHandlers && !isSSR,
disabled: !usePrefix || isSSR,
onChange(e: Event) {
compilerOptions.cacheHandlers = (e.target as HTMLInputElement).checked
}
}),
h('label', { for: 'cache' }, 'cacheHandlers')
]),
// toggle scopeId
h('input', {
type: 'checkbox',
id: 'scope-id',
disabled: !isModule,
checked: isModule && compilerOptions.scopeId,
onChange(e: Event) {
compilerOptions.scopeId =
isModule && (e.target as HTMLInputElement).checked
? 'scope-id'
: null
}
}),
h('label', { for: 'scope-id' }, 'scopeId')
// toggle scopeId
h('li', [
h('input', {
type: 'checkbox',
id: 'scope-id',
disabled: !isModule,
checked: isModule && compilerOptions.scopeId,
onChange(e: Event) {
compilerOptions.scopeId =
isModule && (e.target as HTMLInputElement).checked
? 'scope-id'
: null
}
}),
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;
padding: 0.3em 1.6em;
color: #fff;
z-index: 1;
}
h1 {
@ -22,17 +23,34 @@ h1 {
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 {
float: right;
margin-top: 1em;
display: none;
margin-top: 15px;
list-style-type: none;
background-color: #1e1e1e;
border: 1px solid #333;
padding: 15px 30px;
}
.options-group {
margin-right: 30px;
}
#header span, #header label, #header input, #header a {
display: inline-block;
#options li {
margin: 8px 0;
}
#header a {
@ -45,7 +63,6 @@ h1 {
}
#header input {
margin-left: 12px;
margin-right: 6px;
}