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 { debounce } from 'perfect-debounce'
import { hash } from 'ohash' import { hash } from 'ohash'
import { appendHeader } from 'h3' import { appendHeader } from 'h3'
@ -33,7 +34,7 @@ export default defineComponent({
const instance = getCurrentInstance()! const instance = getCurrentInstance()!
const event = useRequestEvent() 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: [] }) const cHead = ref<Record<'link' | 'style', Array<Record<string, string>>>>({ link: [], style: [] })
useHead(cHead) useHead(cHead)
@ -51,7 +52,7 @@ export default defineComponent({
} }
}) })
} }
const key = ref(0)
async function fetchComponent () { async function fetchComponent () {
nuxtApp[pKey] = nuxtApp[pKey] || {} nuxtApp[pKey] = nuxtApp[pKey] || {}
if (!nuxtApp[pKey][hashId.value]) { if (!nuxtApp[pKey][hashId.value]) {
@ -63,6 +64,7 @@ export default defineComponent({
cHead.value.link = res.head.link cHead.value.link = res.head.link
cHead.value.style = res.head.style cHead.value.style = res.head.style
html.value = res.html html.value = res.html
key.value++
} }
if (process.client) { if (process.client) {
@ -72,7 +74,40 @@ export default defineComponent({
if (process.server || !nuxtApp.isHydrating) { if (process.server || !nuxtApp.isHydrating) {
await fetchComponent() await fetchComponent()
} }
return () => h((_, { slots }) => slots.default?.(), { key: key.value }, {
return () => createStaticVNode(html.value, 1) 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 { debounce } from 'perfect-debounce'
import { hash } from 'ohash' import { hash } from 'ohash'
import { appendHeader } from 'h3' import { appendHeader } from 'h3'
@ -42,6 +42,7 @@ const NuxtServerComponent = defineComponent({
}, },
async setup (props) { async setup (props) {
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
const key = ref(0)
const hashId = computed(() => hash([props.name, props.props, props.context])) const hashId = computed(() => hash([props.name, props.props, props.context]))
const event = useRequestEvent() const event = useRequestEvent()
@ -92,11 +93,14 @@ const NuxtServerComponent = defineComponent({
useHead(() => res.data.value!.head) useHead(() => res.data.value!.head)
if (process.client) { if (process.client) {
watch(props, debounce(() => res.execute(), 100)) watch(props, debounce(async () => {
await res.execute()
key.value++
}, 100))
} }
await res 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() 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', () => { describe('rich payloads', () => {
@ -1109,7 +1126,7 @@ describe('component islands', () => {
const result: NuxtIslandResponse = await $fetch('/__nuxt_island/RouteComponent?url=/foo') const result: NuxtIslandResponse = await $fetch('/__nuxt_island/RouteComponent?url=/foo')
if (isDev()) { 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(` expect(result).toMatchInlineSnapshot(`
@ -1132,7 +1149,7 @@ describe('component islands', () => {
}) })
})) }))
if (isDev()) { 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(` expect(result).toMatchInlineSnapshot(`
{ {
@ -1153,7 +1170,7 @@ describe('component islands', () => {
}) })
})) }))
if (isDev()) { 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(` expect(result).toMatchInlineSnapshot(`
{ {

View File

@ -18,7 +18,7 @@ const count = ref(0)
<NuxtIsland name="PureComponent" :props="islandProps" /> <NuxtIsland name="PureComponent" :props="islandProps" />
<NuxtIsland name="PureComponent" :props="islandProps" /> <NuxtIsland name="PureComponent" :props="islandProps" />
</div> </div>
<button @click="islandProps.number++"> <button id="increase-pure-component" @click="islandProps.number++">
Increase Increase
</button> </button>
<hr> <hr>
@ -26,7 +26,7 @@ const count = ref(0)
<div v-if="routeIslandVisible" class="box"> <div v-if="routeIslandVisible" class="box">
<NuxtIsland name="RouteComponent" :context="{ url: '/test' }" /> <NuxtIsland name="RouteComponent" :context="{ url: '/test' }" />
</div> </div>
<button v-else @click="routeIslandVisible = true"> <button v-else id="show-route" @click="routeIslandVisible = true">
Show Show
</button> </button>
@ -35,7 +35,7 @@ const count = ref(0)
<div> <div>
Async island component (20ms): Async island component (20ms):
<NuxtIsland name="LongAsyncComponent" :props="{ count }" /> <NuxtIsland name="LongAsyncComponent" :props="{ count }" />
<button @click="count++"> <button id="count-async-server-long-async" @click="count++">
add +1 to count add +1 to count
</button> </button>
</div> </div>