refactor: move things to utils and more linear setup for server component

This commit is contained in:
julien huang 2023-12-29 22:39:28 +01:00
parent 6f3406e640
commit 282763c1b9
3 changed files with 56 additions and 82 deletions

View File

@ -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))])]

View File

@ -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>('')
@ -117,36 +87,32 @@ export default defineComponent({
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()}"`
}) })
nonReactivePayload.teleports = res.teleports Object.assign(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)]
})) }))

View File

@ -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
}
}