perf(nuxt): remove code duplication in client-only (#30460)

This commit is contained in:
Julien Huang 2025-01-05 15:35:59 +01:00 committed by GitHub
parent 2438130f6b
commit 8b8a731dff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 26 additions and 16 deletions

View File

@ -1,12 +1,14 @@
import { cloneVNode, createElementBlock, createStaticVNode, defineComponent, getCurrentInstance, h, onMounted, provide, ref } from 'vue' import { cloneVNode, createElementBlock, defineComponent, getCurrentInstance, h, onMounted, provide, ref } from 'vue'
import type { ComponentInternalInstance, ComponentOptions, InjectionKey } from 'vue' import type { ComponentInternalInstance, ComponentOptions, InjectionKey } from 'vue'
import { isPromise } from '@vue/shared' import { isPromise } from '@vue/shared'
import { useNuxtApp } from '../nuxt' import { useNuxtApp } from '../nuxt'
import { getFragmentHTML } from './utils'
import ServerPlaceholder from './server-placeholder' import ServerPlaceholder from './server-placeholder'
import { elToStaticVNode } from './utils'
export const clientOnlySymbol: InjectionKey<boolean> = Symbol.for('nuxt:client-only') export const clientOnlySymbol: InjectionKey<boolean> = Symbol.for('nuxt:client-only')
const STATIC_DIV = '<div></div>'
export default defineComponent({ export default defineComponent({
name: 'ClientOnly', name: 'ClientOnly',
inheritAttrs: false, inheritAttrs: false,
@ -54,16 +56,14 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
return (res.children === null || typeof res.children === 'string') return (res.children === null || typeof res.children === 'string')
? cloneVNode(res) ? cloneVNode(res)
: h(res) : h(res)
} else {
const fragment = getFragmentHTML(ctx._.vnode.el ?? null) ?? ['<div></div>']
return createStaticVNode(fragment.join(''), fragment.length)
} }
return elToStaticVNode(ctx._.vnode.el, STATIC_DIV)
} }
} else if (clone.template) { } else if (clone.template) {
// handle runtime-compiler template // handle runtime-compiler template
clone.template = ` clone.template = `
<template v-if="mounted$">${component.template}</template> <template v-if="mounted$">${component.template}</template>
<template v-else><div></div></template> <template v-else>${STATIC_DIV}</template>
` `
} }
@ -105,10 +105,8 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
return (res.children === null || typeof res.children === 'string') return (res.children === null || typeof res.children === 'string')
? cloneVNode(res) ? cloneVNode(res)
: h(res) : h(res)
} else {
const fragment = getFragmentHTML(instance?.vnode.el ?? null) ?? ['<div></div>']
return createStaticVNode(fragment.join(''), fragment.length)
} }
return elToStaticVNode(instance?.vnode.el, STATIC_DIV)
} }
}) })
} else { } else {
@ -117,8 +115,7 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
if (mounted$.value) { if (mounted$.value) {
return h(setupState(...args), ctx.attrs) return h(setupState(...args), ctx.attrs)
} }
const fragment = getFragmentHTML(instance?.vnode.el ?? null) ?? ['<div></div>'] return elToStaticVNode(instance?.vnode.el, STATIC_DIV)
return createStaticVNode(fragment.join(''), fragment.length)
} }
} }
return Object.assign(setupState, { mounted$ }) return Object.assign(setupState, { mounted$ })

View File

@ -1,5 +1,5 @@
import { h } from 'vue' import { createStaticVNode, h } from 'vue'
import type { Component, RendererNode } from 'vue' import type { Component, RendererNode, VNode } from 'vue'
// eslint-disable-next-line // eslint-disable-next-line
import { isString, isPromise, isArray, isObject } from '@vue/shared' import { isString, isPromise, isArray, isObject } from '@vue/shared'
import type { RouteLocationNormalized } from 'vue-router' import type { RouteLocationNormalized } from 'vue-router'
@ -117,9 +117,9 @@ export function vforToArray (source: any): any[] {
* Handles `<!--[-->` Fragment elements * Handles `<!--[-->` Fragment elements
* @param element the element to retrieve the HTML * @param element the element to retrieve the HTML
* @param withoutSlots purge all slots from the HTML string retrieved * @param withoutSlots purge all slots from the HTML string retrieved
* @returns {string[]} An array of string which represent the content of each element. Use `.join('')` to retrieve a component vnode.el HTML * @returns {string[]|undefined} An array of string which represent the content of each element. Use `.join('')` to retrieve a component vnode.el HTML
*/ */
export function getFragmentHTML (element: RendererNode | null, withoutSlots = false): string[] | null { export function getFragmentHTML (element: RendererNode | null, withoutSlots = false): string[] | undefined {
if (element) { if (element) {
if (element.nodeName === '#comment' && element.nodeValue === '[') { if (element.nodeName === '#comment' && element.nodeValue === '[') {
return getFragmentChildren(element, [], withoutSlots) return getFragmentChildren(element, [], withoutSlots)
@ -131,7 +131,6 @@ export function getFragmentHTML (element: RendererNode | null, withoutSlots = fa
} }
return [element.outerHTML] return [element.outerHTML]
} }
return null
} }
function getFragmentChildren (element: RendererNode | null, blocks: string[] = [], withoutSlots = false) { function getFragmentChildren (element: RendererNode | null, blocks: string[] = [], withoutSlots = false) {
@ -151,6 +150,20 @@ function getFragmentChildren (element: RendererNode | null, blocks: string[] = [
return blocks return blocks
} }
/**
* Return a static vnode from an element
* Default to a div if the element is not found and if a fallback is not provided
* @param el renderer node retrieved from the component internal instance
* @param staticNodeFallback fallback string to use if the element is not found. Must be a valid HTML string
*/
export function elToStaticVNode (el: RendererNode | null, staticNodeFallback?: string): VNode {
const fragment: string[] | undefined = el ? getFragmentHTML(el) : staticNodeFallback ? [staticNodeFallback] : undefined
if (fragment) {
return createStaticVNode(fragment.join(''), fragment.length)
}
return h('div')
}
function isStartFragment (element: RendererNode) { function isStartFragment (element: RendererNode) {
return element.nodeName === '#comment' && element.nodeValue === '[' return element.nodeName === '#comment' && element.nodeValue === '['
} }