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"> используйте хелпер resolveComponent из Vue либо импортируйте компонент из #components и передайте его в проп is.

Например:

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 откладывает загрузку кода компонента до нужного момента и помогает уменьшить размер JS-бандла.

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 поддерживает отложенную (ленивую) гидратацию и позволяет управлять моментом, когда компоненты становятся интерактивными.

Стратегии гидратации

Доступен набор встроенных стратегий гидратации. Для одного ленивого компонента можно выбрать только одну.

Любое изменение пропа у компонента с отложенной гидратацией немедленно запускает гидратацию (например, смена пропа у компонента с hydrate-never приведёт к гидратации).
Встроенная отложенная гидратация Nuxt сейчас работает только в однофайловых компонентах (SFC), проп нужно задавать в шаблоне явно (а не через 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>

Если не передать событие или список событий, по умолчанию гидратация происходит на pointerenter, click и focus.

Под капотом используется встроенная в Vue стратегия hydrateOnInteraction.

hydrate-on-media-query

Гидратирует компонент, когда окно соответствует 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 () {
  // trigger custom hydration strategy...
  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. Приоритет контенту в viewport: не откладывайте гидратацию для критичного контента «над сгибом». Стратегия лучше подходит для того, что не нужно сразу.
  2. Условный рендер: если на ленивом компоненте стоит v-if="false", отложенная гидратация часто не нужна — достаточно обычного ленивого компонента.
  3. Общее состояние: учитывайте общее состояние (v-model) между компонентами. Обновление модели в одном компоненте может запустить гидратацию у всех, привязанных к этой модели.
  4. Назначение стратегий: каждая стратегия заточена под свою задачу.
    • hydrate-when — когда компонент не всегда должен гидратироваться.
    • hydrate-after — когда можно подождать фиксированное время.
    • hydrate-on-idle — когда гидратацию можно отложить до простоя браузера.
  5. Не используйте hydrate-never для интерактивных компонентов: если нужен ввод пользователя, компонент не должен оставаться без гидратации.

Прямой импорт

Компоненты можно явно импортировать из #components, если нужно обойти автоимпорт Nuxt.

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

    // 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',
  ],
})
Вложенные каталоги нужно указывать первыми — сканирование идёт по порядку.

Пакеты 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',
    })
  },
})

Расширения файлов компонентов

По умолчанию компонентом считается любой файл с расширением из ключа extensions в nuxt.config.ts. Чтобы ограничить набор расширений, используйте расширенную форму объявления каталога и ключ 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>
    <!-- this component will only be rendered on client side -->
    <Comments />
  </div>
</template>
Это работает только с автоимпортом Nuxt и импортом из #components. Явный импорт по реальному пути не превращает компонент в клиентский.
Компоненты .client рендерятся только после монтирования. Чтобы в onMounted() обратиться к уже отрендеренному шаблону, в колбэке вызовите await nextTick().
Похожий результат даёт компонент <ClientOnly>.

Серверные компоненты

Серверные компоненты позволяют рендерить отдельные части интерфейса на сервере внутри клиентского приложения. Их можно использовать в Nuxt даже при статической генерации — так проще смешивать динамику, серверный HTML и статические фрагменты разметки.

Серверный компонент можно использовать отдельно или в паре с клиентским компонентом.

Гайд Daniel Roe по Nuxt Server Components.

Автономные серверные компоненты

Автономные серверные компоненты всегда рендерятся на сервере (островные компоненты, islands).

При обновлении пропов выполняется сетевой запрос, и HTML обновляется на месте.

Серверные компоненты пока экспериментальны; чтобы их использовать, включите в nuxt.config функцию «component islands»:

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

После этого можно регистрировать компоненты только для сервера с суффиксом .server и использовать их в приложении с автоимпортом.

Directory Structure
-| components/
---| HighlightedMarkdown.server.vue
app/pages/example.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 и слот #fallback пробрасываются в него.

У серверных компонентов (и островков) должен быть один корневой элемент (HTML-комментарии тоже считаются элементами).
Пропы передаются в серверные компоненты через query-параметры URL, поэтому объём данных ограничен длиной URL — не передавайте через пропы огромные объёмы данных.
Будьте осторожны с вложением островков друг в друга: каждый уровень добавляет накладные расходы.
Большинство возможностей серверных и островных компонентов (слоты, клиентские компоненты и т.д.) доступны в основном для однофайловых компонентов.

Клиентские компоненты внутри серверных

Для этой возможности в конфигурации должно быть experimental.componentIslands.selectiveClient: true.

Частичную гидратацию можно включить, задав атрибут nuxt-client на компоненте, который должен загружаться на клиенте.

app/components/ServerWithClient.vue
<template>
  <div>
    <HighlightedMarkdown markdown="# Headline" />
    <!-- Counter will be loaded and hydrated client-side -->
    <Counter
      nuxt-client
      :count="5"
    />
  </div>
</template>
Работает только внутри серверного компонента. Слоты клиентских компонентов поддерживаются только при experimental.componentIsland.selectiveClient: 'deep'; они рендерятся на сервере и на клиенте остаются неинтерактивными.

Контекст серверного компонента

При рендере серверного или островного компонента <NuxtIsland> выполняет запрос и получает NuxtIslandResponse (внутренний запрос на сервере или видимый в сети при клиентской навигации).

Это означает:

  • На сервере создаётся новое Vue-приложение для формирования NuxtIslandResponse.
  • При рендере создаётся новый «островной» контекст.
  • Контекст островка недоступен из остального приложения и наоборот — серверный компонент или остров изолирован от остального приложения.
  • Плагины при рендере островка выполняются снова, если у них не задано env: { islands: false } (в синтаксисе объекта плагина).
Маршрутный middleware не выполняется при рендере островных компонентов. Middleware относится к маршрутизации страниц, а не к рендеру компонентов.

Внутри островного компонента контекст доступен через nuxtApp.ssrContext.islandContext. Пока островные компоненты экспериментальны, формат контекста может меняться.

Слоты могут быть интерактивными и оборачиваются в <div> с display: contents;

В паре с клиентским компонентом

В этом случае пары .server + .client — две «половины» одного компонента для разных реализаций на сервере и клиенте в продвинутых сценариях.

Directory Structure
-| components/
---| Comments.client.vue
---| Comments.server.vue
app/pages/example.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.

Узнать больше Docs > 4 X > API.

Авторам библиотек

Сделать библиотеку Vue-компонентов с автоматической регистрацией и tree-shaking очень просто. ✨

Используйте метод addComponentsDir из @nuxt/kit, чтобы зарегистрировать каталог компонентов в 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)

    // Add ./components dir to the list
    addComponentsDir({
      path: resolver.resolve('./components'),
      prefix: 'awesome',
    })
  },
})

Готово: в проекте подключите UI-библиотеку как модуль Nuxt в nuxt.config:

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

…и используйте компоненты модуля (с префиксом awesome-) в app/pages/index.vue:

<template>
  <div>
    My <AwesomeButton>UI button</AwesomeButton>!
    <awesome-alert>Here's an alert!</awesome-alert>
  </div>
</template>

Компоненты подгружаются только при использовании и поддерживают HMR при правках в node_modules/awesome-ui/components/.

Прочитайте и отредактируйте живой пример в Docs > 4 X > Examples > Features > Auto Imports.