From 51317af6e8405490c4e17694cc3c767ecd58b3d7 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 7 Feb 2020 18:53:39 -0500 Subject: [PATCH] refactor(compiler): prefix all imported helpers to avoid scope collision --- packages/compiler-core/src/codegen.ts | 54 +++-- packages/compiler-core/src/options.ts | 3 + packages/compiler-ssr/src/runtimeHelpers.ts | 26 +-- packages/server-renderer/src/index.ts | 26 +-- packages/template-explorer/src/options.ts | 207 ++++++++++++-------- packages/template-explorer/style.css | 33 +++- 6 files changed, 218 insertions(+), 131 deletions(-) diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 2541c77c64..01e8bfbb87 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -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` ) } diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts index f88b007b56..b729ac3d07 100644 --- a/packages/compiler-core/src/options.ts +++ b/packages/compiler-core/src/options.ts @@ -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 diff --git a/packages/compiler-ssr/src/runtimeHelpers.ts b/packages/compiler-ssr/src/runtimeHelpers.ts index 00865ca656..2f394514de 100644 --- a/packages/compiler-ssr/src/runtimeHelpers.ts +++ b/packages/compiler-ssr/src/runtimeHelpers.ts @@ -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 diff --git a/packages/server-renderer/src/index.ts b/packages/server-renderer/src/index.ts index 2c038acfcd..06a1845632 100644 --- a/packages/server-renderer/src/index.ts +++ b/packages/server-renderer/src/index.ts @@ -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' diff --git a/packages/template-explorer/src/options.ts b/packages/template-explorer/src/options.ts index 9dd21c8014..7a2a09c127 100644 --- a/packages/template-explorer/src/options.ts +++ b/packages/template-explorer/src/options.ts @@ -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-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') + ]), - 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') - ]), + // 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') + ]), - // 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'), + // 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 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 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 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 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 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 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 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 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') + ]) + ]) ]) ] } diff --git a/packages/template-explorer/style.css b/packages/template-explorer/style.css index fca19d04a4..b51eca4b75 100644 --- a/packages/template-explorer/style.css +++ b/packages/template-explorer/style.css @@ -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 { - float: right; - margin-top: 1em; +#options-wrapper { + position: absolute; + top: 20px; + right: 10px; } -.options-group { - margin-right: 30px; +#options-wrapper:hover #options { + display: block; } -#header span, #header label, #header input, #header a { - display: inline-block; +#options-label { + cursor: pointer; + text-align: right; + padding-right: 10px; + font-weight: bold; +} + +#options { + display: none; + margin-top: 15px; + list-style-type: none; + background-color: #1e1e1e; + border: 1px solid #333; + padding: 15px 30px; +} + +#options li { + margin: 8px 0; } #header a { @@ -45,7 +63,6 @@ h1 { } #header input { - margin-left: 12px; margin-right: 6px; } -- 2.47.3