mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-11 00:23:53 +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.
|
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
|
### 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.
|
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
|
// @ts-ignore
|
||||||
import * as islandComponents from '#build/components.islands.mjs'
|
import * as islandComponents from '#build/components.islands.mjs'
|
||||||
@ -11,9 +12,8 @@ export default defineComponent({
|
|||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async setup (props) {
|
setup (props) {
|
||||||
// TODO: https://github.com/vuejs/core/issues/6207
|
const component = islandComponents[props.context.name] as ReturnType<typeof defineAsyncComponent>
|
||||||
const component = islandComponents[props.context.name]
|
|
||||||
|
|
||||||
if (!component) {
|
if (!component) {
|
||||||
throw createError({
|
throw createError({
|
||||||
@ -21,13 +21,6 @@ export default defineComponent({
|
|||||||
statusMessage: `Island component not found: ${JSON.stringify(component)}`
|
statusMessage: `Island component not found: ${JSON.stringify(component)}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return () => createVNode(component || 'span', props.context.props)
|
||||||
if (typeof component === 'object') {
|
|
||||||
await component.__asyncLoader?.()
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => [
|
|
||||||
createBlock(Teleport as any, { to: 'nuxt-island' }, [h(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 { debounce } from 'perfect-debounce'
|
||||||
import { hash } from 'ohash'
|
import { hash } from 'ohash'
|
||||||
import { appendHeader } from 'h3'
|
import { appendHeader } from 'h3'
|
||||||
@ -30,10 +30,10 @@ export default defineComponent({
|
|||||||
async setup (props) {
|
async setup (props) {
|
||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
const hashId = computed(() => hash([props.name, props.props, props.context]))
|
const hashId = computed(() => hash([props.name, props.props, props.context]))
|
||||||
|
const instance = getCurrentInstance()!
|
||||||
const event = useRequestEvent()
|
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: [] })
|
const cHead = ref<Record<'link' | 'style', Array<Record<string, string>>>>({ link: [], style: [] })
|
||||||
useHead(cHead)
|
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_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 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'])
|
const PRERENDER_NO_SSR_ROUTES = new Set(['/index.html', '/200.html', '/404.html'])
|
||||||
|
|
||||||
@ -284,7 +285,7 @@ export default defineRenderHandler(async (event) => {
|
|||||||
renderedMeta.bodyScriptsPrepend,
|
renderedMeta.bodyScriptsPrepend,
|
||||||
ssrContext.teleports?.body
|
ssrContext.teleports?.body
|
||||||
]),
|
]),
|
||||||
body: (process.env.NUXT_COMPONENT_ISLANDS && islandContext) ? [] : [_rendered.html],
|
body: [_rendered.html],
|
||||||
bodyAppend: normalizeChunks([
|
bodyAppend: normalizeChunks([
|
||||||
process.env.NUXT_NO_SCRIPTS
|
process.env.NUXT_NO_SCRIPTS
|
||||||
? undefined
|
? undefined
|
||||||
@ -318,7 +319,7 @@ export default defineRenderHandler(async (event) => {
|
|||||||
const islandResponse: NuxtIslandResponse = {
|
const islandResponse: NuxtIslandResponse = {
|
||||||
id: islandContext.id,
|
id: islandContext.id,
|
||||||
head,
|
head,
|
||||||
html: ssrContext.teleports!['nuxt-island'].replace(/<!--.*-->/g, ''),
|
html: getServerComponentHTML(htmlContext.body),
|
||||||
state: ssrContext.payload.state
|
state: ssrContext.payload.state
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,3 +429,11 @@ function splitPayload (ssrContext: NuxtSSRContext) {
|
|||||||
payload: { data, prerenderedAt }
|
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 () => {
|
it('renders pure components', async () => {
|
||||||
const result: NuxtIslandResponse = await $fetch(withQuery('/__nuxt_island/PureComponent', {
|
const result: NuxtIslandResponse = await $fetch(withQuery('/__nuxt_island/PureComponent', {
|
||||||
props: JSON.stringify({
|
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 routeIslandVisible = ref(false)
|
||||||
|
|
||||||
|
const count = ref(0)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -27,6 +29,16 @@ const routeIslandVisible = ref(false)
|
|||||||
<button v-else @click="routeIslandVisible = true">
|
<button v-else @click="routeIslandVisible = true">
|
||||||
Show
|
Show
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</template>
|
</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