feat(compiler): support keep-alive in templates
This commit is contained in:
parent
a5f1387d78
commit
98e9b769e6
@ -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,50 +267,97 @@ 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(node.callee).toBe(CREATE_VNODE)
|
)
|
||||||
expect(node.arguments).toMatchObject([
|
expect(root.components.length).toBe(0)
|
||||||
PORTAL,
|
expect(root.helpers).toContain(PORTAL)
|
||||||
createObjectMatcher({
|
expect(node.callee).toBe(CREATE_VNODE)
|
||||||
target: '#foo'
|
expect(node.arguments).toMatchObject([
|
||||||
}),
|
PORTAL,
|
||||||
[
|
createObjectMatcher({
|
||||||
{
|
target: '#foo'
|
||||||
type: NodeTypes.ELEMENT,
|
}),
|
||||||
tag: 'span',
|
[
|
||||||
codegenNode: {
|
{
|
||||||
callee: CREATE_VNODE,
|
type: NodeTypes.ELEMENT,
|
||||||
arguments: [`"span"`]
|
tag: 'span',
|
||||||
|
codegenNode: {
|
||||||
|
callee: CREATE_VNODE,
|
||||||
|
arguments: [`"span"`]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
]
|
])
|
||||||
])
|
}
|
||||||
|
|
||||||
|
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.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`, `<template #default>foo</template>`)
|
||||||
|
assert(
|
||||||
|
`suspense`,
|
||||||
|
`<template #default>foo</template><template #fallback>fallback</template>`,
|
||||||
|
true
|
||||||
)
|
)
|
||||||
expect(node.callee).toBe(CREATE_VNODE)
|
})
|
||||||
expect(node.arguments).toMatchObject([
|
|
||||||
PORTAL,
|
test('should handle <KeepAlive>', () => {
|
||||||
createObjectMatcher({
|
function assert(tag: string) {
|
||||||
target: '#foo'
|
const { root, node } = parseWithElementTransform(
|
||||||
}),
|
`<${tag}><span /></${tag}>`
|
||||||
[
|
)
|
||||||
{
|
expect(root.components.length).toBe(0)
|
||||||
type: NodeTypes.ELEMENT,
|
expect(root.helpers).toContain(KEEP_ALIVE)
|
||||||
tag: 'span',
|
expect(node.callee).toBe(CREATE_VNODE)
|
||||||
codegenNode: {
|
expect(node.arguments).toMatchObject([
|
||||||
callee: CREATE_VNODE,
|
KEEP_ALIVE,
|
||||||
arguments: [`"span"`]
|
`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', () => {
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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`,
|
||||||
|
@ -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) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user