fix(nuxt): use key to force server component re-rendering (#19911)

This commit is contained in:
Julien Huang 2023-04-20 23:41:20 +02:00 committed by GitHub
parent 8b7df05ff0
commit e8e01bac13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 70 additions and 14 deletions

View File

@ -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 === ']'
}

View File

@ -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)])
}
})

View File

@ -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(`
{

View File

@ -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>