]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: hydation for dynamic component
authordaiwei <daiwei521@126.com>
Fri, 25 Apr 2025 03:33:35 +0000 (11:33 +0800)
committerdaiwei <daiwei521@126.com>
Fri, 25 Apr 2025 03:33:35 +0000 (11:33 +0800)
packages/runtime-vapor/__tests__/hydration.spec.ts
packages/runtime-vapor/src/apiCreateDynamicComponent.ts
packages/runtime-vapor/src/block.ts
packages/server-renderer/src/render.ts

index d33d4c24d5ffd6be81aad23e09f9265f53c67ddb..58898305176d89c8b892d94df3cd247a00357e08 100644 (file)
@@ -1159,6 +1159,154 @@ describe('Vapor Mode hydration', () => {
           )
         })
       })
+
+      // problem is there is a continuous `<!--$-->`
+      test.todo('on dynamic component with anchor insertion', async () => {
+        runWithEnv(isProd, async () => {
+          const dynamicComponentAnchorLabel = isProd ? '$' : 'dynamic-component'
+          const data = ref(true)
+          const { container } = await testHydration(
+            `<template>
+              <div>
+                <span/>
+                <component :is="components.Child" v-if="data"/>
+                <span/>
+              </div>
+            </template>`,
+            { Child: `<template>foo</template>` },
+            data,
+          )
+          expect(container.innerHTML).toBe(
+            `<div>` +
+              `<span></span>` +
+              `foo<!--${dynamicComponentAnchorLabel}--><!--${anchorLabel}-->` +
+              `<span></span>` +
+              `</div>`,
+          )
+
+          data.value = false
+          await nextTick()
+          expect(container.innerHTML).toBe(
+            `<div>` +
+              `<span></span>` +
+              `<!--${anchorLabel}-->` +
+              `<span></span>` +
+              `</div>`,
+          )
+        })
+      })
+    }
+  })
+
+  describe('dynamic component', () => {
+    describe('DEV mode', () => {
+      runTests()
+    })
+    describe('PROD mode', () => {
+      runTests(true)
+    })
+
+    function runTests(isProd: boolean = false) {
+      const anchorLabel = isProd ? '$' : 'dynamic-component'
+
+      test('basic dynamic component', async () => {
+        runWithEnv(isProd, async () => {
+          const { container, data } = await testHydration(
+            `<template>
+              <component :is="components[data]"/>
+            </template>`,
+            {
+              foo: `<template><div>foo</div></template>`,
+              bar: `<template><div>bar</div></template>`,
+            },
+            ref('foo'),
+          )
+          expect(container.innerHTML).toBe(
+            `<div>foo</div><!--${anchorLabel}-->`,
+          )
+
+          data.value = 'bar'
+          await nextTick()
+          expect(container.innerHTML).toBe(
+            `<div>bar</div><!--${anchorLabel}-->`,
+          )
+        })
+      })
+
+      test('dynamic component with anchor insertion', async () => {
+        runWithEnv(isProd, async () => {
+          const { container, data } = await testHydration(
+            `<template>
+            <div>
+              <span/>
+              <component :is="components[data]"/>
+              <span/>
+            </div>
+          </template>`,
+            {
+              foo: `<template><div>foo</div></template>`,
+              bar: `<template><div>bar</div></template>`,
+            },
+            ref('foo'),
+          )
+          expect(container.innerHTML).toBe(
+            `<div>` +
+              `<span></span>` +
+              `<div>foo</div><!--${anchorLabel}-->` +
+              `<span></span>` +
+              `</div>`,
+          )
+
+          data.value = 'bar'
+          await nextTick()
+          expect(container.innerHTML).toBe(
+            `<div>` +
+              `<span></span>` +
+              `<div>bar</div><!--${anchorLabel}-->` +
+              `<span></span>` +
+              `</div>`,
+          )
+        })
+      })
+
+      test('consecutive dynamic components with anchor insertion', async () => {
+        runWithEnv(isProd, async () => {
+          const { container, data } = await testHydration(
+            `<template>
+              <div>
+                <span/>
+                <component :is="components[data]"/>
+                <component :is="components[data]"/>
+                <span/>
+              </div>
+            </template>`,
+            {
+              foo: `<template><div>foo</div></template>`,
+              bar: `<template><div>bar</div></template>`,
+            },
+            ref('foo'),
+          )
+          expect(container.innerHTML).toBe(
+            `<div>` +
+              `<span></span>` +
+              `<div>foo</div><!--${anchorLabel}-->` +
+              `<!--[[--><div>foo</div><!--${anchorLabel}--><!--]]-->` +
+              `<span></span>` +
+              `</div>`,
+          )
+
+          data.value = 'bar'
+          await nextTick()
+          expect(container.innerHTML).toBe(
+            `<div>` +
+              `<span></span>` +
+              `<div>bar</div><!--${anchorLabel}-->` +
+              `<!--[[--><div>bar</div><!--${anchorLabel}--><!--]]-->` +
+              `<span></span>` +
+              `</div>`,
+          )
+        })
+      })
     }
   })
 
