mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 15:15:19 +00:00
feat(nuxt): support server components with extracted payloads (#10113)
This commit is contained in:
parent
7f26373237
commit
5e1881c20a
@ -2,9 +2,10 @@ import { defineComponent, createStaticVNode, computed, ref, watch } from 'vue'
|
|||||||
import { debounce } from 'perfect-debounce'
|
import { debounce } from 'perfect-debounce'
|
||||||
import { hash } from 'ohash'
|
import { hash } from 'ohash'
|
||||||
import type { MetaObject } from '@nuxt/schema'
|
import type { MetaObject } from '@nuxt/schema'
|
||||||
|
import { appendHeader } from 'h3'
|
||||||
// eslint-disable-next-line import/no-restricted-paths
|
// eslint-disable-next-line import/no-restricted-paths
|
||||||
import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
|
import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
|
||||||
import { useHead, useNuxtApp } from '#app'
|
import { useHead, useNuxtApp, useRequestEvent } from '#app'
|
||||||
|
|
||||||
const pKey = '_islandPromises'
|
const pKey = '_islandPromises'
|
||||||
|
|
||||||
@ -27,13 +28,21 @@ 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 event = useRequestEvent()
|
||||||
|
|
||||||
const html = ref<string>('')
|
const html = ref<string>('')
|
||||||
const cHead = ref<MetaObject>({ link: [], style: [] })
|
const cHead = ref<MetaObject>({ link: [], style: [] })
|
||||||
useHead(cHead)
|
useHead(cHead)
|
||||||
|
|
||||||
function _fetchComponent () {
|
function _fetchComponent () {
|
||||||
|
const url = `/__nuxt_island/${props.name}:${hashId.value}`
|
||||||
|
if (process.server && process.env.prerender) {
|
||||||
|
// Hint to Nitro to prerender the island component
|
||||||
|
appendHeader(event, 'x-nitro-prerender', url)
|
||||||
|
}
|
||||||
// TODO: Validate response
|
// TODO: Validate response
|
||||||
return $fetch<NuxtIslandResponse>(`/__nuxt_island/${props.name}:${hashId.value}`, {
|
return $fetch<NuxtIslandResponse>(url, {
|
||||||
params: {
|
params: {
|
||||||
...props.context,
|
...props.context,
|
||||||
props: props.props ? JSON.stringify(props.props) : undefined
|
props: props.props ? JSON.stringify(props.props) : undefined
|
||||||
|
@ -1,16 +1,99 @@
|
|||||||
import { defineComponent, h } from 'vue'
|
import { defineComponent, createStaticVNode, computed, h, watch } from 'vue'
|
||||||
// @ts-expect-error virtual import
|
import { debounce } from 'perfect-debounce'
|
||||||
import { NuxtIsland } from '#components'
|
import { hash } from 'ohash'
|
||||||
|
import { appendHeader } from 'h3'
|
||||||
|
|
||||||
|
import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
|
||||||
|
import { useAsyncData, useHead, useNuxtApp, useRequestEvent } from '#app'
|
||||||
|
|
||||||
|
const pKey = '_islandPromises'
|
||||||
|
|
||||||
export const createServerComponent = (name: string) => {
|
export const createServerComponent = (name: string) => {
|
||||||
return defineComponent({
|
return defineComponent({
|
||||||
name,
|
name,
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
setup (_props, { attrs }) {
|
setup (_props, { attrs }) {
|
||||||
return () => h(NuxtIsland, {
|
return () => h(NuxtServerComponent, {
|
||||||
name,
|
name,
|
||||||
props: attrs
|
props: attrs
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NuxtServerComponent = defineComponent({
|
||||||
|
name: 'NuxtServerComponent',
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
type: Object,
|
||||||
|
default: () => undefined
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setup (props) {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
const hashId = computed(() => hash([props.name, props.props, props.context]))
|
||||||
|
|
||||||
|
const event = useRequestEvent()
|
||||||
|
|
||||||
|
function _fetchComponent () {
|
||||||
|
const url = `/__nuxt_island/${props.name}:${hashId.value}`
|
||||||
|
if (process.server && process.env.prerender) {
|
||||||
|
// Hint to Nitro to prerender the island component
|
||||||
|
appendHeader(event, 'x-nitro-prerender', url)
|
||||||
|
}
|
||||||
|
// TODO: Validate response
|
||||||
|
return $fetch<NuxtIslandResponse>(url, {
|
||||||
|
params: {
|
||||||
|
...props.context,
|
||||||
|
props: props.props ? JSON.stringify(props.props) : undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = useAsyncData(
|
||||||
|
`${props.name}:${hashId.value}`,
|
||||||
|
async () => {
|
||||||
|
nuxtApp[pKey] = nuxtApp[pKey] || {}
|
||||||
|
if (!nuxtApp[pKey][hashId.value]) {
|
||||||
|
nuxtApp[pKey][hashId.value] = _fetchComponent().finally(() => {
|
||||||
|
delete nuxtApp[pKey][hashId.value]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const res: NuxtIslandResponse = await nuxtApp[pKey][hashId.value]
|
||||||
|
return {
|
||||||
|
html: res.html,
|
||||||
|
head: {
|
||||||
|
link: res.head.link,
|
||||||
|
style: res.head.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
immediate: process.server || !nuxtApp.isHydrating,
|
||||||
|
default: () => ({
|
||||||
|
html: '',
|
||||||
|
head: {
|
||||||
|
link: [], style: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
useHead(() => res.data.value!.head)
|
||||||
|
|
||||||
|
if (process.client) {
|
||||||
|
watch(props, debounce(() => res.execute(), 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
await res
|
||||||
|
|
||||||
|
return () => createStaticVNode(res.data.value!.html, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@ -3,7 +3,7 @@ import { createHooks, createDebugger } from 'hookable'
|
|||||||
import type { Nuxt, NuxtOptions, NuxtHooks } from '@nuxt/schema'
|
import type { Nuxt, NuxtOptions, NuxtHooks } from '@nuxt/schema'
|
||||||
import type { LoadNuxtOptions } from '@nuxt/kit'
|
import type { LoadNuxtOptions } from '@nuxt/kit'
|
||||||
import { loadNuxtConfig, nuxtCtx, installModule, addComponent, addVitePlugin, addWebpackPlugin, tryResolveModule, addPlugin } from '@nuxt/kit'
|
import { loadNuxtConfig, nuxtCtx, installModule, addComponent, addVitePlugin, addWebpackPlugin, tryResolveModule, addPlugin } from '@nuxt/kit'
|
||||||
/* eslint-disable import/no-restricted-paths */
|
|
||||||
import escapeRE from 'escape-string-regexp'
|
import escapeRE from 'escape-string-regexp'
|
||||||
import fse from 'fs-extra'
|
import fse from 'fs-extra'
|
||||||
import { withoutLeadingSlash } from 'ufo'
|
import { withoutLeadingSlash } from 'ufo'
|
||||||
|
@ -908,7 +908,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV || isWindows)('payload rendering', ()
|
|||||||
it('renders a payload', async () => {
|
it('renders a payload', async () => {
|
||||||
const payload = await $fetch('/random/a/_payload.js', { responseType: 'text' })
|
const payload = await $fetch('/random/a/_payload.js', { responseType: 'text' })
|
||||||
expect(payload).toMatch(
|
expect(payload).toMatch(
|
||||||
/export default \{data:\{hey:{[^}]*},rand_a:\[[^\]]*\]\},prerenderedAt:\d*\}/
|
/export default \{data:\{hey:\{[^}]*\},rand_a:\[[^\]]*\],".*":\{html:".*server-only component.*",head:\{link:\[\],style:\[\]\}\}\},prerenderedAt:\d*\}/
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -930,6 +930,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV || isWindows)('payload rendering', ()
|
|||||||
|
|
||||||
// We are not triggering API requests in the payload
|
// We are not triggering API requests in the payload
|
||||||
expect(requests).not.toContain(expect.stringContaining('/api/random'))
|
expect(requests).not.toContain(expect.stringContaining('/api/random'))
|
||||||
|
expect(requests).not.toContain(expect.stringContaining('/__nuxt_island'))
|
||||||
// requests.length = 0
|
// requests.length = 0
|
||||||
|
|
||||||
await page.click('[href="/random/b"]')
|
await page.click('[href="/random/b"]')
|
||||||
@ -937,6 +938,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV || isWindows)('payload rendering', ()
|
|||||||
|
|
||||||
// We are not triggering API requests in the payload in client-side nav
|
// We are not triggering API requests in the payload in client-side nav
|
||||||
expect(requests).not.toContain('/api/random')
|
expect(requests).not.toContain('/api/random')
|
||||||
|
expect(requests).not.toContain(expect.stringContaining('/__nuxt_island'))
|
||||||
|
|
||||||
// We are fetching a payload we did not prefetch
|
// We are fetching a payload we did not prefetch
|
||||||
expect(requests).toContain('/random/b/_payload.js' + importSuffix)
|
expect(requests).toContain('/random/b/_payload.js' + importSuffix)
|
||||||
@ -950,6 +952,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV || isWindows)('payload rendering', ()
|
|||||||
|
|
||||||
// We are not triggering API requests in the payload in client-side nav
|
// We are not triggering API requests in the payload in client-side nav
|
||||||
expect(requests).not.toContain('/api/random')
|
expect(requests).not.toContain('/api/random')
|
||||||
|
expect(requests).not.toContain(expect.stringContaining('/__nuxt_island'))
|
||||||
|
|
||||||
// We are not refetching payloads we've already prefetched
|
// We are not refetching payloads we've already prefetched
|
||||||
// Note: we refetch on dev as urls differ between '' and '?import'
|
// Note: we refetch on dev as urls differ between '' and '?import'
|
||||||
|
1
test/fixtures/basic/pages/random/[id].vue
vendored
1
test/fixtures/basic/pages/random/[id].vue
vendored
@ -12,6 +12,7 @@
|
|||||||
<NuxtLink to="/random/c" prefetched-class="prefetched">
|
<NuxtLink to="/random/c" prefetched-class="prefetched">
|
||||||
Random (C)
|
Random (C)
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
<ServerOnlyComponent />
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
Random: {{ random }}
|
Random: {{ random }}
|
||||||
|
3
test/fixtures/basic/types.ts
vendored
3
test/fixtures/basic/types.ts
vendored
@ -5,9 +5,8 @@ import type { AppConfig } from '@nuxt/schema'
|
|||||||
|
|
||||||
import type { FetchError } from 'ofetch'
|
import type { FetchError } from 'ofetch'
|
||||||
import type { NavigationFailure, RouteLocationNormalizedLoaded, RouteLocationRaw, useRouter as vueUseRouter } from 'vue-router'
|
import type { NavigationFailure, RouteLocationNormalizedLoaded, RouteLocationRaw, useRouter as vueUseRouter } from 'vue-router'
|
||||||
import type { NavigateToOptions } from '~~/../../../packages/nuxt/dist/app/composables/router'
|
|
||||||
// eslint-disable-next-line import/order
|
|
||||||
import { isVue3 } from '#app'
|
import { isVue3 } from '#app'
|
||||||
|
import type { NavigateToOptions } from '~~/../../../packages/nuxt/dist/app/composables/router'
|
||||||
import { defineNuxtConfig } from '~~/../../../packages/nuxt/config'
|
import { defineNuxtConfig } from '~~/../../../packages/nuxt/config'
|
||||||
import { useRouter } from '#imports'
|
import { useRouter } from '#imports'
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user