From 98e9b769e6528a303b06f7e52a9544b3d057f0a6 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 5 Nov 2019 10:26:36 -0500 Subject: [PATCH] feat(compiler): support keep-alive in templates --- .../transforms/transformElement.spec.ts | 131 ++++++++++++------ packages/compiler-core/src/ast.ts | 16 +-- packages/compiler-core/src/parse.ts | 22 +-- packages/compiler-core/src/runtimeHelpers.ts | 2 + .../src/transforms/transformElement.ts | 47 ++++--- 5 files changed, 136 insertions(+), 82 deletions(-) diff --git a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts index 06ed14e5..00f5274a 100644 --- a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts @@ -8,7 +8,9 @@ import { TO_HANDLERS, helperNameMap, PORTAL, - RESOLVE_DYNAMIC_COMPONENT + RESOLVE_DYNAMIC_COMPONENT, + SUSPENSE, + KEEP_ALIVE } from '../../src/runtimeHelpers' import { CallExpression, @@ -265,50 +267,97 @@ describe('compiler: element transform', () => { ]) }) - test('should handle element', () => { - const { node } = parseWithElementTransform( - `` - ) - expect(node.callee).toBe(CREATE_VNODE) - expect(node.arguments).toMatchObject([ - PORTAL, - createObjectMatcher({ - target: '#foo' - }), - [ - { - type: NodeTypes.ELEMENT, - tag: 'span', - codegenNode: { - callee: CREATE_VNODE, - arguments: [`"span"`] + test('should handle with normal children', () => { + function assert(tag: string) { + const { root, node } = parseWithElementTransform( + `<${tag} target="#foo">` + ) + expect(root.components.length).toBe(0) + expect(root.helpers).toContain(PORTAL) + expect(node.callee).toBe(CREATE_VNODE) + expect(node.arguments).toMatchObject([ + PORTAL, + createObjectMatcher({ + target: '#foo' + }), + [ + { + type: NodeTypes.ELEMENT, + tag: 'span', + codegenNode: { + callee: CREATE_VNODE, + arguments: [`"span"`] + } } - } - ] - ]) + ] + ]) + } + + assert(`portal`) + assert(`Portal`) }) - test('should handle element', () => { - const { node } = parseWithElementTransform( - `` + test('should handle ', () => { + function assert(tag: string, content: string, hasFallback?: boolean) { + const { root, node } = parseWithElementTransform( + `<${tag}>${content}` + ) + expect(root.components.length).toBe(0) + expect(root.helpers).toContain(SUSPENSE) + expect(node.callee).toBe(CREATE_VNODE) + expect(node.arguments).toMatchObject([ + SUSPENSE, + `null`, + hasFallback + ? createObjectMatcher({ + default: { + type: NodeTypes.JS_FUNCTION_EXPRESSION + }, + fallback: { + type: NodeTypes.JS_FUNCTION_EXPRESSION + }, + _compiled: `[true]` + }) + : createObjectMatcher({ + default: { + type: NodeTypes.JS_FUNCTION_EXPRESSION + }, + _compiled: `[true]` + }) + ]) + } + + assert(`suspense`, `foo`) + assert(`suspense`, ``) + assert( + `suspense`, + ``, + true ) - expect(node.callee).toBe(CREATE_VNODE) - expect(node.arguments).toMatchObject([ - PORTAL, - createObjectMatcher({ - target: '#foo' - }), - [ - { - type: NodeTypes.ELEMENT, - tag: 'span', - codegenNode: { - callee: CREATE_VNODE, - arguments: [`"span"`] - } - } - ] - ]) + }) + + test('should handle ', () => { + function assert(tag: string) { + const { root, node } = parseWithElementTransform( + `<${tag}>` + ) + expect(root.components.length).toBe(0) + expect(root.helpers).toContain(KEEP_ALIVE) + expect(node.callee).toBe(CREATE_VNODE) + expect(node.arguments).toMatchObject([ + KEEP_ALIVE, + `null`, + createObjectMatcher({ + default: { + type: NodeTypes.JS_FUNCTION_EXPRESSION + }, + _compiled: `[true]` + }) + ]) + } + + assert(`keep-alive`) + assert(`KeepAlive`) }) test('error on v-bind with no argument', () => { diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 9ba06608..ebd7a605 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -51,9 +51,7 @@ export const enum ElementTypes { ELEMENT, COMPONENT, SLOT, - TEMPLATE, - PORTAL, - SUSPENSE + TEMPLATE } export interface Node { @@ -105,8 +103,6 @@ export type ElementNode = | ComponentNode | SlotOutletNode | TemplateNode - | PortalNode - | SuspenseNode export interface BaseElementNode extends Node { type: NodeTypes.ELEMENT @@ -147,16 +143,6 @@ export interface TemplateNode extends BaseElementNode { codegenNode: ElementCodegenNode | undefined | CacheExpression } -export interface PortalNode extends BaseElementNode { - tagType: ElementTypes.PORTAL - codegenNode: ElementCodegenNode | undefined | CacheExpression -} - -export interface SuspenseNode extends BaseElementNode { - tagType: ElementTypes.SUSPENSE - codegenNode: ElementCodegenNode | undefined | CacheExpression -} - export interface TextNode extends Node { type: NodeTypes.TEXT content: string diff --git a/packages/compiler-core/src/parse.ts b/packages/compiler-core/src/parse.ts index 4b2cec42..57f21680 100644 --- a/packages/compiler-core/src/parse.ts +++ b/packages/compiler-core/src/parse.ts @@ -1,4 +1,4 @@ -import { NO } from '@vue/shared' +import { NO, makeMap } from '@vue/shared' import { ErrorCodes, createCompilerError, @@ -29,6 +29,12 @@ import { } from './ast' import { extend } from '@vue/shared' +// Portal and Fragment are native types, not components +const isBuiltInComponent = /*#__PURE__*/ makeMap( + `suspense,keep-alive,keepalive,transition`, + true +) + export interface ParserOptions { isVoidTag?: (tag: string) => boolean // e.g. img, br, hr isNativeTag?: (tag: string) => boolean // e.g. loading-indicator in weex @@ -467,15 +473,15 @@ function parseTag( if (!context.inPre && !context.options.isCustomElement(tag)) { if (context.options.isNativeTag) { if (!context.options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT - } else { - if (/^[A-Z]/.test(tag)) tagType = ElementTypes.COMPONENT + } else if (isBuiltInComponent(tag) || /^[A-Z]/.test(tag)) { + tagType = ElementTypes.COMPONENT } - if (tag === 'slot') tagType = ElementTypes.SLOT - else if (tag === 'template') tagType = ElementTypes.TEMPLATE - else if (tag === 'portal' || tag === 'Portal') tagType = ElementTypes.PORTAL - else if (tag === 'suspense' || tag === 'Suspense') - tagType = ElementTypes.SUSPENSE + if (tag === 'slot') { + tagType = ElementTypes.SLOT + } else if (tag === 'template') { + tagType = ElementTypes.TEMPLATE + } } return { diff --git a/packages/compiler-core/src/runtimeHelpers.ts b/packages/compiler-core/src/runtimeHelpers.ts index f6963168..a6d679ef 100644 --- a/packages/compiler-core/src/runtimeHelpers.ts +++ b/packages/compiler-core/src/runtimeHelpers.ts @@ -1,6 +1,7 @@ export const FRAGMENT = Symbol(__DEV__ ? `Fragment` : ``) export const PORTAL = Symbol(__DEV__ ? `Portal` : ``) export const SUSPENSE = Symbol(__DEV__ ? `Suspense` : ``) +export const KEEP_ALIVE = Symbol(__DEV__ ? `KeepAlive` : ``) export const OPEN_BLOCK = Symbol(__DEV__ ? `openBlock` : ``) export const CREATE_BLOCK = Symbol(__DEV__ ? `createBlock` : ``) export const CREATE_VNODE = Symbol(__DEV__ ? `createVNode` : ``) @@ -28,6 +29,7 @@ export const helperNameMap: any = { [FRAGMENT]: `Fragment`, [PORTAL]: `Portal`, [SUSPENSE]: `Suspense`, + [KEEP_ALIVE]: `KeepAlive`, [OPEN_BLOCK]: `openBlock`, [CREATE_BLOCK]: `createBlock`, [CREATE_VNODE]: `createVNode`, diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index f348c8ce..909b460f 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -26,7 +26,8 @@ import { MERGE_PROPS, TO_HANDLERS, PORTAL, - SUSPENSE + SUSPENSE, + KEEP_ALIVE } from '../runtimeHelpers' import { getInnerRange, isVSlot, toValidAssetId, findProp } from '../utils' import { buildSlots } from './vSlot' @@ -51,8 +52,13 @@ export const transformElement: NodeTransform = (node, context) => { // perform the work on exit, after all child expressions have been // processed and merged. return () => { - const isComponent = node.tagType === ElementTypes.COMPONENT - let hasProps = node.props.length > 0 + const { tag, tagType, props } = node + const isPortal = tag === 'portal' || tag === 'Portal' + const isSuspense = tag === 'suspense' || tag === 'Suspense' + const isKeepAlive = tag === 'keep-alive' || tag === 'KeepAlive' + const isComponent = tagType === ElementTypes.COMPONENT + + let hasProps = props.length > 0 let patchFlag: number = 0 let runtimeDirectives: DirectiveNode[] | undefined let dynamicPropNames: string[] | undefined @@ -60,7 +66,7 @@ export const transformElement: NodeTransform = (node, context) => { // handle dynamic component const isProp = findProp(node, 'is') - if (node.tag === 'component') { + if (tag === 'component') { if (isProp) { // static if (isProp.type === NodeTypes.ATTRIBUTE) { @@ -81,22 +87,26 @@ export const transformElement: NodeTransform = (node, context) => { } } - if (isComponent && !dynamicComponent) { + let nodeType + if (dynamicComponent) { + nodeType = dynamicComponent + } else if (isPortal) { + nodeType = context.helper(PORTAL) + } else if (isSuspense) { + nodeType = context.helper(SUSPENSE) + } else if (isKeepAlive) { + nodeType = context.helper(KEEP_ALIVE) + } else if (isComponent) { + // user component w/ resolve context.helper(RESOLVE_COMPONENT) - context.components.add(node.tag) + context.components.add(tag) + nodeType = toValidAssetId(tag, `component`) + } else { + // plain element + nodeType = `"${node.tag}"` } - const args: CallExpression['arguments'] = [ - dynamicComponent - ? dynamicComponent - : isComponent - ? toValidAssetId(node.tag, `component`) - : node.tagType === ElementTypes.PORTAL - ? context.helper(PORTAL) - : node.tagType === ElementTypes.SUSPENSE - ? context.helper(SUSPENSE) - : `"${node.tag}"` - ] + const args: CallExpression['arguments'] = [nodeType] // props if (hasProps) { const propsBuildResult = buildProps( @@ -120,7 +130,8 @@ export const transformElement: NodeTransform = (node, context) => { if (!hasProps) { args.push(`null`) } - if (isComponent || node.tagType === ElementTypes.SUSPENSE) { + // Portal should have normal children instead of slots + if (isComponent && !isPortal) { const { slots, hasDynamicSlots } = buildSlots(node, context) args.push(slots) if (hasDynamicSlots) {