mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 15:15:19 +00:00
fix(nuxt): use key to force server component re-rendering (#19911)
This commit is contained in:
parent
8b7df05ff0
commit
e8e01bac13
@ -1,4 +1,5 @@
|
||||
import { computed, createStaticVNode, defineComponent, getCurrentInstance, ref, watch } from 'vue'
|
||||
import type { RendererNode } from 'vue'
|
||||
import { computed, createStaticVNode, defineComponent, getCurrentInstance, h, ref, watch } from 'vue'
|
||||
import { debounce } from 'perfect-debounce'
|
||||
import { hash } from 'ohash'
|
||||
import { appendHeader } from 'h3'
|
||||
@ -33,7 +34,7 @@ export default defineComponent({
|
||||
const instance = getCurrentInstance()!
|
||||
const event = useRequestEvent()
|
||||
|
||||
const html = ref<string>(process.client ? instance.vnode.el?.outerHTML ?? '<div></div>' : '<div></div>')
|
||||
const html = ref<string>(process.client ? getFragmentHTML(instance?.vnode?.el).join('') ?? '<div></div>' : '<div></div>')
|
||||
const cHead = ref<Record<'link' | 'style', Array<Record<string, string>>>>({ link: [], style: [] })
|
||||
useHead(cHead)
|
||||
|
||||
@ -51,7 +52,7 @@ export default defineComponent({
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const key = ref(0)
|
||||
async function fetchComponent () {
|
||||
nuxtApp[pKey] = nuxtApp[pKey] || {}
|
||||
if (!nuxtApp[pKey][hashId.value]) {
|
||||
@ -63,6 +64,7 @@ export default defineComponent({
|
||||
cHead.value.link = res.head.link
|
||||
cHead.value.style = res.head.style
|
||||
html.value = res.html
|
||||
key.value++
|
||||
}
|
||||
|
||||
if (process.client) {
|
||||
@ -72,7 +74,40 @@ export default defineComponent({
|
||||
if (process.server || !nuxtApp.isHydrating) {
|
||||
await fetchComponent()
|
||||
}
|
||||
|
||||
return () => createStaticVNode(html.value, 1)
|
||||
return () => h((_, { slots }) => slots.default?.(), { key: key.value }, {
|
||||
default: () => [createStaticVNode(html.value, 1)]
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// TODO refactor with https://github.com/nuxt/nuxt/pull/19231
|
||||
function getFragmentHTML (element: RendererNode | null) {
|
||||
if (element) {
|
||||
if (element.nodeName === '#comment' && element.nodeValue === '[') {
|
||||
return getFragmentChildren(element)
|
||||
}
|
||||
return [element.outerHTML]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function getFragmentChildren (element: RendererNode | null, blocks: string[] = []) {
|
||||
if (element && element.nodeName) {
|
||||
if (isEndFragment(element)) {
|
||||
return blocks
|
||||
} else if (!isStartFragment(element)) {
|
||||
blocks.push(element.outerHTML)
|
||||
}
|
||||
|
||||
getFragmentChildren(element.nextSibling, blocks)
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
function isStartFragment (element: RendererNode) {
|
||||
return element.nodeName === '#comment' && element.nodeValue === '['
|
||||
}
|
||||
|
||||
function isEndFragment (element: RendererNode) {
|
||||
return element.nodeName === '#comment' && element.nodeValue === ']'
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { computed, createStaticVNode, defineComponent, h, watch } from 'vue'
|
||||
import { Fragment, computed, createStaticVNode, createVNode, defineComponent, h, ref, watch } from 'vue'
|
||||
import { debounce } from 'perfect-debounce'
|
||||
import { hash } from 'ohash'
|
||||
import { appendHeader } from 'h3'
|
||||
@ -42,6 +42,7 @@ const NuxtServerComponent = defineComponent({
|
||||
},
|
||||
async setup (props) {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const key = ref(0)
|
||||
const hashId = computed(() => hash([props.name, props.props, props.context]))
|
||||
|
||||
const event = useRequestEvent()
|
||||
@ -92,11 +93,14 @@ const NuxtServerComponent = defineComponent({
|
||||
useHead(() => res.data.value!.head)
|
||||
|
||||
if (process.client) {
|
||||
watch(props, debounce(() => res.execute(), 100))
|
||||
watch(props, debounce(async () => {
|
||||
await res.execute()
|
||||
key.value++
|
||||
}, 100))
|
||||
}
|
||||
|
||||
await res
|
||||
|
||||
return () => createStaticVNode(res.data.value!.html, 1)
|
||||
return () => createVNode(Fragment, { key: key.value }, [createStaticVNode(res.data.value!.html, 1)])
|
||||
}
|
||||
})
|
||||
|
@ -337,6 +337,23 @@ describe('pages', () => {
|
||||
|
||||
await page.close()
|
||||
})
|
||||
|
||||
it('/islands', async () => {
|
||||
const page = await createPage('/islands')
|
||||
await page.waitForLoadState('networkidle')
|
||||
await page.locator('#increase-pure-component').click()
|
||||
await page.waitForResponse(response => response.url().includes('/__nuxt_island/') && response.status() === 200)
|
||||
await page.waitForLoadState('networkidle')
|
||||
expect(await page.locator('.box').innerHTML()).toContain('"number": 101,')
|
||||
await page.locator('#count-async-server-long-async').click()
|
||||
await Promise.all([
|
||||
page.waitForResponse(response => response.url().includes('/__nuxt_island/LongAsyncComponent') && response.status() === 200),
|
||||
page.waitForResponse(response => response.url().includes('/__nuxt_island/AsyncServerComponent') && response.status() === 200)
|
||||
])
|
||||
await page.waitForLoadState('networkidle')
|
||||
expect(await page.locator('#async-server-component-count').innerHTML()).toContain(('1'))
|
||||
expect(await page.locator('#long-async-component-count').innerHTML()).toContain('1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('rich payloads', () => {
|
||||
@ -1109,7 +1126,7 @@ describe('component islands', () => {
|
||||
const result: NuxtIslandResponse = await $fetch('/__nuxt_island/RouteComponent?url=/foo')
|
||||
|
||||
if (isDev()) {
|
||||
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates'))
|
||||
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates') && (l.href.startsWith('_nuxt/components/islands/') && l.href.includes('_nuxt/components/islands/RouteComponent')))
|
||||
}
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
@ -1132,7 +1149,7 @@ describe('component islands', () => {
|
||||
})
|
||||
}))
|
||||
if (isDev()) {
|
||||
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates'))
|
||||
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates') && (l.href.startsWith('_nuxt/components/islands/') && l.href.includes('_nuxt/components/islands/LongAsyncComponent')))
|
||||
}
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
@ -1153,7 +1170,7 @@ describe('component islands', () => {
|
||||
})
|
||||
}))
|
||||
if (isDev()) {
|
||||
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates'))
|
||||
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates') && (l.href.startsWith('_nuxt/components/islands/') && l.href.includes('_nuxt/components/islands/AsyncServerComponent')))
|
||||
}
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
|
6
test/fixtures/basic/pages/islands.vue
vendored
6
test/fixtures/basic/pages/islands.vue
vendored
@ -18,7 +18,7 @@ const count = ref(0)
|
||||
<NuxtIsland name="PureComponent" :props="islandProps" />
|
||||
<NuxtIsland name="PureComponent" :props="islandProps" />
|
||||
</div>
|
||||
<button @click="islandProps.number++">
|
||||
<button id="increase-pure-component" @click="islandProps.number++">
|
||||
Increase
|
||||
</button>
|
||||
<hr>
|
||||
@ -26,7 +26,7 @@ const count = ref(0)
|
||||
<div v-if="routeIslandVisible" class="box">
|
||||
<NuxtIsland name="RouteComponent" :context="{ url: '/test' }" />
|
||||
</div>
|
||||
<button v-else @click="routeIslandVisible = true">
|
||||
<button v-else id="show-route" @click="routeIslandVisible = true">
|
||||
Show
|
||||
</button>
|
||||
|
||||
@ -35,7 +35,7 @@ const count = ref(0)
|
||||
<div>
|
||||
Async island component (20ms):
|
||||
<NuxtIsland name="LongAsyncComponent" :props="{ count }" />
|
||||
<button @click="count++">
|
||||
<button id="count-async-server-long-async" @click="count++">
|
||||
add +1 to count
|
||||
</button>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user