wip: vdom interop

This commit is contained in:
daiwei 2025-04-29 10:15:35 +08:00
parent d281d62312
commit ea34f2f555
7 changed files with 92 additions and 66 deletions

View File

@ -33,7 +33,6 @@ import type { NormalizedPropsOptions } from './componentProps'
import type { ObjectEmitsOptions } from './componentEmits'
import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
import type { DefineComponent } from './apiDefineComponent'
import type { createHydrationFunctions } from './hydration'
export interface App<HostElement = any> {
version: string
@ -105,7 +104,6 @@ export interface App<HostElement = any> {
_container: HostElement | null
_context: AppContext
_instance: GenericComponentInstance | null
_ssr?: boolean
/**
* @internal custom element vnode
@ -206,7 +204,6 @@ export interface VaporInteropInterface {
parentComponent: any, // VaporComponentInstance
fallback?: any, // VaporSlot
) => any
vdomHydrate: ReturnType<typeof createHydrationFunctions>[1] | undefined
}
/**

View File

@ -149,7 +149,6 @@ export const createApp = ((...args) => {
export const createSSRApp = ((...args) => {
const app = ensureHydrationRenderer().createApp(...args)
app._ssr = true
if (__DEV__) {
injectNativeTagCheck(app)

View File

@ -54,6 +54,14 @@ function compile(
)
}
async function testHydrationInterop(
code: string,
components?: Record<string, string | { code: string; vapor: boolean }>,
data?: any,
) {
return testHydration(code, components, data, { interop: true, vapor: false })
}
async function testHydration(
code: string,
components: Record<string, string | { code: string; vapor: boolean }> = {},
@ -65,7 +73,7 @@ async function testHydration(
for (const key in components) {
const comp = components[key]
const code = isString(comp) ? comp : comp.code
const isVaporComp = !isString(comp) ? comp.vapor : true
const isVaporComp = isString(comp) || !!comp.vapor
clientComponents[key] = compile(code, data, clientComponents, {
vapor: isVaporComp,
ssr: false,
@ -3838,9 +3846,9 @@ describe('Vapor Mode hydration', () => {
})
describe('VDOM hydration interop', () => {
test('basic component', async () => {
test('basic vapor component', async () => {
const data = ref(true)
const { container } = await testHydration(
const { container } = await testHydrationInterop(
`<script setup>const data = _data; const components = _components;</script>
<template>
<components.VaporChild/>
@ -3852,7 +3860,6 @@ describe('VDOM hydration interop', () => {
},
},
data,
{ interop: true, vapor: false },
)
expect(container.innerHTML).toMatchInlineSnapshot(`"true"`)
@ -3864,7 +3871,7 @@ describe('VDOM hydration interop', () => {
test('nested components (VDOM -> Vapor -> VDOM)', async () => {
const data = ref(true)
const { container } = await testHydration(
const { container } = await testHydrationInterop(
`<script setup>const data = _data; const components = _components;</script>
<template>
<components.VaporChild/>
@ -3881,7 +3888,6 @@ describe('VDOM hydration interop', () => {
},
},
data,
{ interop: true, vapor: false },
)
expect(container.innerHTML).toMatchInlineSnapshot(`"true"`)
@ -3891,9 +3897,9 @@ describe('VDOM hydration interop', () => {
expect(container.innerHTML).toMatchInlineSnapshot(`"false"`)
})
test.todo('slots', async () => {
test('vapor slot render vdom component', async () => {
const data = ref(true)
const { container } = await testHydration(
const { container } = await testHydrationInterop(
`<script setup>const data = _data; const components = _components;</script>
<template>
<components.VaporChild>
@ -3912,7 +3918,6 @@ describe('VDOM hydration interop', () => {
},
},
data,
{ interop: true, vapor: false },
)
expect(container.innerHTML).toMatchInlineSnapshot(

View File

@ -154,7 +154,6 @@ export function insert(
} else {
// fragment
if (block.insert) {
// TODO handle hydration for vdom interop
block.insert(parent, anchor)
} else {
insert(block.nodes, parent, anchor)

View File

@ -58,11 +58,7 @@ import {
getSlot,
} from './componentSlots'
import { hmrReload, hmrRerender } from './hmr'
import {
currentHydrationNode,
isHydrating,
locateHydrationNode,
} from './dom/hydration'
import { isHydrating, locateHydrationNode } from './dom/hydration'
import {
insertionAnchor,
insertionParent,
@ -156,22 +152,15 @@ export function createComponent(
// vdom interop enabled and component is not an explicit vapor component
if (appContext.vapor && !component.__vapor) {
const [frag, vnode] = appContext.vapor.vdomMount(
const frag = appContext.vapor.vdomMount(
component as any,
rawProps,
rawSlots,
)
if (!isHydrating && _insertionParent) {
// `frag.insert` handles both hydration and mounting
if (_insertionParent) {
insert(frag, _insertionParent, _insertionAnchor)
} else if (isHydrating) {
appContext.vapor.vdomHydrate!(
currentHydrationNode!,
vnode,
currentInstance as any,
null,
null,
false,
)
}
return frag
}

View File

@ -114,7 +114,6 @@ export function createSlot(
: EMPTY_OBJ
let fragment: DynamicFragment
if (isRef(rawSlots._)) {
fragment = instance.appContext.vapor!.vdomSlot(
rawSlots._,
@ -157,7 +156,12 @@ export function createSlot(
}
}
if (!isHydrating && _insertionParent) {
if (
_insertionParent &&
(!isHydrating ||
// for vdom interop fragment, `fragment.insert` handles both hydration and mounting
fragment.insert)
) {
insert(fragment, _insertionParent, _insertionAnchor)
}

View File

@ -35,12 +35,17 @@ import type { RawSlots, VaporSlot } from './componentSlots'
import { renderEffect } from './renderEffect'
import { createTextNode } from './dom/node'
import { optimizePropertyLookup } from './dom/prop'
import { hydrateNode as vaporHydrateNode } from './dom/hydration'
import {
currentHydrationNode,
isHydrating,
locateHydrationNode,
hydrateNode as vaporHydrateNode,
} from './dom/hydration'
// mounting vapor components and slots in vdom
const vaporInteropImpl: Omit<
VaporInteropInterface,
'vdomMount' | 'vdomUnmount' | 'vdomSlot' | 'vdomHydrate'
'vdomMount' | 'vdomUnmount' | 'vdomSlot'
> = {
mount(vnode, container, anchor, parentComponent) {
const selfAnchor = (vnode.el = vnode.anchor = createTextNode())
@ -144,6 +149,8 @@ const vaporSlotsProxyHandler: ProxyHandler<any> = {
},
}
let vdomHydrateNode: HydrationRenderer['hydrateNode'] | undefined
/**
* Mount vdom component in vapor
*/
@ -152,7 +159,7 @@ function createVDOMComponent(
component: ConcreteComponent,
rawProps?: LooseRawProps | null,
rawSlots?: LooseRawSlots | null,
): [VaporFragment, VNode] {
): VaporFragment {
const frag = new VaporFragment([])
const vnode = createVNode(
component,
@ -181,16 +188,30 @@ function createVDOMComponent(
}
frag.insert = (parentNode, anchor) => {
if (!isMounted) {
internals.mt(
vnode,
parentNode,
anchor,
parentInstance as any,
null,
undefined,
false,
)
if (!isMounted || isHydrating) {
if (isHydrating) {
;(
vdomHydrateNode ||
(vdomHydrateNode = ensureHydrationRenderer().hydrateNode!)
)(
currentHydrationNode!,
vnode,
parentInstance as any,
null,
null,
false,
)
} else {
internals.mt(
vnode,
parentNode,
anchor,
parentInstance as any,
null,
undefined,
false,
)
}
onScopeDispose(unmount, true)
isMounted = true
} else {
@ -207,7 +228,7 @@ function createVDOMComponent(
frag.remove = unmount
return [frag, vnode]
return frag
}
/**
@ -235,28 +256,43 @@ function renderVDOMSlot(
isFunction(name) ? name() : name,
props,
)
if ((vnode.children as any[]).length) {
if (fallbackNodes) {
remove(fallbackNodes, parentNode)
fallbackNodes = undefined
}
internals.p(
oldVNode,
if (isHydrating) {
locateHydrationNode(true)
;(
vdomHydrateNode ||
(vdomHydrateNode = ensureHydrationRenderer().hydrateNode!)
)(
currentHydrationNode!,
vnode,
parentNode,
anchor,
parentComponent as any,
null,
null,
false,
)
oldVNode = vnode
} else {
if (fallback && !fallbackNodes) {
// mount fallback
if (oldVNode) {
internals.um(oldVNode, parentComponent as any, null, true)
if ((vnode.children as any[]).length) {
if (fallbackNodes) {
remove(fallbackNodes, parentNode)
fallbackNodes = undefined
}
insert((fallbackNodes = fallback(props)), parentNode, anchor)
internals.p(
oldVNode,
vnode,
parentNode,
anchor,
parentComponent as any,
)
oldVNode = vnode
} else {
if (fallback && !fallbackNodes) {
// mount fallback
if (oldVNode) {
internals.um(oldVNode, parentComponent as any, null, true)
}
insert((fallbackNodes = fallback(props)), parentNode, anchor)
}
oldVNode = null
}
oldVNode = null
}
})
isMounted = true
@ -284,14 +320,11 @@ function renderVDOMSlot(
}
export const vaporInteropPlugin: Plugin = app => {
const { internals, hydrateNode } = (
app._ssr ? ensureHydrationRenderer() : ensureRenderer()
) as HydrationRenderer
const internals = ensureRenderer().internals
app._context.vapor = extend(vaporInteropImpl, {
vdomMount: createVDOMComponent.bind(null, internals),
vdomUnmount: internals.umt,
vdomSlot: renderVDOMSlot.bind(null, internals),
vdomHydrate: hydrateNode,
})
const mount = app.mount
app.mount = ((...args) => {