components

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

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

Directory Structure
|-| components/
---| AppHeader.vue
---| AppFooter.vue
app/app.vue
<template>
  <div>
    <AppHeader />
    <NuxtPage />
    <AppFooter />
  </div>
</template>

组件名称

如果你在嵌套目录中有组件,例如:

Directory Structure
|-| 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 prop。

例如:

app/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 包大小很有帮助。

app/pages/index.vue
<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 的组件的属性将导致其水合)
目前,Nuxt 的内置懒水合仅在单文件组件(SFC)中有效,并且要求你在模板中定义 prop(而不是通过 v-bind 传播对象属性)。它也不适用于从 #components 的直接导入。

hydrate-on-visible

当组件在视口中可见时水合。

app/pages/index.vue
<template>
  <div>
    <LazyMyComponent hydrate-on-visible />
  </div>
</template>
阅读有关 hydrate-on-visible 选项的更多信息。
在底层,这使用 Vue 的内置 hydrateOnVisible 策略

hydrate-on-idle

当浏览器空闲时水合组件。如果你需要组件尽快加载但不阻塞关键渲染路径,这很有用。

你也可以传递一个数字作为最大超时时间。

app/pages/index.vue
<template>
  <div>
    <LazyMyComponent hydrate-on-idle />
  </div>
</template>
在底层,这使用 Vue 的内置 hydrateOnIdle 策略

hydrate-on-interaction

在指定交互(例如,点击、鼠标悬停)后水合组件。

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

如果你不传递事件或事件列表,它默认为在 pointerenterclickfocus 上水合。

在底层,这使用 Vue 的内置 hydrateOnInteraction 策略

hydrate-on-media-query

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

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

hydrate-after

在指定延迟(以毫秒为单位)后水合组件。

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

hydrate-when

根据布尔条件水合组件。

app/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

从不水合组件。

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

监听水合事件

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

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

<script setup lang="ts">
function onHydrate () {
  console.log('Component has been hydrated!')
}
</script>

注意事项和最佳实践

延迟水合可以带来性能优势,但必须正确使用它:

  1. 优先考虑视口内容: 避免对关键的、首屏内容进行延迟水合。它最适合不需要立即使用的内容。
  2. 条件渲染: 在懒组件上使用 v-if="false" 时,你可能不需要延迟水合。你可以只使用普通的懒组件。
  3. 共享状态: 注意多个组件之间的共享状态(v-model)。在一个组件中更新模型可能会触发绑定到该模型的所有组件的水合。
  4. 使用每种策略的预期用例: 每种策略都针对特定目的进行了优化。
    • hydrate-when 最适合可能并不总是需要水合的组件。
    • hydrate-after 适用于可以等待特定时间的组件。
    • hydrate-on-idle 适用于可以在浏览器空闲时水合的组件。
  5. 避免在交互式组件上使用 hydrate-never 如果组件需要用户交互,则不应设置为永不水合。

直接导入

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

app/pages/index.vue
<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 目录。如果你想添加其他目录,或更改在此目录的子文件夹中扫描组件的方式,可以将其他目录添加到配置中:

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 后缀。

Directory Structure
|| components/
|--| Comments.client.vue
app/pages/example.vue
<template>
  <div>
    <!-- 此组件将仅在客户端呈现 -->
    <Comments />
  </div>
</template>
此功能仅适用于 Nuxt 自动导入和 #components 导入。从其实际路径显式导入这些组件不会将它们转换为仅客户端组件。
.client 组件仅在挂载后呈现。要使用 onMounted() 访问呈现的模板,请在 onMounted() 钩子的回调中添加 await nextTick()
ClientOnly 组件已在 Nuxt 3.x 中移除,你可以使用 .client 后缀或 Nuxt 提供的其他方式实现类似效果。

服务器组件

服务器组件允许在客户端应用程序中服务器呈现单个组件。即使在生成静态站点时,也可以在 Nuxt 中使用服务器组件。这可以构建复杂的站点,混合动态组件、服务器呈现的 HTML,甚至是静态标记块。

服务器组件可以单独使用,也可以与客户端组件配对使用。

阅读 Daniel Roe 关于 Nuxt 服务器组件的指南。

独立服务器组件

独立服务器组件将始终在服务器上呈现,也称为岛屿组件。

当它们的属性更新时,这将导致网络请求,该请求将就地更新呈现的 HTML。

