components

components/ 目录是放置所有 Vue 组件的地方。

Nuxt 会自动导入该目录中的任何组件(以及您可能使用的任何模块注册的组件)。

目录结构
-| components/
---| AppHeader.vue
---| AppFooter.vue
app.vue
<template>
  <div>
    <AppHeader />
    <NuxtPage />
    <AppFooter />
  </div>
</template>

组件名称

如果您在嵌套目录中有一个组件,比如:

目录结构
-| components/
---| base/
-----| foo/
-------| Button.vue

……那么组件的名称将基于其路径目录和文件名,重复的部分将被去除。因此,该组件的名称将是:

<BaseFooButton />
为了清晰起见,建议组件的文件名与其名称匹配。所以,在上述示例中,你可以将 Button.vue 重命名为 BaseFooButton.vue

如果您只想根据名称而非路径来自动导入组件,则需要使用配置对象的扩展形式,将 pathPrefix 选项设置为 false

nuxt.config.ts
export default defineNuxtConfig({
  components: [
    {
      path: '~/components',
      pathPrefix: false,    },
  ],
});

这会使用与 Nuxt 2 相同的策略注册组件。例如,~/components/Some/MyComponent.vue 将可用作 <MyComponent>,而不是 <SomeMyComponent>

动态组件

如果您想使用 Vue 的 <component :is="someComputedComponent"> 语法,需要使用 Vue 提供的 resolveComponent 辅助函数,或直接从 #components 导入组件并传递给 is 属性。

例如:

pages/index.vue
<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 包的大小。

pages/index.vue
<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>

延迟(水合)渲染

懒加载组件非常适合控制应用中的代码块大小,但它们并不总是提升运行时性能,因为除非条件渲染,否则仍然会急切加载。现实应用中,有些页面会包含大量内容和组件,而多数情况下不需要页面加载时立即交互。让所有组件都急切加载可能会影响性能。

为优化应用,您可能想要延迟某些组件的水合过程,直到其可见或者浏览器完成了更重要的任务。

Nuxt 支持延迟(水合)渲染,允许您控制组件何时变为可交互。

水合策略

Nuxt 提供了一系列内建的水合策略。每个延迟组件只能使用一种策略。

目前,Nuxt 内建的延迟水合只适用于单文件组件(SFC),且需要您在模板中定义 prop(而非通过 v-bind 展开 prop 对象)。它也不支持从 #components 的直接导入。

hydrate-on-visible

当组件在视口内变得可见时水合。

pages/index.vue
<template>
  <div>
    <LazyMyComponent hydrate-on-visible />
  </div>
</template>
进一步阅读有关 hydrate-on-visible 的选项。
底层使用了 Vue 内建的 hydrateOnVisible 策略

hydrate-on-idle

当浏览器空闲时水合组件。适合需要尽快加载组件,但不阻塞关键渲染路径的场景。

也可以传入数字,作为最大超时时间。

pages/index.vue
<template>
  <div>
    <LazyMyComponent hydrate-on-idle />
  </div>
</template>
底层使用了 Vue 内建的 hydrateOnIdle 策略

hydrate-on-interaction

在指定交互事件后(水合组件,例如 click, mouseover)。

pages/index.vue
<template>
  <div>
    <LazyMyComponent hydrate-on-interaction="mouseover" />
  </div>
</template>

如果不传递事件或事件列表,默认会在 pointerenterfocus 时水合。

底层使用了 Vue 内建的 hydrateOnInteraction 策略

hydrate-on-media-query

当窗口匹配指定媒体查询时水合组件。

pages/index.vue
<template>
  <div>
    <LazyMyComponent hydrate-on-media-query="(max-width: 768px)" />
  </div>
</template>
底层使用了 Vue 内建的 hydrateOnMediaQuery 策略

hydrate-after

在指定延迟(毫秒)后水合组件。

pages/index.vue
<template>
  <div>
    <LazyMyComponent :hydrate-after="2000" />
  </div>
</template>

hydrate-when

基于布尔条件水合组件。

pages/index.vue
<template>
  <div>
    <LazyMyComponent :hydrate-when="isReady" />
  </div>
