mirror of
https://github.com/nuxt/nuxt.git
synced 2025-02-07 09:22:27 +00:00
feat: add event-based lazy loading
This commit is contained in:
parent
e3a0242048
commit
06096dcc0c
33
docs/3.api/3.utils/create-event-loader.md
Normal file
33
docs/3.api/3.utils/create-event-loader.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
title: 'createEventLoader'
|
||||||
|
description: A utility function to select the events used for event-based delayed hydration.
|
||||||
|
links:
|
||||||
|
- label: Source
|
||||||
|
icon: i-simple-icons-github
|
||||||
|
to: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/composables/hydrate.ts
|
||||||
|
size: xs
|
||||||
|
---
|
||||||
|
|
||||||
|
You can use this utility to set specific events that would trigger hydration in event-based delayed hydration components.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
- `options`: An array of valid HTML events.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
If you would like to trigger hydration when the element is either clicked or has the mouse over it:
|
||||||
|
|
||||||
|
```vue [pages/index.vue]
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<LazyIdleMyComponent :loader="createEventLoader(['click','mouseover'])"/>
|
||||||
|
</div>
|
||||||
|
<template>
|
||||||
|
```
|
||||||
|
::read-more{to="/docs/guide/directory-structure/components#delayed-hydration"}
|
||||||
|
::
|
||||||
|
|
||||||
|
::read-more{to="https://developer.mozilla.org/en-US/docs/Web/API/Element#events"}
|
||||||
|
Read more on the possible events that can be used.
|
||||||
|
::
|
@ -25,13 +25,19 @@ export const useHydration = <K extends keyof NuxtPayload, T = NuxtPayload[K]> (k
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A `requestIdleCallback` options utility, used for determining custom timeout for idle-callback based delayed hydration.
|
* A `requestIdleCallback` options utility, used to determine custom timeout for idle-callback based delayed hydration.
|
||||||
* @param opts the options object, containing the wanted timeout
|
* @param opts the options object, containing the wanted timeout
|
||||||
*/
|
*/
|
||||||
export const createIdleLoader = (opts: IdleRequestOptions) => opts
|
export const createIdleLoader = (opts: IdleRequestOptions) => opts
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An `IntersectionObserver` options utility, used for determining custom viewport-based delayed hydration.
|
* An `IntersectionObserver` options utility, used to determine custom viewport-based delayed hydration behavior.
|
||||||
* @param opts the options object, containing the wanted viewport options
|
* @param opts the options object, containing the wanted viewport options
|
||||||
*/
|
*/
|
||||||
export const createVisibleLoader = (opts: IntersectionObserverInit) => opts
|
export const createVisibleLoader = (opts: Partial<IntersectionObserverInit>) => opts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility used to determine which events should trigger hydration in components with event-based delayed hydration.
|
||||||
|
* @param events an array of events that will be used to trigger the hydration
|
||||||
|
*/
|
||||||
|
export const createEventLoader = (events: Array<keyof HTMLElementEventMap>) => events
|
||||||
|
@ -43,7 +43,7 @@ export const loaderPlugin = createUnplugin((options: LoaderOptions) => {
|
|||||||
const s = new MagicString(code)
|
const s = new MagicString(code)
|
||||||
const nuxt = tryUseNuxt()
|
const nuxt = tryUseNuxt()
|
||||||
// replace `_resolveComponent("...")` to direct import
|
// replace `_resolveComponent("...")` to direct import
|
||||||
s.replace(/(?<=[ (])_?resolveComponent\(\s*["'](lazy-|Lazy)?(Idle|Visible|idle-|visible-)?([^'"]*)["'][^)]*\)/g, (full: string, lazy: string, modifier: string, name: string) => {
|
s.replace(/(?<=[ (])_?resolveComponent\(\s*["'](lazy-|Lazy)?(Idle|Visible|idle-|visible-|Event|event-)?([^'"]*)["'][^)]*\)/g, (full: string, lazy: string, modifier: string, name: string) => {
|
||||||
const component = findComponent(components, name, options.mode)
|
const component = findComponent(components, name, options.mode)
|
||||||
if (component) {
|
if (component) {
|
||||||
// @ts-expect-error TODO: refactor to nuxi
|
// @ts-expect-error TODO: refactor to nuxi
|
||||||
@ -81,6 +81,12 @@ export const loaderPlugin = createUnplugin((options: LoaderOptions) => {
|
|||||||
identifier += '_delayedIO'
|
identifier += '_delayedIO'
|
||||||
imports.add(`const ${identifier} = createLazyIOClientPage(__defineAsyncComponent(${genDynamicImport(component.filePath, { interopDefault: false })}.then(c => c.${component.export ?? 'default'} || c)))`)
|
imports.add(`const ${identifier} = createLazyIOClientPage(__defineAsyncComponent(${genDynamicImport(component.filePath, { interopDefault: false })}.then(c => c.${component.export ?? 'default'} || c)))`)
|
||||||
break
|
break
|
||||||
|
case 'Event':
|
||||||
|
case 'event-':
|
||||||
|
imports.add(genImport(clientDelayedComponentRuntime, [{ name: 'createLazyEventClientPage' }]))
|
||||||
|
identifier += '_delayedEvent'
|
||||||
|
imports.add(`const ${identifier} = createLazyEventClientPage(__defineAsyncComponent(${genDynamicImport(component.filePath, { interopDefault: false })}.then(c => c.${component.export ?? 'default'} || c)))`)
|
||||||
|
break
|
||||||
case 'Idle':
|
case 'Idle':
|
||||||
case 'idle-':
|
case 'idle-':
|
||||||
imports.add(genImport(clientDelayedComponentRuntime, [{ name: 'createLazyNetworkClientPage' }]))
|
imports.add(genImport(clientDelayedComponentRuntime, [{ name: 'createLazyNetworkClientPage' }]))
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
import type { Component, Ref } from 'vue'
|
import type { Component, Ref, ComponentInternalInstance } from 'vue'
|
||||||
// import ClientOnly from '#app/components/client-only'
|
// import ClientOnly from '#app/components/client-only'
|
||||||
import { getFragmentHTML } from '#app/components/utils'
|
import { getFragmentHTML } from '#app/components/utils'
|
||||||
import { useNuxtApp } from '#app/nuxt'
|
import { useNuxtApp } from '#app/nuxt'
|
||||||
@ -89,3 +89,39 @@ export const createLazyNetworkClientPage = (componentLoader: Component) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const eventsMapper = new WeakMap<ComponentInternalInstance,(() => void)[]>()
|
||||||
|
/* @__NO_SIDE_EFFECTS__ */
|
||||||
|
export const createLazyEventClientPage = (componentLoader: Component) => {
|
||||||
|
return defineComponent({
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup (_, { attrs }) {
|
||||||
|
if (import.meta.server) {
|
||||||
|
return () => h(componentLoader, attrs)
|
||||||
|
}
|
||||||
|
const nuxt = useNuxtApp()
|
||||||
|
const instance = getCurrentInstance()!
|
||||||
|
const isTriggered = ref(false)
|
||||||
|
const events: string[] = attrs.loader ?? ['mouseover']
|
||||||
|
|
||||||
|
const registeredEvents: (() => void)[] = []
|
||||||
|
onMounted(() => {
|
||||||
|
events.forEach((event) => {
|
||||||
|
const handler = () => {
|
||||||
|
isTriggered.value = true
|
||||||
|
registeredEvents.forEach((remove) => remove())
|
||||||
|
eventsMapper.delete(instance)
|
||||||
|
}
|
||||||
|
instance.vnode.el.addEventListener(event, handler)
|
||||||
|
registeredEvents.push(() => instance.vnode.el.removeEventListener(event, handler))
|
||||||
|
})
|
||||||
|
eventsMapper.set(instance, registeredEvents)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
registeredEvents?.forEach((remove) => remove())
|
||||||
|
eventsMapper.delete(instance)
|
||||||
|
})
|
||||||
|
return () => isTriggered.value ? h(componentLoader, attrs) : (instance.vnode.el && nuxt.isHydrating) ? createVNode(createStaticVNode(getFragmentHTML(instance.vnode.el ?? null, true)?.join('') || '', 1)) : null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -42,7 +42,7 @@ const granularAppPresets: InlinePreset[] = [
|
|||||||
from: '#app/composables/asyncData',
|
from: '#app/composables/asyncData',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
imports: ['useHydration', 'createVisibleLoader', 'createIdleLoader'],
|
imports: ['useHydration', 'createVisibleLoader', 'createIdleLoader', 'createEventLoader'],
|
||||||
from: '#app/composables/hydrate',
|
from: '#app/composables/hydrate',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2675,6 +2675,12 @@ describe('lazy import components', () => {
|
|||||||
await page.waitForLoadState('networkidle')
|
await page.waitForLoadState('networkidle')
|
||||||
expect(await page.locator('body').getByText('This shouldn\'t be visible at first with network!').all()).toHaveLength(1)
|
expect(await page.locator('body').getByText('This shouldn\'t be visible at first with network!').all()).toHaveLength(1)
|
||||||
expect(await page.locator('body').getByText('This should be visible at first with viewport!').all()).toHaveLength(1)
|
expect(await page.locator('body').getByText('This should be visible at first with viewport!').all()).toHaveLength(1)
|
||||||
|
expect(await page.locator('body').getByText('This should be visible at first with events!').all()).toHaveLength(1)
|
||||||
|
const component = await page.locator('#lazyevent');
|
||||||
|
const rect = component.boundingBox()
|
||||||
|
await page.mouse.move(rect.x + rect.width / 2, rect.y + rect.height / 2)
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
expect(await page.locator('body').getByText('This shouldn\'t be visible at first with events!').all()).toHaveLength(1)
|
||||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight))
|
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight))
|
||||||
await page.waitForTimeout(1000) // attempt a hard-coded delay to ensure IO isn't triggered after network is idle
|
await page.waitForTimeout(1000) // attempt a hard-coded delay to ensure IO isn't triggered after network is idle
|
||||||
await page.waitForLoadState('networkidle')
|
await page.waitForLoadState('networkidle')
|
||||||
|
5
test/fixtures/basic/components/DelayedEvent.client.vue
vendored
Normal file
5
test/fixtures/basic/components/DelayedEvent.client.vue
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
This shouldn't be visible at first with events!
|
||||||
|
</div>
|
||||||
|
</template>
|
5
test/fixtures/basic/components/DelayedEvent.server.vue
vendored
Normal file
5
test/fixtures/basic/components/DelayedEvent.server.vue
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
This should be visible at first with events!
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -3,6 +3,7 @@
|
|||||||
<LazyNCompAll message="lazy-named-comp-all" />
|
<LazyNCompAll message="lazy-named-comp-all" />
|
||||||
<LazyNCompClient message="lazy-named-comp-client" />
|
<LazyNCompClient message="lazy-named-comp-client" />
|
||||||
<LazyNCompServer message="lazy-named-comp-server" />
|
<LazyNCompServer message="lazy-named-comp-server" />
|
||||||
|
<LazyEventDelayedEvent id="lazyevent"/>
|
||||||
<LazyIdleDelayedNetwork />
|
<LazyIdleDelayedNetwork />
|
||||||
<div style="height:3000px">
|
<div style="height:3000px">
|
||||||
This is a very tall div
|
This is a very tall div
|
||||||
|
Loading…
Reference in New Issue
Block a user