mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-11 08:33:53 +00:00
perf(nuxt): avoid making client-only component setup async (#28334)
This commit is contained in:
parent
dfb54e94bb
commit
a4ef08059d
@ -1,5 +1,6 @@
|
|||||||
import { cloneVNode, createElementBlock, createStaticVNode, defineComponent, getCurrentInstance, h, onMounted, provide, ref } from 'vue'
|
import { cloneVNode, createElementBlock, createStaticVNode, 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 { useNuxtApp } from '../nuxt'
|
import { useNuxtApp } from '../nuxt'
|
||||||
import { getFragmentHTML } from './utils'
|
import { getFragmentHTML } from './utils'
|
||||||
|
|
||||||
@ -42,9 +43,10 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
|
|||||||
const clone = { ...component }
|
const clone = { ...component }
|
||||||
|
|
||||||
if (clone.render) {
|
if (clone.render) {
|
||||||
// override the component render (non script setup component)
|
// override the component render (non script setup component) or dev mode
|
||||||
clone.render = (ctx: any, cache: any, $props: any, $setup: any, $data: any, $options: any) => {
|
clone.render = (ctx: any, cache: any, $props: any, $setup: any, $data: any, $options: any) => {
|
||||||
if ($setup.mounted$ ?? ctx.mounted$) {
|
// import.meta.client for server-side treeshakking
|
||||||
|
if (import.meta.client && ($setup.mounted$ ?? ctx.mounted$)) {
|
||||||
const res = component.render?.bind(ctx)(ctx, cache, $props, $setup, $data, $options)
|
const res = component.render?.bind(ctx)(ctx, cache, $props, $setup, $data, $options)
|
||||||
return (res.children === null || typeof res.children === 'string')
|
return (res.children === null || typeof res.children === 'string')
|
||||||
? cloneVNode(res)
|
? cloneVNode(res)
|
||||||
@ -63,33 +65,39 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clone.setup = (props, ctx) => {
|
clone.setup = (props, ctx) => {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
const mounted$ = ref(import.meta.client && nuxtApp.isHydrating === false)
|
||||||
const instance = getCurrentInstance()!
|
const instance = getCurrentInstance()!
|
||||||
|
|
||||||
const attrs = { ...instance.attrs }
|
if (import.meta.server || nuxtApp.isHydrating) {
|
||||||
|
const attrs = { ...instance.attrs }
|
||||||
|
// remove existing directives during hydration
|
||||||
|
const directives = extractDirectives(instance)
|
||||||
|
// prevent attrs inheritance since a staticVNode is rendered before hydration
|
||||||
|
for (const key in attrs) {
|
||||||
|
delete instance.attrs[key]
|
||||||
|
}
|
||||||
|
|
||||||
// remove existing directives during hydration
|
onMounted(() => {
|
||||||
const directives = extractDirectives(instance)
|
Object.assign(instance.attrs, attrs)
|
||||||
// prevent attrs inheritance since a staticVNode is rendered before hydration
|
instance.vnode.dirs = directives
|
||||||
for (const key in attrs) {
|
})
|
||||||
delete instance.attrs[key]
|
|
||||||
}
|
}
|
||||||
const mounted$ = ref(false)
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
Object.assign(instance.attrs, attrs)
|
|
||||||
instance.vnode.dirs = directives
|
|
||||||
mounted$.value = true
|
mounted$.value = true
|
||||||
})
|
})
|
||||||
|
const setupState = component.setup?.(props, ctx) || {}
|
||||||
|
|
||||||
return Promise.resolve(component.setup?.(props, ctx) || {})
|
if (isPromise(setupState)) {
|
||||||
.then((setupState) => {
|
return Promise.resolve(setupState).then((setupState) => {
|
||||||
if (typeof setupState !== 'function') {
|
if (typeof setupState !== 'function') {
|
||||||
setupState = setupState || {}
|
setupState = setupState || {}
|
||||||
setupState.mounted$ = mounted$
|
setupState.mounted$ = mounted$
|
||||||
return setupState
|
return setupState
|
||||||
}
|
}
|
||||||
return (...args: any[]) => {
|
return (...args: any[]) => {
|
||||||
if (mounted$.value) {
|
if (import.meta.client && (mounted$.value || !nuxtApp.isHydrating)) {
|
||||||
const res = setupState(...args)
|
const res = setupState(...args)
|
||||||
return (res.children === null || typeof res.children === 'string')
|
return (res.children === null || typeof res.children === 'string')
|
||||||
? cloneVNode(res)
|
? cloneVNode(res)
|
||||||
@ -100,6 +108,20 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
if (typeof setupState === 'function') {
|
||||||
|
return (...args: any[]) => {
|
||||||
|
if (mounted$.value) {
|
||||||
|
return h(setupState(...args), ctx.attrs)
|
||||||
|
}
|
||||||
|
const fragment = getFragmentHTML(instance?.vnode.el ?? null) ?? ['<div></div>']
|
||||||
|
return import.meta.client
|
||||||
|
? createStaticVNode(fragment.join(''), fragment.length) :
|
||||||
|
h('div', ctx.attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.assign(setupState, { mounted$ })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cache.set(component, clone)
|
cache.set(component, clone)
|
||||||
|
@ -390,12 +390,14 @@ describe('pages', () => {
|
|||||||
expect(await page.locator('.client-only-script button').innerHTML()).toContain('2')
|
expect(await page.locator('.client-only-script button').innerHTML()).toContain('2')
|
||||||
expect(await page.locator('.string-stateful-script').innerHTML()).toContain('1')
|
expect(await page.locator('.string-stateful-script').innerHTML()).toContain('1')
|
||||||
expect(await page.locator('.string-stateful').innerHTML()).toContain('1')
|
expect(await page.locator('.string-stateful').innerHTML()).toContain('1')
|
||||||
|
const waitForConsoleLog = page.waitForEvent('console', consoleLog => consoleLog.text() === 'has $el')
|
||||||
|
|
||||||
// ensure directives are reactive
|
// ensure directives are reactive
|
||||||
await page.locator('button#show-all').click()
|
await page.locator('button#show-all').click()
|
||||||
await Promise.all(hiddenSelectors.map(selector => page.locator(selector).isVisible()))
|
await Promise.all(hiddenSelectors.map(selector => page.locator(selector).isVisible()))
|
||||||
.then(results => results.forEach(isVisible => expect(isVisible).toBeTruthy()))
|
.then(results => results.forEach(isVisible => expect(isVisible).toBeTruthy()))
|
||||||
|
|
||||||
|
await waitForConsoleLog
|
||||||
expect(pageErrors).toEqual([])
|
expect(pageErrors).toEqual([])
|
||||||
await page.close()
|
await page.close()
|
||||||
// don't expect any errors or warning on client-side navigation
|
// don't expect any errors or warning on client-side navigation
|
||||||
|
16
test/fixtures/basic/components/WrapClientComponent.vue
vendored
Normal file
16
test/fixtures/basic/components/WrapClientComponent.vue
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ClientSetupScript
|
||||||
|
ref="clientSetupScript"
|
||||||
|
class="client-only-script-setup"
|
||||||
|
foo="hello"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const clientSetupScript = ref<{ $el: HTMLElement }>()
|
||||||
|
onMounted(() => {
|
||||||
|
console.log(clientSetupScript.value?.$el as HTMLElement ? 'has $el' : 'no $el')
|
||||||
|
})
|
||||||
|
</script>
|
@ -59,6 +59,7 @@
|
|||||||
class="no-state-hidden"
|
class="no-state-hidden"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<WrapClientComponent v-if="show" />
|
||||||
<button
|
<button
|
||||||
class="test-ref-1"
|
class="test-ref-1"
|
||||||
@click="stringStatefulComp.add"
|
@click="stringStatefulComp.add"
|
||||||
@ -94,16 +95,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Ref } from 'vue'
|
|
||||||
// bypass client import protection to ensure this is treeshaken from .client components
|
// bypass client import protection to ensure this is treeshaken from .client components
|
||||||
import BreaksServer from '~~/components/BreaksServer.client'
|
import BreaksServer from '~~/components/BreaksServer.client'
|
||||||
|
|
||||||
type Comp = Ref<{ add: () => void }>
|
type Comp = { add: () => void }
|
||||||
|
const stringStatefulComp = ref<Comp>(null)
|
||||||
const stringStatefulComp = ref(null) as any as Comp
|
const stringStatefulScriptComp = ref<Comp>(null)
|
||||||
const stringStatefulScriptComp = ref(null) as any as Comp
|
const clientScript = ref<Comp>(null)
|
||||||
const clientScript = ref(null) as any as Comp
|
const clientSetupScript = ref<Comp>(null)
|
||||||
const clientSetupScript = ref(null) as any as Comp
|
|
||||||
const BreakServerComponent = defineAsyncComponent(() => {
|
const BreakServerComponent = defineAsyncComponent(() => {
|
||||||
return import('./../components/BreaksServer.client')
|
return import('./../components/BreaksServer.client')
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user