</template>
<script setup lang="ts">
const isReady = ref(false)
function myFunction() {
  // 触发自定义水合策略...
  isReady.value = true
}
</script>

hydrate-never

永不水合组件。

pages/index.vue
<template>
  <div>
    <LazyMyComponent hydrate-never />
  </div>
</template>

监听水合事件

所有延迟水合组件在水合时都会触发一个 @hydrated 事件。

pages/index.vue
<template>
  <div>
    <LazyMyComponent hydrate-on-visible @hydrated="onHydrate" />
  </div>
</template>

<script setup lang="ts">
function onHydrate() {
  console.log("组件已水合!")
}
</script>

注意事项与最佳实践

延迟水合能带来性能优势,但使用时需注意:

  1. 优先考虑视口内内容: 避免对关键内容(首屏内容)使用延迟水合,适合对非首屏内容使用。
  2. 条件渲染: 对懒加载组件使用 v-if="false" 时,可能无需再使用延迟水合,直接用普通懒加载即可。
  3. 共享状态: 注意多个组件间通过 v-model 共享状态,更新一个组件会触发相关组件的水合。
  4. 合理使用各策略: 各水合策略适用场景不同。
    • hydrate-when 适合可能不需要立即水合的组件。
    • hydrate-after 适合可延迟一定时间再水合的组件。
    • hydrate-on-idle 适合浏览器空闲时水合的组件。
  5. 避免对可交互组件使用 hydrate-never 有用户交互需求的组件不应设置成永不水合。

直接导入

如果你想绕过 Nuxt 的自动导入功能,也可以显式地从 #components 导入组件。

pages/index.vue
<script setup lang="ts">
import { NuxtLink, LazyMountainsList } from '#components'

const show = ref(false)
</script>

<template>
  <div>
    <h1>群山</h1>
    <LazyMountainsList v-if="show" />
    <button v-if="!show" @click="show = true">显示列表</button>
    <NuxtLink to="/">首页</NuxtLink>
  </div>
</template>

自定义目录

默认情况下,只有 ~/components 目录会被扫描。如果你想添加其他目录,或修改子文件夹中的组件扫描方式,可以向配置添加额外目录:

nuxt.config.ts
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' },

    // 如果有覆盖 `~/components` 子目录的配置,请确保这一条在最后。
    //
    // ~/components/Btn.vue => <Btn />
    // ~/components/base/Btn.vue => <BaseBtn />
    '~/components'
  ]
})
需要首先添加所有嵌套目录,因为他们按顺序扫描。

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',
    })
  },
})

组件扩展名

默认情况下,任何使用 nuxt.config.ts 中 extensions 键 指定后缀的文件都会被视为组件。

如需限制注册为组件的文件扩展名,您可以使用组件目录声明的扩展形式,并设置 extensions 键:

nuxt.config.ts
export default defineNuxtConfig({
  components: [
    {
      path: '~/components',
      extensions: ['.vue'],    }
  ]
})

客户端组件

如果组件仅打算在客户端渲染,可以给组件加上 .client 后缀。

目录结构
| components/
--| Comments.client.vue
pages/example.vue
<template>
  <div>
    <!-- 该组件只会在客户端渲染 -->
    <Comments />
  </div>
</template>
此功能仅适用于 Nuxt 的自动导入和 #components 导入。若显式从真实路径导入组件,无法将其转换为仅客户端组件。
.client 组件仅在挂载后渲染。若想使用 onMounted() 访问渲染后模板,请在 onMounted() 回调中添加 await nextTick()
你也可以使用 <ClientOnly> 组件实现类似效果。

服务端组件

服务端组件允许在客户端应用中针对单个组件进行服务器渲染。即使是生成静态站点,也可以在 Nuxt 中使用服务端组件。这允许构建混合动态组件、服务器渲染 HTML 甚至静态标记的复杂站点。

服务端组件可单独使用,也可搭配 客户端组件 一起使用。

阅读 Daniel Roe 的 Nuxt 服务端组件指南。

独立的服务端组件

独立的服务端组件始终在服务器端渲染,也称为“Islands”组件。

当它们的 props 更新时,会发起网络请求,原地更新渲染的 HTML。

