feat(compiler): support keep-alive in templates

This commit is contained in:
Evan You 2019-11-05 10:26:36 -05:00
parent a5f1387d78
commit 98e9b769e6
5 changed files with 136 additions and 82 deletions

View File

@ -8,7 +8,9 @@ import {
TO_HANDLERS, TO_HANDLERS,
helperNameMap, helperNameMap,
PORTAL, PORTAL,
RESOLVE_DYNAMIC_COMPONENT RESOLVE_DYNAMIC_COMPONENT,
SUSPENSE,
KEEP_ALIVE
} from '../../src/runtimeHelpers' } from '../../src/runtimeHelpers'
import { import {
CallExpression, CallExpression,
@ -265,10 +267,13 @@ describe('compiler: element transform', () => {
]) ])
}) })
test('should handle <portal> element', () => { test('should handle <Portal> with normal children', () => {
const { node } = parseWithElementTransform( function assert(tag: string) {
`<portal target="#foo"><span /></portal>` const { root, node } = parseWithElementTransform(
`<${tag} target="#foo"><span /></${tag}>`
) )
expect(root.components.length).toBe(0)
expect(root.helpers).toContain(PORTAL)
expect(node.callee).toBe(CREATE_VNODE) expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments).toMatchObject([ expect(node.arguments).toMatchObject([
PORTAL, PORTAL,
@ -286,29 +291,73 @@ describe('compiler: element transform', () => {
} }
] ]
]) ])
}
assert(`portal`)
assert(`Portal`)
}) })
test('should handle <Portal> element', () => { test('should handle <Suspense>', () => {
const { node } = parseWithElementTransform( function assert(tag: string, content: string, hasFallback?: boolean) {
`<Portal target="#foo"><span /></Portal>` const { root, node } = parseWithElementTransform(
`<${tag}>${content}</${tag}>`
) )
expect(root.components.length).toBe(0)
expect(root.helpers).toContain(SUSPENSE)
expect(node.callee).toBe(CREATE_VNODE) expect(node.callee).toBe(CREATE_VNODE)
expect(node.arguments).toMatchObject([ expect(node.arguments).toMatchObject([
PORTAL, SUSPENSE,
createObjectMatcher({ `null`,
target: '#foo' hasFallback
}), ? createObjectMatcher({
[ default: {
{ type: NodeTypes.JS_FUNCTION_EXPRESSION
type: NodeTypes.ELEMENT, },
tag: 'span', fallback: {
codegenNode: { type: NodeTypes.JS_FUNCTION_EXPRESSION
callee: CREATE_VNODE, },
arguments: [`"span"`] _compiled: `[true]`
} })
} : createObjectMatcher({
] default: {
type: NodeTypes.JS_FUNCTION_EXPRESSION
},
_compiled: `[true]`
})
]) ])
}
assert(`suspense`, `foo`)
assert(`suspense`, `<template #default>foo</template>`)
assert(
`suspense`,
`<template #default>foo</template><template #fallback>fallback</template>`,
true
)
})
test('should handle <KeepAlive>', () => {
function assert(tag: string) {
const { root, node } = parseWithElementTransform(
`<${tag}><span /></${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', () => { test('error on v-bind with no argument', () => {

View File

@ -51,9 +51,7 @@ export const enum ElementTypes {
ELEMENT, ELEMENT,
COMPONENT, COMPONENT,
SLOT, SLOT,
TEMPLATE, TEMPLATE
PORTAL,
SUSPENSE
} }
export interface Node { export interface Node {
@ -105,8 +103,6 @@ export type ElementNode =
| ComponentNode | ComponentNode
| SlotOutletNode | SlotOutletNode
| TemplateNode | TemplateNode
| PortalNode
| SuspenseNode
export interface BaseElementNode extends Node { export interface BaseElementNode extends Node {
type: NodeTypes.ELEMENT type: NodeTypes.ELEMENT
@ -147,16 +143,6 @@ export interface TemplateNode extends BaseElementNode {
codegenNode: ElementCodegenNode | undefined | CacheExpression 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 { export interface TextNode extends Node {
type: NodeTypes.TEXT type: NodeTypes.TEXT
content: string content: string

View File

@ -1,4 +1,4 @@
import { NO } from '@vue/shared' import { NO, makeMap } from '@vue/shared'
import { import {
ErrorCodes, ErrorCodes,
createCompilerError, createCompilerError,
@ -29,6 +29,12 @@ import {
} from './ast' } from './ast'
import { extend } from '@vue/shared' 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 { export interface ParserOptions {
isVoidTag?: (tag: string) => boolean // e.g. img, br, hr isVoidTag?: (tag: string) => boolean // e.g. img, br, hr
isNativeTag?: (tag: string) => boolean // e.g. loading-indicator in weex 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.inPre && !context.options.isCustomElement(tag)) {
if (context.options.isNativeTag) { if (context.options.isNativeTag) {
if (!context.options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT if (!context.options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT
} else { } else if (isBuiltInComponent(tag) || /^[A-Z]/.test(tag)) {
if (/^[A-Z]/.test(tag)) tagType = ElementTypes.COMPONENT tagType = ElementTypes.COMPONENT
} }
if (tag === 'slot') tagType = ElementTypes.SLOT if (tag === 'slot') {
else if (tag === 'template') tagType = ElementTypes.TEMPLATE tagType = ElementTypes.SLOT
else if (tag === 'portal' || tag === 'Portal') tagType = ElementTypes.PORTAL } else if (tag === 'template') {
else if (tag === 'suspense' || tag === 'Suspense') tagType = ElementTypes.TEMPLATE
tagType = ElementTypes.SUSPENSE }
} }
return { return {

View File

@ -1,6 +1,7 @@
export const FRAGMENT = Symbol(__DEV__ ? `Fragment` : ``) export const FRAGMENT = Symbol(__DEV__ ? `Fragment` : ``)
export const PORTAL = Symbol(__DEV__ ? `Portal` : ``) export const PORTAL = Symbol(__DEV__ ? `Portal` : ``)
export const SUSPENSE = Symbol(__DEV__ ? `Suspense` : ``) export const SUSPENSE = Symbol(__DEV__ ? `Suspense` : ``)
export const KEEP_ALIVE = Symbol(__DEV__ ? `KeepAlive` : ``)
export const OPEN_BLOCK = Symbol(__DEV__ ? `openBlock` : ``) export const OPEN_BLOCK = Symbol(__DEV__ ? `openBlock` : ``)
export const CREATE_BLOCK = Symbol(__DEV__ ? `createBlock` : ``) export const CREATE_BLOCK = Symbol(__DEV__ ? `createBlock` : ``)
export const CREATE_VNODE = Symbol(__DEV__ ? `createVNode` : ``) export const CREATE_VNODE = Symbol(__DEV__ ? `createVNode` : ``)
@ -28,6 +29,7 @@ export const helperNameMap: any = {
[FRAGMENT]: `Fragment`, [FRAGMENT]: `Fragment`,
[PORTAL]: `Portal`, [PORTAL]: `Portal`,
[SUSPENSE]: `Suspense`, [SUSPENSE]: `Suspense`,
[KEEP_ALIVE]: `KeepAlive`,
[OPEN_BLOCK]: `openBlock`, [OPEN_BLOCK]: `openBlock`,
[CREATE_BLOCK]: `createBlock`, [CREATE_BLOCK]: `createBlock`,
[CREATE_VNODE]: `createVNode`, [CREATE_VNODE]: `createVNode`,

View File

@ -26,7 +26,8 @@ import {
MERGE_PROPS, MERGE_PROPS,
TO_HANDLERS, TO_HANDLERS,
PORTAL, PORTAL,
SUSPENSE SUSPENSE,
KEEP_ALIVE
} from '../runtimeHelpers' } from '../runtimeHelpers'
import { getInnerRange, isVSlot, toValidAssetId, findProp } from '../utils' import { getInnerRange, isVSlot, toValidAssetId, findProp } from '../utils'
import { buildSlots } from './vSlot' 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 // perform the work on exit, after all child expressions have been
// processed and merged. // processed and merged.
return () => { return () => {
const isComponent = node.tagType === ElementTypes.COMPONENT const { tag, tagType, props } = node
let hasProps = node.props.length > 0 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 patchFlag: number = 0
let runtimeDirectives: DirectiveNode[] | undefined let runtimeDirectives: DirectiveNode[] | undefined
let dynamicPropNames: string[] | undefined let dynamicPropNames: string[] | undefined
@ -60,7 +66,7 @@ export const transformElement: NodeTransform = (node, context) => {
// handle dynamic component // handle dynamic component
const isProp = findProp(node, 'is') const isProp = findProp(node, 'is')
if (node.tag === 'component') { if (tag === 'component') {
if (isProp) { if (isProp) {
// static <component is="foo" /> // static <component is="foo" />
if (isProp.type === NodeTypes.ATTRIBUTE) { 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.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'] = [ const args: CallExpression['arguments'] = [nodeType]
dynamicComponent
? dynamicComponent
: isComponent
? toValidAssetId(node.tag, `component`)
: node.tagType === ElementTypes.PORTAL
? context.helper(PORTAL)
: node.tagType === ElementTypes.SUSPENSE
? context.helper(SUSPENSE)
: `"${node.tag}"`
]
// props // props
if (hasProps) { if (hasProps) {
const propsBuildResult = buildProps( const propsBuildResult = buildProps(
@ -120,7 +130,8 @@ export const transformElement: NodeTransform = (node, context) => {
if (!hasProps) { if (!hasProps) {
args.push(`null`) 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) const { slots, hasDynamicSlots } = buildSlots(node, context)
args.push(slots) args.push(slots)
if (hasDynamicSlots) { if (hasDynamicSlots) {