mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-29 00:52:01 +00:00
fix(nuxt): directly render server components (#19605)
This commit is contained in:
parent
3a971d0b36
commit
4671294229
@ -297,10 +297,6 @@ Now you can register server-only components with the `.server` suffix and use th
|
||||
Slots are not supported by server components in their current state of development.
|
||||
::
|
||||
|
||||
::alert{type=info}
|
||||
Please note that there is a known issue with using async code in a server component. It will lead to a hydration mismatch error on initial render. See [#18500](https://github.com/nuxt/nuxt/issues/18500#issuecomment-1403528142). Until there is a workaround, server-components must be synchronous.
|
||||
::
|
||||
|
||||
### Paired with a `.client` component
|
||||
|
||||
In this case, the `.server` + `.client` components are two 'halves' of a component and can be used in advanced use cases for separate implementations of a component on server and client side.
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { createBlock, defineComponent, h, Teleport } from 'vue'
|
||||
import type { defineAsyncComponent } from 'vue'
|
||||
import { defineComponent, createVNode } from 'vue'
|
||||
|
||||
// @ts-ignore
|
||||
import * as islandComponents from '#build/components.islands.mjs'
|
||||
@ -11,9 +12,8 @@ export default defineComponent({
|
||||
required: true
|
||||
}
|
||||
},
|
||||
async setup (props) {
|
||||
// TODO: https://github.com/vuejs/core/issues/6207
|
||||
const component = islandComponents[props.context.name]
|
||||
setup (props) {
|
||||
const component = islandComponents[props.context.name] as ReturnType<typeof defineAsyncComponent>
|
||||
|
||||
if (!component) {
|
||||
throw createError({
|
||||
@ -21,13 +21,6 @@ export default defineComponent({
|
||||
statusMessage: `Island component not found: ${JSON.stringify(component)}`
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof component === 'object') {
|
||||
await component.__asyncLoader?.()
|
||||
}
|
||||
|
||||
return () => [
|
||||
createBlock(Teleport as any, { to: 'nuxt-island' }, [h(component || 'span', props.context.props)])
|
||||
]
|
||||
return () => createVNode(component || 'span', props.context.props)
|
||||
}
|
||||
})
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { defineComponent, createStaticVNode, computed, ref, watch } from 'vue'
|
||||
import { defineComponent, createStaticVNode, computed, ref, watch, getCurrentInstance } from 'vue'
|
||||
import { debounce } from 'perfect-debounce'
|
||||
import { hash } from 'ohash'
|
||||
import { appendHeader } from 'h3'
|
||||
@ -30,10 +30,10 @@ export default defineComponent({
|
||||
async setup (props) {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const hashId = computed(() => hash([props.name, props.props, props.context]))
|
||||
|
||||
const instance = getCurrentInstance()!
|
||||
const event = useRequestEvent()
|
||||
|
||||
const html = ref<string>('')
|
||||
const html = ref<string>(process.client ? instance.vnode.el?.outerHTML ?? '<div></div>' : '<div></div>')
|
||||
const cHead = ref<Record<'link' | 'style', Array<Record<string, string>>>>({ link: [], style: [] })
|
||||
useHead(cHead)
|
||||
|
||||
|
@ -160,6 +160,7 @@ async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
|
||||
|
||||
const PAYLOAD_CACHE = (process.env.NUXT_PAYLOAD_EXTRACTION && process.env.prerender) ? new Map() : null // TODO: Use LRU cache
|
||||
const PAYLOAD_URL_RE = /\/_payload(\.[a-zA-Z0-9]+)?.js(\?.*)?$/
|
||||
const ROOT_NODE_REGEX = new RegExp(`^<${appRootTag} id="${appRootId}">([\\s\\S]*)</${appRootTag}>$`)
|
||||
|
||||
const PRERENDER_NO_SSR_ROUTES = new Set(['/index.html', '/200.html', '/404.html'])
|
||||
|
||||
@ -284,7 +285,7 @@ export default defineRenderHandler(async (event) => {
|
||||
renderedMeta.bodyScriptsPrepend,
|
||||
ssrContext.teleports?.body
|
||||
]),
|
||||
body: (process.env.NUXT_COMPONENT_ISLANDS && islandContext) ? [] : [_rendered.html],
|
||||
body: [_rendered.html],
|
||||
bodyAppend: normalizeChunks([
|
||||
process.env.NUXT_NO_SCRIPTS
|
||||
? undefined
|
||||
@ -318,7 +319,7 @@ export default defineRenderHandler(async (event) => {
|
||||
const islandResponse: NuxtIslandResponse = {
|
||||
id: islandContext.id,
|
||||
head,
|
||||
html: ssrContext.teleports!['nuxt-island'].replace(/<!--.*-->/g, ''),
|
||||
html: getServerComponentHTML(htmlContext.body),
|
||||
state: ssrContext.payload.state
|
||||
}
|
||||
|
||||
@ -428,3 +429,11 @@ function splitPayload (ssrContext: NuxtSSRContext) {
|
||||
payload: { data, prerenderedAt }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* remove the root node from the html body
|
||||
*/
|
||||
function getServerComponentHTML (body: string[]): string {
|
||||
const match = body[0].match(ROOT_NODE_REGEX)
|
||||
return match ? match[1] : body[0]
|
||||
}
|
||||
|
@ -1069,6 +1069,48 @@ describe('component islands', () => {
|
||||
`)
|
||||
})
|
||||
|
||||
it('render async component', async () => {
|
||||
const result: NuxtIslandResponse = await $fetch(withQuery('/__nuxt_island/LongAsyncComponent', {
|
||||
props: JSON.stringify({
|
||||
count: 3
|
||||
})
|
||||
}))
|
||||
if (isDev()) {
|
||||
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates'))
|
||||
}
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"head": {
|
||||
"link": [],
|
||||
"style": [],
|
||||
},
|
||||
"html": "<div>that was very long ... <div id=\\"long-async-component-count\\">3</div><p>hello world !!!</p></div>",
|
||||
"state": {},
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('render .server async component', async () => {
|
||||
const result: NuxtIslandResponse = await $fetch(withQuery('/__nuxt_island/AsyncServerComponent', {
|
||||
props: JSON.stringify({
|
||||
count: 2
|
||||
})
|
||||
}))
|
||||
if (isDev()) {
|
||||
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates'))
|
||||
}
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"head": {
|
||||
"link": [],
|
||||
"style": [],
|
||||
},
|
||||
"html": "<div> This is a .server (20ms) async component that was very long ... <div id=\\"async-server-component-count\\">2</div></div>",
|
||||
"state": {},
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('renders pure components', async () => {
|
||||
const result: NuxtIslandResponse = await $fetch(withQuery('/__nuxt_island/PureComponent', {
|
||||
props: JSON.stringify({
|
||||
|
17
test/fixtures/basic/components/AsyncServerComponent.server.vue
vendored
Normal file
17
test/fixtures/basic/components/AsyncServerComponent.server.vue
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div>
|
||||
This is a .server (20ms) async component
|
||||
{{ data }}
|
||||
<div id="async-server-component-count">
|
||||
{{ count }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
count: number
|
||||
}>()
|
||||
|
||||
const { data } = await useFetch('/api/very-long-request')
|
||||
</script>
|
17
test/fixtures/basic/components/islands/LongAsyncComponent.vue
vendored
Normal file
17
test/fixtures/basic/components/islands/LongAsyncComponent.vue
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div>
|
||||
{{ data }}
|
||||
<div id="long-async-component-count">
|
||||
{{ count }}
|
||||
</div>
|
||||
<p>hello world !!!</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
count: number
|
||||
}>()
|
||||
|
||||
const { data } = await useFetch('/api/very-long-request')
|
||||
</script>
|
12
test/fixtures/basic/pages/islands.vue
vendored
12
test/fixtures/basic/pages/islands.vue
vendored
@ -7,6 +7,8 @@ const islandProps = ref({
|
||||
})
|
||||
|
||||
const routeIslandVisible = ref(false)
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -27,6 +29,16 @@ const routeIslandVisible = ref(false)
|
||||
<button v-else @click="routeIslandVisible = true">
|
||||
Show
|
||||
</button>
|
||||
|
||||
<p>async .server component</p>
|
||||
<AsyncServerComponent :count="count" />
|
||||
<div>
|
||||
Async island component (20ms):
|
||||
<NuxtIsland name="LongAsyncComponent" :props="{ count }" />
|
||||
<button @click="count++">
|
||||
add +1 to count
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
7
test/fixtures/basic/server/api/very-long-request.ts
vendored
Normal file
7
test/fixtures/basic/server/api/very-long-request.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
export default defineEventHandler(async () => {
|
||||
await timeout(20)
|
||||
return 'that was very long ...'
|
||||
})
|
||||
function timeout (ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
Loading…
Reference in New Issue
Block a user