index 2126611d7182e1cb548d4179e09dd536c8f3c920..db690184292e06e5665d52cfed106c0d34a362de 100644 (file)
@@ -1,9 +1,15 @@
 import { resolveDynamicComponent } from '@vue/runtime-dom'
-import { DynamicFragment, type VaporFragment } from './block'
+import { DynamicFragment, type VaporFragment, insert } from './block'
 import { createComponentWithFallback } from './component'
 import { renderEffect } from './renderEffect'
 import type { RawProps } from './componentProps'
 import type { RawSlots } from './componentSlots'
+import {
+  insertionAnchor,
+  insertionParent,
+  resetInsertionState,
+} from './insertionState'
+import { isHydrating, locateHydrationNode } from './dom/hydration'
 
 export function createDynamicComponent(
   getter: () => any,
@@ -11,6 +17,14 @@ export function createDynamicComponent(
   rawSlots?: RawSlots | null,
   isSingleRoot?: boolean,
 ): VaporFragment {
+  const _insertionParent = insertionParent
+  const _insertionAnchor = insertionAnchor
+  if (isHydrating) {
+    locateHydrationNode(true)
+  } else {
+    resetInsertionState()
+  }
+
   const frag = __DEV__
     ? new DynamicFragment('dynamic-component')
     : new DynamicFragment()
@@ -27,5 +41,9 @@ export function createDynamicComponent(
       value,
     )
   })
+
+  if (!isHydrating && _insertionParent) {
+    insert(frag, _insertionParent, _insertionAnchor)
+  }
   return frag
 }
index 15128d89df24c46efe30b9ac854dfe22b10b3671..254779c6457806eeafa10335cd38649224fee3be 100644 (file)
@@ -97,7 +97,7 @@ export class DynamicFragment extends VaporFragment {
         warn(`DynamicFragment anchor not found...`)
       }
     }
-    if (__DEV__ && label) (this.anchor as Comment).data = label
+    if (__DEV__ && label && this.anchor) (this.anchor as Comment).data = label
   }
 }
 
index 221d3895e2288f80699d918f333d3631498825f2..f4775bb9d505c9a84cc30d0e2a5288f55773e5aa 100644 (file)
@@ -264,6 +264,7 @@ export function renderVNode(
         renderElementVNode(push, vnode, parentComponent, slotScopeId)
       } else if (shapeFlag & ShapeFlags.COMPONENT) {
         push(renderComponentVNode(vnode, parentComponent, slotScopeId))
+        push(`<!--$-->`) // anchor for vapor hydration
       } else if (shapeFlag & ShapeFlags.TELEPORT) {
         renderTeleportVNode(push, vnode, parentComponent, slotScopeId)
       } else if (shapeFlag & ShapeFlags.SUSPENSE) {