服务器组件目前是实验性的,要使用它们,你需要在 nuxt.config 中启用"组件岛屿"功能:

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

现在你可以使用 .server 后缀注册仅服务器组件,并在应用程序中的任何位置自动使用它们。

Directory Structure
|-| components/
---| HighlightedMarkdown.server.vue
app/pages/example.vue
<template>
  <div>
    <!--
      这将自动在服务器上呈现,意味着你的 markdown 解析 + 高亮
      库不包括在你的客户端包中。
     -->
    <HighlightedMarkdown markdown="# Headline" />
  </div>
</template>

仅服务器组件在底层使用 <NuxtIsland>,意味着 lazy prop 和 #fallback 插槽都传递给它。

服务器组件(和岛屿)必须具有单个根元素。(HTML 注释也被视为元素。)
属性通过 URL 查询参数传递给服务器组件,因此受到 URL 可能长度的限制,因此请小心不要通过属性向服务器组件传递大量数据。
在其他岛屿中嵌套岛屿时要小心,因为每个岛屿都会增加一些额外开销。
仅服务器组件和岛屿组件的大多数功能,例如插槽和客户端组件,仅适用于单文件组件。

服务器组件内的客户端组件

此功能需要配置中的 experimental.componentIslands.selectiveClient 为 true。

你可以通过在希望加载客户端的组件上设置 nuxt-client 属性来部分水合组件。

app/components/ServerWithClient.vue
<template>
  <div>
    <HighlightedMarkdown markdown="# Headline" />
    <!-- Counter 将在客户端加载和水合 -->
    <Counter
      nuxt-client
      :count="5"
    />
  </div>
</template>
这仅在服务器组件内有效。客户端组件的插槽仅在 experimental.componentIsland.selectiveClient 设置为 'deep' 时有效,并且由于它们在服务器端呈现,因此一旦在客户端就无法交互。

服务器组件上下文

在呈现仅服务器或岛屿组件时,<NuxtIsland> 发出 fetch 请求,该请求返回 NuxtIslandResponse。(如果在服务器上呈现,这是内部请求,或者如果在客户端导航上呈现,则你可以在网络选项卡中看到的请求。)

这意味着:

  • 将创建一个新的 Vue 应用程序服务器端以创建 NuxtIslandResponse
  • 将在呈现组件时创建新的"岛屿上下文"。
  • 你无法从应用程序的其余部分访问"岛屿上下文",也无法从岛屿组件访问应用程序其余部分的上下文。换句话说,服务器组件或岛屿从应用程序的其余部分中_隔离_。
  • 你的插件将在呈现岛屿时再次运行,除非它们设置了 env: { islands: false }(你可以在对象语法插件中执行此操作)。
路由中间件在呈现岛屿组件时不会运行。中间件是适用于页面的路由概念,而不是设计用于控制组件呈现的。

在岛屿组件内,你可以通过 nuxtApp.ssrContext.islandContext 访问其岛屿上下文。请注意,虽然岛屿组件仍标记为实验性,但此上下文的格式可能会更改。

插槽可以是交互式的,并包装在带有 display: contents;<div>

与客户端组件配对

在这种情况下,.server + .client 组件是组件的两个"一半",可以用于高级用例,分别在服务器端和客户端端实现组件。

Directory Structure
|-| components/
---| Comments.client.vue
---| Comments.server.vue
app/pages/example.vue
<template>
  <div>
    <!-- 此组件将在服务器上呈现 Comments.server,然后在浏览器中挂载后呈现 Comments.client -->
    <Comments />
  </div>
</template>

内置 Nuxt 组件

Nuxt 提供了许多组件,包括 <ClientOnly><DevOnly>。你可以在 API 文档中阅读更多相关信息。

Docs > API 中阅读更多。

库作者

使用自动 tree-shaking 和组件注册制作 Vue 组件库非常容易。✨

你可以使用 @nuxt/kit 提供的 `addComponentsDir`` 方法在 Nuxt 模块中注册你的组件目录。

想象一个这样的目录结构:

Directory Structure
|-| 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)

    // 将 ./components dir 添加到列表
    addComponentsDir({
      path: resolver.resolve('./components'),
      prefix: 'awesome',
    })
  },
})

就是这样!现在在你的项目中,你可以在 nuxt.config 文件中导入 UI 库作为 Nuxt 模块:

nuxt.config.ts
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。