components
Nuxt 会自动导入该目录中的所有组件(以及你正在使用的任何模块所注册的组件)。
-| components/
---| AppHeader.vue
---| AppFooter.vue
<template>
<div>
<AppHeader />
<NuxtPage />
<AppFooter />
</div>
</template>
组件名称
如果你在嵌套目录中有一个组件,例如
-| components/
---| base/
-----| foo/
-------| Button.vue
...那么组件的名称将基于其自身的路径目录和文件名,并移除重复的片段。因此,组件的名称将是
<BaseFooButton />
Button.vue 重命名为 BaseFooButton.vue。如果你想仅根据组件名称而不是路径来自动导入组件,则需要使用配置对象的扩展形式将 pathPrefix 选项设为 false
export default defineNuxtConfig({
components: [
{
path: '~/components',
pathPrefix: false, },
],
})
这会使用与 Nuxt 2 相同的策略来注册组件。例如,~/components/Some/MyComponent.vue 可以作为 <MyComponent> 使用,而不是 <SomeMyComponent>。
动态组件
如果你想使用 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>
resolveComponent 来处理动态组件,请确保除了组件名称之外不要插入任何其他内容,该名称必须是字面量字符串,不能是或包含变量。该字符串在编译阶段会进行静态分析。或者,虽然不推荐,但你可以将所有组件全局注册,这会为所有组件创建异步代码块,并使它们在整个应用程序中可用。
export default defineNuxtConfig({
components: {
+ global: true,
+ dirs: ['~/components']
},
})
你也可以通过将某些组件放置在 ~/components/global 目录中,或者在文件名中使用 .global.vue 后缀,来选择性地全局注册它们。如上所述,每个全局组件都会在一个单独的代码块中渲染,因此请注意不要过度使用此功能。
global 选项也可以按组件目录进行设置。动态导入
要动态导入组件(也称为懒加载组件),你只需在组件名称前添加 Lazy 前缀。如果并非总是需要该组件,这会非常有用。
通过使用 Lazy 前缀,你可以将组件代码的加载推迟到合适的时机,这对于优化 JavaScript 包体积很有帮助。
<script setup lang="ts">
const show = ref(false)
</script>
<template>
<div>
<h1>Mountains</h1>
<LazyMountainsList v-if="show" />
<button
v-if="!show"
@click="show = true"
>
Show List
</button>
</div>
</template>
延迟(或懒)水合
懒组件非常适合控制应用中的代码块大小,但它们并不总能提升运行时性能,因为除非采用条件渲染,否则它们仍会急切加载。在实际应用中,某些页面可能包含大量内容和组件,并且大多数情况下,并非所有组件都需要在页面加载后立即具备交互性。让所有组件都急切加载会对性能产生负面影响。
为了优化应用,你可能希望推迟某些组件的水合,直到它们可见,或者直到浏览器完成更重要的任务。
Nuxt 通过使用懒(或延迟)水合支持这一点,允许你控制组件何时变得可交互。
水合策略
Nuxt 提供了多种内置的水合策略。每个懒组件只能使用一种策略。
hydrate-never 的组件上的 prop 会导致其水合)v-bind 展开一个 prop 对象)。它也不适用于从 #components 直接导入。hydrate-on-visible
当组件在视口中可见时对其进行水合。
<template>
<div>
<LazyMyComponent hydrate-on-visible />
</div>
</template>
hydrateOnVisible 策略。hydrate-on-idle
当浏览器空闲时对组件进行水合。如果你需要组件尽早加载但不阻塞关键渲染路径,这非常适用。
你还可以传递一个数字作为最大超时时间。
<template>
<div>
<LazyMyComponent hydrate-on-idle />
</div>
</template>
hydrateOnIdle 策略。hydrate-on-interaction
在指定的交互(例如点击、鼠标悬停)后对组件进行水合。
<template>
<div>
<LazyMyComponent hydrate-on-interaction="mouseover" />
</div>
</template>
如果你未传递事件或事件列表,它默认会在 pointerenter、click 和 focus 时进行水合。
hydrateOnInteraction 策略。hydrate-on-media-query
当窗口匹配媒体查询时对组件进行水合。
<template>
<div>
<LazyMyComponent hydrate-on-media-query="(max-width: 768px)" />
</div>
</template>
hydrateOnMediaQuery 策略。hydrate-after
在指定的延迟(以毫秒为单位)后对组件进行水合。
<template>
<div>
<LazyMyComponent :hydrate-after="2000" />
</div>
</template>
hydrate-when
根据布尔条件对组件进行水合。
<template>
<div>
<LazyMyComponent :hydrate-when="isReady" />
</div>
</template>
<script setup lang="ts">
const isReady = ref(false)
function myFunction () {
// trigger custom hydration strategy...
isReady.value = true
}
</script>
hydrate-never
永不水合该组件。
<template>
<div>
<LazyMyComponent hydrate-never />
</div>
</template>
监听水合事件
所有延迟水合组件在水合时都会触发 @hydrated 事件。
<template>
<div>
<LazyMyComponent
hydrate-on-visible
@hydrated="onHydrate"
/>
</div>
</template>
<script setup lang="ts">
function onHydrate () {
console.log('Component has been hydrated!')
}
</script>
注意事项与最佳实践
延迟水合可以带来性能优势,但正确使用它至关重要
- 优先处理视口内的内容:避免对首屏关键内容使用延迟水合。它最适合不需要立即加载的内容。
- 条件渲染:在懒组件上使用
v-if="false"时,你可能不需要延迟水合。你可以直接使用普通的懒组件。 - 共享状态:注意多个组件之间的共享状态(
v-model)。在一个组件中更新模型可能会触发绑定到该模型的所有组件的水合。 - 使用每种策略的目标用例:每种策略都针对特定目的进行了优化。
hydrate-when最适合可能并不总是需要水合的组件。hydrate-after适用于可以等待特定时间的组件。hydrate-on-idle适用于可以在浏览器空闲时进行水合的组件。
- 避免在交互式组件上使用
hydrate-never:如果组件需要用户交互,则不应将其设置为永不水合。
直接导入
如果你希望或需要绕过 Nuxt 的自动导入功能,也可以显式从 #components 导入组件。
<script setup lang="ts">
import { LazyMountainsList, NuxtLink } from '#components'
const show = ref(false)
</script>
<template>
<div>
<h1>Mountains</h1>
<LazyMountainsList v-if="show" />
<button
v-if="!show"
@click="show = true"
>
Show List
</button>
<NuxtLink to="/">Home</NuxtLink>
</div>
</template>
自定义目录
默认情况下,仅扫描 ~/components 目录。如果你想添加其他目录,或更改该目录子文件夹中组件的扫描方式,可以在配置中添加额外的目录
export default defineNuxtConfig({
components: [
// ~/calendar-module/components/event/Update.vue => <EventUpdate />
{ path: '~/calendar-module/components' },
// ~/user-module/components/account/UserDeleteDialog.vue => <UserDeleteDialog />
{ path: '~/user-module/components', pathPrefix: false },
// ~/components/special-components/Btn.vue => <SpecialBtn />
{ path: '~/components/special-components', prefix: 'Special' },
// It's important that this comes last if you have overrides you wish to apply
// to sub-directories of `~/components`.
//
// ~/components/Btn.vue => <Btn />
// ~/components/base/Btn.vue => <BaseBtn />
'~/components',
],
})
每个目录条目还接受 pattern 和 ignore 通配符选项,它们控制在 path 中扫描哪些文件。当你的组件位于不遵循默认布局的嵌套文件夹中(例如领域驱动结构)时,这非常有用
export default defineNuxtConfig({
components: [
// ~/domains/blog/components/PostCard.vue => <PostCard />
{
path: '~/domains',
pattern: '*/components/**',
pathPrefix: false,
},
],
})
pattern,则 extensions 选项将无效,因此请确保你的模式与要扫描的文件扩展名相匹配。npm 包
如果你想从 npm 包中自动导入组件,可以使用addComponent在本地模块中注册它们。
import { addComponent, defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
setup () {
// import { MyComponent as MyAutoImportedComponent } from 'my-npm-package'
addComponent({
name: 'MyAutoImportedComponent',
export: 'MyComponent',
filePath: 'my-npm-package',
})
},
})
<template>
<div>
<!-- the component uses the name we specified and is auto-imported -->
<MyAutoImportedComponent />
</div>
</template>
组件扩展名
默认情况下,扩展名在 nuxt.config.ts 的 extensions 键中指定的任何文件都被视为组件。如果你需要限制应注册为组件的文件扩展名,可以使用组件目录声明的扩展形式及其 extensions 键
export default defineNuxtConfig({
components: [
{
path: '~/components',
extensions: ['.vue'], },
],
})
客户端组件
如果某个组件仅打算在客户端渲染,你可以为组件添加 .client 后缀。
| components/
--| Comments.client.vue
<template>
<div>
<!-- this component will only be rendered on client side -->
<Comments />
</div>
</template>
#components 导入。从其真实路径显式导入这些组件不会将其转换为仅客户端组件。.client 组件仅在挂载后渲染。若要使用 onMounted() 访问渲染后的模板,请在 onMounted() 钩子的回调中添加 await nextTick()。服务端组件
服务端组件允许在你的客户端应用中服务端渲染单独的组件。即便你正在生成静态站点,也可以在 Nuxt 中使用服务端组件。这使得构建混合了动态组件、服务端渲染 HTML 甚至静态标记代码块的复杂站点成为可能。
服务端组件可以单独使用,也可以与客户端组件配对使用。
独立服务端组件
独立服务端组件将始终在服务端渲染,也称为岛屿组件。
当它们的 prop 更新时,这将触发一个网络请求,从而就地更新渲染的 HTML。
服务端组件目前处于实验阶段,为了使用它们,你需要在 nuxt.config 中启用“组件岛屿”功能
export default defineNuxtConfig({
experimental: {
componentIslands: true,
},
})
现在,你可以使用 .server 后缀注册仅服务端组件,并在应用程序中的任何位置自动使用它们。
-| components/
---| HighlightedMarkdown.server.vue
<template>
<div>
<!--
this will automatically be rendered on the server, meaning your markdown parsing + highlighting
libraries are not included in your client bundle.
-->
<HighlightedMarkdown markdown="# Headline" />
</div>
</template>
仅服务端组件在底层使用 <NuxtIsland>,这意味着 lazy prop 和 #fallback 插槽都会传递给它。
服务端组件内的客户端组件
experimental.componentIslands.selectiveClient 为 true。你可以通过在你希望在客户端加载的组件上设置 nuxt-client 属性来对组件进行部分水合。
<template>
<div>
<HighlightedMarkdown markdown="# Headline" />
<!-- Counter will be loaded and hydrated client-side -->
<Counter
nuxt-client
:count="5"
/>
</div>
</template>
experimental.componentIsland.selectiveClient 设置为 'deep' 时有效,并且由于它们是在服务端渲染的,因此一旦到达客户端,它们就不再具备交互性。.vue SFC 上使用 nuxt-client。<NuxtLink> 等内置组件会跳过岛屿转换。客户端导航后,你可能会看到 Failed to locate Teleport target,或者链接在没有报错的情况下消失。请将内置组件包装在你自己的 .vue 文件中,并将 nuxt-client 放在该包装器上,或者从内置组件中移除 nuxt-client。请参阅 #29251 和 #26002。服务端组件上下文
当渲染仅服务端组件或岛屿组件时,<NuxtIsland> 会发起一个 fetch 请求并返回一个 NuxtIslandResponse。(如果在服务端渲染,这是一个内部请求;如果是客户端导航渲染,则是在网络标签页中可见的请求。)
这意味着
- 将在服务端创建一个新的 Vue 应用来生成
NuxtIslandResponse。 - 在渲染组件时将创建一个新的“岛屿上下文”。
- 你无法从应用的其余部分访问“岛屿上下文”,也无法从岛屿组件访问应用其余部分的上下文。换句话说,服务端组件或岛屿与应用的其余部分是隔离的。
- 渲染岛屿时,你的插件将再次运行,除非它们设置了
env: { islands: false }(你可以在对象语法的插件中这样做)。
useRoute() 和其他 vue-router 组合式函数不会在服务端(岛屿)组件内部跟踪当前页面路由。岛屿在其自己的隔离 Vue 应用中渲染,该应用仅由其 prop(以及任何显式上下文)进行键控,这正是保持岛屿能够独立于渲染它们的页面进行缓存的原因。在岛屿内部,useRoute() 将反映岛屿自身的请求,而不是用户所在的页面。如果岛屿需要有关当前路由的信息,请显式将其传入——可以作为来自父组件的 prop,或通过 <NuxtIsland> 上的 context prop(在岛屿内部从 nuxtApp.ssrContext.islandContext 读取)。在岛屿组件内部,你可以通过 nuxtApp.ssrContext.islandContext 访问其岛屿上下文。请注意,虽然岛屿组件仍被标记为实验性功能,但此上下文的格式可能会发生变化。
display: contents; 的 <div> 中与客户端组件配对
在这种情况下,.server 和 .client 组件是一个组件的两个“半部分”,可用于在服务端和客户端分别实现组件的高级用例。
-| components/
---| Comments.client.vue
---| Comments.server.vue
<template>
<div>
<!-- this component will render Comments.server on the server then Comments.client once mounted in the browser -->
<Comments />
</div>
</template>
Nuxt 内置组件
Nuxt 提供了许多组件,包括 <ClientOnly> 和 <DevOnly>。你可以在 API 文档中阅读更多关于它们的信息。
库作者
制作带有自动 tree-shaking 和组件注册功能的 Vue 组件库超级简单。✨
你可以使用 @nuxt/kit 提供的 addComponentsDir 方法在你的 Nuxt 模块中注册组件目录。
想象一下这样一个目录结构
-| node_modules/
---| awesome-ui/
-----| components/
-------| Alert.vue
-------| Button.vue
-----| nuxt.ts
-| pages/
---| index.vue
-| nuxt.config.ts
然后在 awesome-ui/nuxt.ts 中,你可以使用 addComponentsDir 钩子
import { addComponentsDir, createResolver, defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
setup () {
const resolver = createResolver(import.meta.url)
// Add ./components dir to the list
addComponentsDir({
path: resolver.resolve('./components'),
prefix: 'awesome',
})
},
})
就是这样!现在在你的项目中,你可以在 nuxt.config 文件中将你的 UI 库作为 Nuxt 模块导入
export default defineNuxtConfig({
modules: ['awesome-ui/nuxt'],
})
……并直接在我们的 app/pages/index.vue 中使用该模块的组件(带有 awesome- 前缀)
<template>
<div>
My <AwesomeButton>UI button</AwesomeButton>!
<awesome-alert>Here's an alert!</awesome-alert>
</div>
</template>
它只会自动导入被使用的组件,并且在更新 node_modules/awesome-ui/components/ 中的组件时支持 HMR。