feat: introduce dangerouslyLoadCLientComponent

This commit is contained in:
julien huang 2023-08-15 17:21:50 +02:00
parent 15782f7a71
commit a3f76bfcb6
2 changed files with 48 additions and 26 deletions

View File

@ -27,6 +27,7 @@ const SLOT_FALLBACK_RE = /<div nuxt-slot-fallback-start="([^"]*)"[^>]*><\/div>((
let id = 0 let id = 0
const getId = import.meta.client ? () => (id++).toString() : randomUUID const getId = import.meta.client ? () => (id++).toString() : randomUUID
const components = import.meta.client ? new Map<string, Component>() : undefined const components = import.meta.client ? new Map<string, Component>() : undefined
async function loadComponents (source = '/', paths: Record<string, string>) { async function loadComponents (source = '/', paths: Record<string, string>) {
@ -63,15 +64,22 @@ export default defineComponent({
source: { source: {
type: String, type: String,
default: () => undefined default: () => undefined
},
dangerouslyLoadClientComponents: {
type: Boolean,
default: false
} }
}, },
async setup (props, { slots }) { async setup (props, { slots }) {
const key = ref(0)
const canLoadClientComponent = computed(() => props.dangerouslyLoadClientComponents || !props.source)
const error = ref<unknown>(null) const error = ref<unknown>(null)
const config = useRuntimeConfig() const config = useRuntimeConfig()
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
const hashId = computed(() => hash([props.name, props.props, props.context, props.source])) const hashId = computed(() => hash([props.name, props.props, props.context, props.source]))
const instance = getCurrentInstance()! const instance = getCurrentInstance()!
const event = useRequestEvent() const event = useRequestEvent()
// TODO: remove use of `$fetch.raw` when nitro 503 issues on windows dev server are resolved // TODO: remove use of `$fetch.raw` when nitro 503 issues on windows dev server are resolved
const eventFetch = import.meta.server ? event.fetch : import.meta.dev ? $fetch.raw : globalThis.fetch const eventFetch = import.meta.server ? event.fetch : import.meta.dev ? $fetch.raw : globalThis.fetch
const mounted = ref(false) const mounted = ref(false)
@ -90,6 +98,7 @@ export default defineComponent({
} }
const ssrHTML = ref<string>('') const ssrHTML = ref<string>('')
if (import.meta.client) { if (import.meta.client) {
const renderedHTML = getFragmentHTML(instance.vnode?.el ?? null).join('') const renderedHTML = getFragmentHTML(instance.vnode?.el ?? null).join('')
if (renderedHTML && nuxtApp.isHydrating) { if (renderedHTML && nuxtApp.isHydrating) {
@ -107,18 +116,25 @@ export default defineComponent({
} }
ssrHTML.value = renderedHTML ssrHTML.value = renderedHTML
} }
const slotProps = computed(() => getSlotProps(ssrHTML.value)) const slotProps = computed(() => getSlotProps(ssrHTML.value))
const uid = ref<string>(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? randomUUID()) const uid = ref<string>(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? randomUUID())
const availableSlots = computed(() => [...ssrHTML.value.matchAll(SLOTNAME_RE)].map(m => m[1])) const availableSlots = computed(() => [...ssrHTML.value.matchAll(SLOTNAME_RE)].map(m => m[1]))
// no need for reactivity // no need for reactivity
// eslint-disable-next-line vue/no-setup-props-destructure const interactivePayload: Pick<NuxtIslandResponse, 'chunks' | 'props' | 'teleports'> = import.meta.client && nuxtApp.isHydrating ? toRaw(nuxtApp.payload.data)[`${props.name}_${hashId.value}_interactive`] : {}
let interactiveProps: Record<string, Record<string, any>> = import.meta.client && nuxtApp.isHydrating ? toRaw(nuxtApp.payload.data[`${props.name}_${hashId.value}_interactive`].props) : {}
const interactiveChunksList = import.meta.client && nuxtApp.isHydrating ? nuxtApp.payload.data[`${props.name}_${hashId.value}_interactive`].chunks : {}
let interactiveTeleports: Record<string, string> = {}
const html = computed(() => { const html = computed(() => {
const currentSlots = Object.keys(slots) const currentSlots = Object.keys(slots)
const html = ssrHTML.value let html = ssrHTML.value
if (import.meta.client && !canLoadClientComponent.value && Object.keys(interactivePayload.teleports).length) {
for (const key in interactivePayload.teleports) {
html = html.replace(new RegExp(`<div [^>]*nuxt-ssr-client="${key}"[^>]*>`), (full) => {
return full + interactivePayload.teleports[key]
})
}
}
return html.replace(SLOT_FALLBACK_RE, (full, slotName, content) => { return html.replace(SLOT_FALLBACK_RE, (full, slotName, content) => {
// remove fallback to insert slots // remove fallback to insert slots
@ -128,6 +144,7 @@ export default defineComponent({
return content return content
}) })
}) })
function setUid () { function setUid () {
uid.value = ssrHTML.value.match(SSR_UID_RE)?.[1] ?? getId() as string uid.value = ssrHTML.value.match(SSR_UID_RE)?.[1] ?? getId() as string
} }
@ -162,7 +179,7 @@ export default defineComponent({
setPayload(key, result) setPayload(key, result)
return result return result
} }
const key = ref(0)
async function fetchComponent (force = false) { async function fetchComponent (force = false) {
nuxtApp[pKey] = nuxtApp[pKey] || {} nuxtApp[pKey] = nuxtApp[pKey] || {}
if (!nuxtApp[pKey][uid.value]) { if (!nuxtApp[pKey][uid.value]) {
@ -179,21 +196,25 @@ export default defineComponent({
}) })
key.value++ key.value++
error.value = null error.value = null
nuxtApp.payload.data[`${props.name}_${hashId.value}_interactive`] = {
chunks: res.chunks,
props: res.props,
teleports: res.teleports
}
if (import.meta.client) {
if (canLoadClientComponent.value) {
await loadComponents(props.source, res.chunks)
}
interactivePayload.props = res.props
}
interactivePayload.teleports = res.teleports
interactivePayload.chunks = res.chunks
if (import.meta.client) { if (import.meta.client) {
// must await next tick for Teleport to work correctly with static node re-rendering // must await next tick for Teleport to work correctly with static node re-rendering
await nextTick() await nextTick()
} }
nuxtApp.payload.data[`${props.name}_${hashId.value}_interactive`] = {
chunks: res.chunks,
props: res.props
}
if (import.meta.client) {
await loadComponents(props.source, res.chunks)
interactiveProps = res.props
} else {
interactiveTeleports = res.teleports || {}
}
setUid() setUid()
} catch (e) { } catch (e) {
@ -215,8 +236,8 @@ export default defineComponent({
fetchComponent() fetchComponent()
} else if (import.meta.server || !nuxtApp.isHydrating) { } else if (import.meta.server || !nuxtApp.isHydrating) {
await fetchComponent() await fetchComponent()
} else if (nuxtApp.isHydrating) { } else if (nuxtApp.isHydrating && canLoadClientComponent.value) {
await loadComponents(props.source, interactiveChunksList) await loadComponents(props.source, interactivePayload.chunks)
} }
return () => { return () => {
@ -237,14 +258,14 @@ export default defineComponent({
} }
} }
if (import.meta.server) { if (import.meta.server) {
for (const [id, html] of Object.entries(interactiveTeleports)) { for (const [id, html] of Object.entries(interactivePayload.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)]
})) }))
} }
} }
if (import.meta.client && html.value.includes('nuxt-ssr-client')) { if (import.meta.client && canLoadClientComponent.value && html.value.includes('nuxt-ssr-client')) {
for (const [id, props] of Object.entries(interactiveProps)) { for (const [id, props] of Object.entries(interactivePayload.props)) {
// @ts-expect-error _ is the component's default export in build chunks // @ts-expect-error _ is the component's default export in build chunks
const component = components!.get(id.split('-')[0])!._ ?? components!.get(id.split('-')[0])! const component = components!.get(id.split('-')[0])!._ ?? components!.get(id.split('-')[0])!
const vnode = createVNode(Teleport, { to: `[nuxt-ssr-component-uid='${uid.value}'] [nuxt-ssr-client="${id}"]` }, { const vnode = createVNode(Teleport, { to: `[nuxt-ssr-component-uid='${uid.value}'] [nuxt-ssr-client="${id}"]` }, {

View File

@ -6,12 +6,13 @@
{{ count }} {{ count }}
</div> </div>
<div style="border: solid 1px red;"> <SugarCounter :multiplier="1" />
<div style="border: solid 1px red;">
The component bellow is not a slot but declared as interactive The component bellow is not a slot but declared as interactive
<SugarCounter nuxt-client :multiplier="1" /> <SugarCounter nuxt-client :multiplier="1" />
</div> </div>
<slot /> <slot />
</div> </div>
</template> </template>