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

View File

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

View File

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

View File

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

View File

@ -114,7 +114,6 @@ export function createSlot(
: EMPTY_OBJ : EMPTY_OBJ
let fragment: DynamicFragment let fragment: DynamicFragment
if (isRef(rawSlots._)) { if (isRef(rawSlots._)) {
fragment = instance.appContext.vapor!.vdomSlot( fragment = instance.appContext.vapor!.vdomSlot(
rawSlots._, 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) insert(fragment, _insertionParent, _insertionAnchor)
} }

View File

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