服务端组件目前处于试验阶段,需要在 nuxt.config 中启用“组件岛屿(component islands)”功能:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    componentIslands: true
  }
})

现在,你可以用 .server 后缀注册仅服务端组件,并在应用中自动使用它们。

目录结构
-| components/
---| HighlightedMarkdown.server.vue
pages/example.vue
<template>
  <div>
    <!--
      该组件将自动在服务器端渲染,意味着 markdown 解析和高亮等库不会被打包到客户端。
     -->
    <HighlightedMarkdown markdown="# 标题" />
  </div>
</template>

服务端组件底层使用 <NuxtIsland>lazy 属性和 #fallback 插槽都会传递给它。

服务端组件(及 islands)必须只有单个根元素。(HTML 注释也视为元素。)
Props 通过 URL 查询参数传递给服务端组件,因此受 URL 长度限制,避免传递大量数据。
嵌套岛屿组件时需注意,每个岛屿都会带来额外开销。
大多数面向服务端和岛屿组件的功能,如插槽和客户端组件,仅对单文件组件可用。

服务端组件中的客户端组件

此功能需要在配置中启用 experimental.componentIslands.selectiveClient

你可以通过给希望在客户端加载的组件添加 nuxt-client 属性来进行部分水合。

components/ServerWithClient.vue
<template>
  <div>
    <HighlightedMarkdown markdown="# 标题" />
    <!-- Counter 将在客户端加载并水合 -->
    <Counter nuxt-client :count="5" />
  </div>
</template>
此功能仅在服务端组件内有效。客户端组件的插槽仅在 experimental.componentIsland.selectiveClient 设置为 'deep' 时可用,且因为服务端渲染,客户端交互不可用。

服务端组件上下文

渲染服务端或岛屿组件时,<NuxtIsland> 会发起一个请求,返回 NuxtIslandResponse。(如果服务端渲染,则为内部请求;客户端导航渲染,则可在网络面板中看到该请求。)

这意味着:

  • 服务器端将新建一个 Vue 应用以生成 NuxtIslandResponse
  • 渲染组件时会创建新的“岛屿上下文”。
  • 你不能从应用其他部分访问该上下文,也不能从岛屿组件访问其他部分的上下文。换言之,服务端组件或岛屿与应用隔离。
  • 在渲染岛屿时,插件会重新运行,除非在对象语法插件中设置 env: { islands: false }

在岛屿组件中,可以通过 nuxtApp.ssrContext.islandContext 访问其岛屿上下文。注意该特性仍处于实验阶段,上下文格式可能改变。

插槽可交互,且会被包裹在带有 display: contents;<div> 中。

搭配客户端组件

此情况下,.server + .client 组件为组件的两个“半边”,用于高级场景,在服务端和客户端分别实现不同逻辑。

目录结构
-| components/
---| Comments.client.vue
---| Comments.server.vue
pages/example.vue
<template>
  <div>
    <!-- 该组件先渲染服务端的 Comments.server,浏览器挂载后切换为 Comments.client -->
    <Comments />
  </div>
</template>

内置 Nuxt 组件

Nuxt 提供了多个内置组件,包括 <ClientOnly><DevOnly>,详情请参阅 API 文档。

Read more in Docs > API.

库作者

制作带自动树摇和组件注册的 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 { createResolver, defineNuxtModule, addComponentsDir } from '@nuxt/kit'

export default defineNuxtModule({
  setup() {
    const resolver = createResolver(import.meta.url)

    // 添加 ./components 目录
    addComponentsDir({
      path: resolver.resolve('./components'),
      prefix: 'awesome',
    })
  },
})

完成!现在在项目中只需在 nuxt.config 文件中导入该模块:

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['awesome-ui/nuxt']
})

……然后在 pages/index.vue 里直接使用库组件(带 awesome- 前缀):

<template>
  <div>
    我的 <AwesomeButton>UI 按钮</AwesomeButton>    <awesome-alert>这是一个提示!</awesome-alert>
  </div>
</template>

组件仅会在使用时自动导入,并在更新 node_modules/awesome-ui/components/ 的组件时支持热更新(HMR)。

Read and edit a live example in Docs > Examples > Features > Auto Imports.