动态组件
如果你想使用 Vue 的 <component :is="someComputedComponent"> 语法,你需要使用 Vue 提供的 resolveComponent 辅助函数,或者直接从 #components 导入组件并将其传递给 is 属性。
<script setup lang="ts">
import { SomeComponent } from '#components'
const MyButton = resolveComponent('MyButton')
</script>
<template>
<component :is="clickable ? MyButton : 'div'" />
<component :is="SomeComponent" />
</template>
要动态导入组件(也称为延迟加载组件),只需在组件名称前添加 Lazy 前缀。如果组件并非总是需要,这非常有用。
通过使用 Lazy 前缀,你可以延迟加载组件代码,直到需要时才加载,这有助于优化 JavaScript 包大小。
<script setup lang="ts">
const show = ref(false)
</script>
<template>
<div>
<h1>山脉</h1>
<LazyMountainsList v-if="show" />
<button v-if="!show" @click="show = true">显示列表</button>
</div>
</template>
延迟组件非常适合控制应用的块大小,但它们并不总是能提升运行时性能,因为除非有条件渲染,否则它们仍然会急切加载。在现实世界的应用中,有些页面可能包含大量内容和组件,大多数时候并非所有组件都需要在页面加载时立即交互。让它们全部急切加载可能会对性能产生负面影响。
为了优化你的应用,你可能希望延迟某些组件的 hydration,直到它们可见或浏览器完成更重要的任务。Nuxt 通过延迟(或惰性)hydration 支持这一点,允许你控制组件何时变得可交互。Nuxt 提供了一系列内置的 hydration 策略。每个延迟组件只能使用一种策略。
目前 Nuxt 的内置延迟 hydration 仅适用于单文件组件(SFC),并且要求你在模板中定义 prop(而不是通过 v-bind 展开 prop 对象)。它也不适用于直接从 #components 导入的组件。
水合策略
hydrate-on-visible
当组件进入视口时进行 hydration。 pages/index.vue
<template>
<div>
<LazyMyComponent hydrate-on-visible />
</div>
</template>
hydrate-on-idle
当浏览器空闲时进行 hydration。如果需要组件尽快加载但不阻塞关键渲染路径,这很适合。
你还可以传递一个数字作为最大超时时间。pages/index.vue
<template>
<div>
<LazyMyComponent hydrate-on-idle />
</div>
</template>
hydrate-on-interaction
在指定交互(例如点击、鼠标悬停)后进行 hydration。pages/index.vue
<template>
<div>
<LazyMyComponent hydrate-on-interaction="mouseover" />
</div>
</template>
如果不传递事件或事件列表,它默认在 pointerenter、click 和 focus 时进行 hydration。
hydrate-on-media-query
当窗口匹配媒体查询时进行 hydration。pages/index.vue
<template>
<div>
<LazyMyComponent hydrate-on-media-query="(max-width: 768px)" />
</div>
</template>
在底层,这使用了 Vue 内置的 hydrateOnMediaQuery 策略。
hydrate-after
在指定延迟(以毫秒为单位)后进行 hydration。pages/index.vue
<template>
<div>
<LazyMyComponent :hydrate-after="2000" />
</div>
</template>
hydrate-when
基于布尔条件进行 hydration。pages/index.vue
<template>
<div>
<LazyMyComponent :hydrate-when="isReady" />
</div>
</template>
<script setup lang="ts">
const isReady = ref(false)
function myFunction() {
// 触发自定义 hydration 策略...
isReady.value = true
}
</script>
hydrate-never
永不 hydration 组件。pages/index.vue
<template>
<div>
<LazyMyComponent hydrate-never />
</div>
</template>
监听 Hydration 事件
所有延迟 hydration 组件在 hydration 时都会发出一个 @hydrated 事件。pages/index.vue
<template>
<div>
<LazyMyComponent hydrate-on-visible @hydrated="onHydrate" />
</div>
</template>
<script setup lang="ts">
function onHydrate() {
console.log("组件已完成 hydration!")
}
</script>
动态路由文件
当我们访问例如/animation/[name].vue 这样的页面的时候,url会类似于/animation/fade类似于这样,我们可以通过route.params.name来匹配文件的名称从而达到动态路由的效果。
中括号 [] 表示这是一个动态参数,它会捕获 URL 中对应位置的值,并存入 route.params 对象中。
这是一个简单的实现:
<template>
<div class="p-6 max-w-4xl mx-auto">
<div class="border-2 border-dashed border-gray-300 rounded-lg p-12 min-h-[400px] flex items-center justify-center bg-gray-50">
<component
:is="component"
:key="componentKey"
class="w-full"
/>
</div>
<!-- 自动扫描 components/animations/ 目录生成按钮 -->
<div class="mt-6 flex gap-3 flex-wrap">
<NuxtLink
v-for="item in availableAnimations"
:key="item"
:to="`/animation/${item}`"
class="px-4 py-2 rounded-lg border transition-colors"
:class="
item === animationName
? 'bg-blue-600 text-white border-blue-600'
: 'border-gray-300 hover:bg-gray-100'
"
>
{{ item }}
</NuxtLink>
</div>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const router = useRouter()
const animationName = computed(() => route.params.name as string)
const component = defineAsyncComponent({
loader: () => import(`~/components/animations/${animationName.value}.vue`),
onError: () => {
console.warn(`动画组件 ${animationName.value} 不存在,重定向到 fade`)
navigateTo('/animation/fade', { replace: true })
}
})
const componentKey = ref(0)
const animationModules = import.meta.glob('~/components/animations/*.vue')
const availableAnimations = computed(() => {
return Object.keys(animationModules).map((path) => {
const fileName = path.split('/').pop() || ''
return fileName.replace('.vue', '')
})
})
onMounted(() => {
if (!availableAnimations.value.includes(animationName.value)) {
navigateTo(`/animation/${availableAnimations.value[0] || 'fade'}`, { replace: true })
}
})
const replay = () => {
componentKey.value++
}
</script>
我们就是通过 const animationName = computed(() => route.params.name as string) 来获取到的名称,通过nuxt提供的组合式函数返回当前路由的信息对象。用computed来创建响应式变量,当变化的时候自动更新。
最关键的地方在动态导入部分:
const component = defineAsyncComponent({
loader: () => import(`~/components/animations/${animationName.value}.vue`),
onError: () => {
console.warn(`动画组件 ${animationName.value} 不存在,重定向到 fade`)
navigateTo('/animation/fade', { replace: true })
}
})
这里我们使用了一个vue的一个方法,定义一个异步组件,它在运行时是懒加载的。参数可以是一个异步加载函数,或是对加载行为进行更具体定制的一个选项对象。
function defineAsyncComponent(
source: AsyncComponentLoader | AsyncComponentOptions
): Component
type AsyncComponentLoader = () => Promise<Component>
interface AsyncComponentOptions {
loader: AsyncComponentLoader
loadingComponent?: Component
errorComponent?: Component
delay?: number
timeout?: number
suspensible?: boolean
onError?: (
error: Error,
retry: () => void,
fail: () => void,
attempts: number
) => any
}
当我们选中了一个组件的时候,我们就会动态导入对应的vue组件进去,并且还包含了错误处理。通过这种动态拼接模板字符串的方式,我们就可以成功处理下来。Vite 的处理:
Vite 在构建时会扫描代码中的 import() 语句,发现这里是动态拼接的,不会在编译时确定具体文件,而是生成一个动态 chunk,在运行时根据实际路径去加载对应的文件
这里有一个强制重播的key:
const componentKey = ref(0)
const replay = () => {
componentKey.value++
}
在模板中使用了 :key="componentKey":
<component :is="component" :key="componentKey" />
Vue 的 key 机制:
- 当
key不变时,Vue 会复用已有的组件实例。 - 当
key变化时,Vue 会销毁旧实例并创建新实例。
调用 replay() 时:
componentKey从 0 → 1 → 2 → ...- Vue 认为这是一个全新的组件,重新挂载,动画重新播放。
我们这里还进行了自动扫描可用的动画:
const animationModules = import.meta.glob('~/components/animations/*.vue')
import.meta.glob这是 Vite 独有的编译时功能,不是 Node.js 或浏览器 API。 编译时(pnpm dev 或 pnpm build 时),Vite 扫描 ~/components/animations/*.vue 这个路径模式。1. 找到所有匹配的文件,生成一个映射对象, 这个对象在运行时可以直接使用。
编译后生成的对象(伪代码):
const animationModules = {
'/components/animations/fade.vue': () => import('/components/animations/fade.vue'),
'/components/animations/slide.vue': () => import('/components/animations/slide.vue'),
'/components/animations/bounce.vue': () => import('/components/animations/bounce.vue'),
'/components/animations/rotate.vue': () => import('/components/animations/rotate.vue'),
// ... 所有匹配的文件
}