mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-28 00:22:05 +00:00
refactor: move things to utils and more linear setup for server component
This commit is contained in:
parent
6f3406e640
commit
282763c1b9
@ -2,9 +2,7 @@ import type { Component } from 'vue'
|
|||||||
import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onMounted, ref, toRaw, watch } from 'vue'
|
import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onMounted, ref, toRaw, watch } from 'vue'
|
||||||
import { debounce } from 'perfect-debounce'
|
import { debounce } from 'perfect-debounce'
|
||||||
import { hash } from 'ohash'
|
import { hash } from 'ohash'
|
||||||
import { appendResponseHeader } from 'h3'
|
|
||||||
import { useHead } from '@unhead/vue'
|
import { useHead } from '@unhead/vue'
|
||||||
import { randomUUID } from 'uncrypto'
|
|
||||||
import { joinURL, withQuery } from 'ufo'
|
import { joinURL, withQuery } from 'ufo'
|
||||||
import type { FetchResponse } from 'ofetch'
|
import type { FetchResponse } from 'ofetch'
|
||||||
import { join } from 'pathe'
|
import { join } from 'pathe'
|
||||||
@ -12,16 +10,11 @@ import { join } from 'pathe'
|
|||||||
// 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 { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
||||||
import { useRequestEvent } from '../composables/ssr'
|
import { SLOTNAME_RE, SSR_UID_RE, UID_ATTR, getFragmentHTML, getSlotProps, nuxtIslandProps, pKey } from './utils'
|
||||||
import { getFragmentHTML, getSlotProps } from './utils'
|
|
||||||
|
|
||||||
// @ts-expect-error virtual file
|
// @ts-expect-error virtual file
|
||||||
import { remoteComponentIslands, selectiveClient } from '#build/nuxt.config.mjs'
|
import { remoteComponentIslands, selectiveClient } from '#build/nuxt.config.mjs'
|
||||||
|
|
||||||
const pKey = '_islandPromises'
|
|
||||||
const SSR_UID_RE = /nuxt-ssr-component-uid="([^"]*)"/
|
|
||||||
const UID_ATTR = /nuxt-ssr-component-uid(="([^"]*)")?/
|
|
||||||
const SLOTNAME_RE = /nuxt-ssr-slot-name="([^"]*)"/g
|
|
||||||
const SLOT_FALLBACK_RE = /<div nuxt-slot-fallback-start="([^"]*)"[^>]*><\/div>(((?!<div nuxt-slot-fallback-end[^>]*>)[\s\S])*)<div nuxt-slot-fallback-end[^>]*><\/div>/g
|
const SLOT_FALLBACK_RE = /<div nuxt-slot-fallback-start="([^"]*)"[^>]*><\/div>(((?!<div nuxt-slot-fallback-end[^>]*>)[\s\S])*)<div nuxt-slot-fallback-end[^>]*><\/div>/g
|
||||||
|
|
||||||
let id = 0
|
let id = 0
|
||||||
@ -55,29 +48,10 @@ function emptyPayload () {
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'NuxtIsland',
|
name: 'NuxtIsland',
|
||||||
props: {
|
props: {
|
||||||
name: {
|
...nuxtIslandProps
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
lazy: Boolean,
|
|
||||||
props: {
|
|
||||||
type: Object,
|
|
||||||
default: () => undefined
|
|
||||||
},
|
|
||||||
context: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
source: {
|
|
||||||
type: String,
|
|
||||||
default: () => undefined
|
|
||||||
},
|
|
||||||
dangerouslyLoadClientComponents: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async setup (props, { slots, expose }) {
|
async setup (props, { slots, expose }) {
|
||||||
|
// used to force re-render the static content
|
||||||
const key = ref(0)
|
const key = ref(0)
|
||||||
const canLoadClientComponent = computed(() => selectiveClient && (props.dangerouslyLoadClientComponents || !props.source))
|
const canLoadClientComponent = computed(() => selectiveClient && (props.dangerouslyLoadClientComponents || !props.source))
|
||||||
const error = ref<unknown>(null)
|
const error = ref<unknown>(null)
|
||||||
@ -120,6 +94,7 @@ export default defineComponent({
|
|||||||
const ssrHTML = ref<string>(getFragmentHTML(instance.vnode?.el ?? null, true)?.join('') || '')
|
const ssrHTML = ref<string>(getFragmentHTML(instance.vnode?.el ?? null, true)?.join('') || '')
|
||||||
|
|
||||||
const slotProps = computed(() => getSlotProps(ssrHTML.value))
|
const slotProps = computed(() => getSlotProps(ssrHTML.value))
|
||||||
|
// during hydration we directly retrieve the uid from the payload
|
||||||
const uid = ref<string>(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? getId())
|
const uid = ref<string>(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? getId())
|
||||||
const availableSlots = computed(() => [...ssrHTML.value.matchAll(SLOTNAME_RE)].map(m => m[1]))
|
const availableSlots = computed(() => [...ssrHTML.value.matchAll(SLOTNAME_RE)].map(m => m[1]))
|
||||||
|
|
||||||
@ -128,6 +103,7 @@ export default defineComponent({
|
|||||||
let html = ssrHTML.value
|
let html = ssrHTML.value
|
||||||
|
|
||||||
if (!canLoadClientComponent.value) {
|
if (!canLoadClientComponent.value) {
|
||||||
|
// replace all client components with their static content
|
||||||
for (const [key, value] of Object.entries(nonReactivePayload.teleports || {})) {
|
for (const [key, value] of Object.entries(nonReactivePayload.teleports || {})) {
|
||||||
html = html.replace(new RegExp(`<div [^>]*nuxt-ssr-client="${key}"[^>]*>`), (full) => {
|
html = html.replace(new RegExp(`<div [^>]*nuxt-ssr-client="${key}"[^>]*>`), (full) => {
|
||||||
return full + value
|
return full + value
|
||||||
@ -184,6 +160,7 @@ export default defineComponent({
|
|||||||
ssrHTML.value = res.html.replace(UID_ATTR, () => {
|
ssrHTML.value = res.html.replace(UID_ATTR, () => {
|
||||||
return `nuxt-ssr-component-uid="${getId()}"`
|
return `nuxt-ssr-component-uid="${getId()}"`
|
||||||
})
|
})
|
||||||
|
// force re-render the static content
|
||||||
key.value++
|
key.value++
|
||||||
error.value = null
|
error.value = null
|
||||||
|
|
||||||
@ -196,7 +173,8 @@ export default defineComponent({
|
|||||||
nonReactivePayload.teleports = res.teleports
|
nonReactivePayload.teleports = res.teleports
|
||||||
nonReactivePayload.chunks = res.chunks
|
nonReactivePayload.chunks = res.chunks
|
||||||
|
|
||||||
// must await next tick for Teleport to work correctly with static node re-rendering
|
// must await next tick for Teleport to work correctly so vue can teleport the content to the new static node
|
||||||
|
// teleport update is based on uid
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
setUid()
|
setUid()
|
||||||
@ -230,6 +208,7 @@ export default defineComponent({
|
|||||||
return [slots.fallback?.({ error: error.value }) ?? createVNode('div')]
|
return [slots.fallback?.({ error: error.value }) ?? createVNode('div')]
|
||||||
}
|
}
|
||||||
const nodes = [createVNode(Fragment, {
|
const nodes = [createVNode(Fragment, {
|
||||||
|
// static nodes in build need to be keyed to force it to re-render
|
||||||
key: key.value
|
key: key.value
|
||||||
}, [h(createStaticVNode(html.value || '<div></div>', 1))])]
|
}, [h(createStaticVNode(html.value || '<div></div>', 1))])]
|
||||||
|
|
||||||
|
@ -9,40 +9,15 @@ import { joinURL, withQuery } from 'ufo'
|
|||||||
import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
|
import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
|
||||||
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
||||||
import { prerenderRoutes, useRequestEvent } from '../composables/ssr'
|
import { prerenderRoutes, useRequestEvent } from '../composables/ssr'
|
||||||
import { getSlotProps } from './utils'
|
import { SLOTNAME_RE, SSR_UID_RE, UID_ATTR, getSlotProps, nuxtIslandProps, pKey } from './utils'
|
||||||
|
|
||||||
// @ts-expect-error virtual file
|
// @ts-expect-error virtual file
|
||||||
import { remoteComponentIslands, selectiveClient } from '#build/nuxt.config.mjs'
|
import { remoteComponentIslands, selectiveClient } from '#build/nuxt.config.mjs'
|
||||||
|
|
||||||
const pKey = '_islandPromises'
|
|
||||||
const SSR_UID_RE = /nuxt-ssr-component-uid="([^"]*)"/
|
|
||||||
const UID_ATTR = /nuxt-ssr-component-uid(="([^"]*)")?/
|
|
||||||
const SLOTNAME_RE = /nuxt-ssr-slot-name="([^"]*)"/g
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'NuxtIsland',
|
name: 'NuxtIsland',
|
||||||
props: {
|
props: {
|
||||||
name: {
|
...nuxtIslandProps
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
lazy: Boolean,
|
|
||||||
props: {
|
|
||||||
type: Object,
|
|
||||||
default: () => undefined
|
|
||||||
},
|
|
||||||
context: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
source: {
|
|
||||||
type: String,
|
|
||||||
default: () => undefined
|
|
||||||
},
|
|
||||||
dangerouslyLoadClientComponents: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async setup(props, { slots }) {
|
async setup(props, { slots }) {
|
||||||
const error = ref<unknown>(null)
|
const error = ref<unknown>(null)
|
||||||
@ -52,7 +27,6 @@ export default defineComponent({
|
|||||||
const hashId = computed(() => hash([props.name, filteredProps.value, props.context, props.source]))
|
const hashId = computed(() => hash([props.name, filteredProps.value, props.context, props.source]))
|
||||||
const event = useRequestEvent()
|
const event = useRequestEvent()
|
||||||
|
|
||||||
|
|
||||||
function setPayload(key: string, result: NuxtIslandResponse) {
|
function setPayload(key: string, result: NuxtIslandResponse) {
|
||||||
nuxtApp.payload.data[key] = {
|
nuxtApp.payload.data[key] = {
|
||||||
__nuxt_island: {
|
__nuxt_island: {
|
||||||
@ -69,11 +43,7 @@ export default defineComponent({
|
|||||||
...result
|
...result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const nonReactivePayload: Pick<NuxtIslandResponse, 'chunks' | 'props' | 'teleports'> = {
|
const teleports: NuxtIslandResponse['teleports'] = {}
|
||||||
chunks: {},
|
|
||||||
props: {},
|
|
||||||
teleports: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ssrHTML = ref<string>('')
|
const ssrHTML = ref<string>('')
|
||||||
|
|
||||||
@ -116,37 +86,33 @@ export default defineComponent({
|
|||||||
setPayload(key, result)
|
setPayload(key, result)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchComponent(force = false) {
|
try {
|
||||||
nuxtApp[pKey] = nuxtApp[pKey] || {}
|
nuxtApp[pKey] = nuxtApp[pKey] || {}
|
||||||
if (!nuxtApp[pKey][uid.value]) {
|
if (!nuxtApp[pKey][uid.value]) {
|
||||||
nuxtApp[pKey][uid.value] = _fetchComponent().finally(() => {
|
nuxtApp[pKey][uid.value] = _fetchComponent().finally(() => {
|
||||||
delete nuxtApp[pKey]![uid.value]
|
delete nuxtApp[pKey]![uid.value]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
try {
|
const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value]
|
||||||
const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value]
|
cHead.value.link = res.head.link
|
||||||
cHead.value.link = res.head.link
|
cHead.value.style = res.head.style
|
||||||
cHead.value.style = res.head.style
|
ssrHTML.value = res.html.replace(UID_ATTR, () => {
|
||||||
ssrHTML.value = res.html.replace(UID_ATTR, () => {
|
return `nuxt-ssr-component-uid="${randomUUID()}"`
|
||||||
return `nuxt-ssr-component-uid="${randomUUID()}"`
|
})
|
||||||
})
|
Object.assign(teleports, res.teleports)
|
||||||
nonReactivePayload.teleports = res.teleports
|
|
||||||
nonReactivePayload.chunks = res.chunks
|
|
||||||
|
|
||||||
setUid()
|
setUid()
|
||||||
} catch (e) {
|
} catch(e) {
|
||||||
error.value = e
|
error.value = e
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetchComponent()
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (!ssrHTML.value || error.value) {
|
if (!ssrHTML.value || error.value) {
|
||||||
return [slots.fallback?.({ error: error.value }) ?? createVNode('div')]
|
return [slots.fallback?.({ error: error.value }) ?? createVNode('div')]
|
||||||
}
|
}
|
||||||
const nodes = [createVNode(Fragment, {}, [h(createStaticVNode(ssrHTML.value || '<div></div>', 1))])]
|
const nodes = [createVNode(Fragment, null, [h(createStaticVNode(ssrHTML.value || '<div></div>', 1))])]
|
||||||
|
|
||||||
// render slots and teleports
|
// render slots and teleports
|
||||||
if (uid.value && ssrHTML.value) {
|
if (uid.value && ssrHTML.value) {
|
||||||
@ -157,7 +123,7 @@ export default defineComponent({
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const [id, html] of Object.entries(nonReactivePayload.teleports ?? {})) {
|
for (const [id, html] of Object.entries(teleports ?? {})) {
|
||||||
nodes.push(createVNode(Teleport, { to: `uid=${uid.value};client=${id}` }, {
|
nodes.push(createVNode(Teleport, { to: `uid=${uid.value};client=${id}` }, {
|
||||||
default: () => [createStaticVNode(html, 1)]
|
default: () => [createStaticVNode(html, 1)]
|
||||||
}))
|
}))
|
||||||
|
@ -185,3 +185,32 @@ export function getSlotProps (html: string) {
|
|||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const pKey = '_islandPromises'
|
||||||
|
export const SSR_UID_RE = /nuxt-ssr-component-uid="([^"]*)"/
|
||||||
|
export const UID_ATTR = /nuxt-ssr-component-uid(="([^"]*)")?/
|
||||||
|
export const SLOTNAME_RE = /nuxt-ssr-slot-name="([^"]*)"/g
|
||||||
|
|
||||||
|
export const nuxtIslandProps = {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
lazy: Boolean,
|
||||||
|
props: {
|
||||||
|
type: Object,
|
||||||
|
default: () => undefined
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
source: {
|
||||||
|
type: String,
|
||||||
|
default: () => undefined
|
||||||
|
},
|
||||||
|
dangerouslyLoadClientComponents: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user