← Курс/Регистрация компонентов: глобальная и локальная#217 из 257+20 XP

Регистрация компонентов: глобальная и локальная

Зачем регистрировать компоненты

Прежде чем использовать компонент в шаблоне, Vue должен о нём знать. Есть два способа: **глобальная** и **локальная** регистрация.

Глобальная регистрация: app.component()

Глобально зарегистрированные компоненты доступны **в шаблонах любого компонента** без явного импорта:

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import BaseButton from './components/BaseButton.vue'
import BaseInput from './components/BaseInput.vue'
import BaseModal from './components/BaseModal.vue'

const app = createApp(App)

// Регистрация глобальных компонентов
app.component('BaseButton', BaseButton)
app.component('BaseInput', BaseInput)
app.component('BaseModal', BaseModal)

app.mount('#app')
<!-- В любом компоненте — без импорта -->
<template>
  <BaseInput v-model="value" />
  <BaseButton @click="submit">Отправить</BaseButton>
</template>

Недостатки глобальной регистрации:

  • Все глобальные компоненты попадают в бандл, даже если не используются
  • Сложнее найти, откуда берётся компонент при просмотре шаблона
  • Создаёт неявные зависимости
  • Локальная регистрация

    Компонент доступен только там, где он явно импортирован:

    <!-- Options API -->
    <script>
    import MyComponent from './MyComponent.vue'
    
    export default {
      components: {
        MyComponent
      }
    }
    </script>
    <!-- Composition API с <script setup> — достаточно импортировать -->
    <script setup>
    import MyComponent from './MyComponent.vue'
    import { ref } from 'vue'
    
    const count = ref(0)
    </script>
    
    <template>
      <!-- MyComponent доступен автоматически -->
      <MyComponent :count="count" />
    </template>

    В <script setup> **импортированный компонент автоматически становится доступным** в шаблоне — регистрировать в components: {} не нужно.

    Авто-импорт с Vite и unplugin-vue-components

    В реальных проектах используют плагин unplugin-vue-components, который автоматически импортирует компоненты по имени из папки:

    // vite.config.js
    import Components from 'unplugin-vue-components/vite'
    
    export default {
      plugins: [
        Components({
          dirs: ['src/components'],
          // Будет искать: BaseButton -> src/components/BaseButton.vue
        })
      ]
    }

    После этого в шаблонах не нужны никакие импорты — компоненты разрешаются автоматически.

    Соглашения об именовании

    Vue поддерживает несколько форматов имён компонентов в шаблоне:

    <!-- PascalCase — рекомендуется для SFC, визуально отличает от HTML -->
    <MyComponent />
    <BaseButton />
    
    <!-- kebab-case — работает всегда, обязателен в DOM-шаблонах -->
    <my-component />
    <base-button />

    Рекомендация: используйте PascalCase для имён компонентов везде в JavaScript/Vue-файлах.

    Плагины как способ регистрации

    Часто глобальные компоненты оформляют в виде плагина:

    // plugins/ui.js
    import BaseButton from '../components/BaseButton.vue'
    import BaseInput from '../components/BaseInput.vue'
    
    export default {
      install(app) {
        app.component('BaseButton', BaseButton)
        app.component('BaseInput', BaseInput)
        app.provide('uiConfig', { theme: 'default' })
      }
    }
    
    // main.js
    import UIPlugin from './plugins/ui.js'
    app.use(UIPlugin)

    Примеры

    Эмуляция реестра компонентов Vue: глобальная/локальная регистрация и разрешение имён

    // Эмулируем реестр компонентов Vue: регистрация и поиск по имени
    
    // Глобальный реестр (аналог app.component())
    class ComponentRegistry {
      constructor() {
        this._global = new Map()
      }
    
      // Глобальная регистрация
      registerGlobal(name, definition) {
        // Нормализуем имя: поддерживаем PascalCase и kebab-case
        const normalized = this._normalize(name)
        this._global.set(normalized, definition)
        console.log(`  [global] Зарегистрирован: "${name}" (ключ: "${normalized}")`)
      }
    
      // Создать контекст компонента с локальными регистрациями
      createContext(localComponents = {}) {
        const registry = this
        const local = new Map(
          Object.entries(localComponents).map(([k, v]) => [registry._normalize(k), v])
        )
    
        return {
          resolve(name) {
            const key = registry._normalize(name)
    
            // Сначала ищем локально
            if (local.has(key)) {
              const comp = local.get(key)
              console.log(`  [resolve] "${name}" найден ЛОКАЛЬНО`)
              return comp
            }
    
            // Затем глобально
            if (registry._global.has(key)) {
              const comp = registry._global.get(key)
              console.log(`  [resolve] "${name}" найден ГЛОБАЛЬНО`)
              return comp
            }
    
            console.warn(`  [resolve] "${name}" НЕ НАЙДЕН!`)
            return null
          },
    
          listLocal() {
            return [...local.keys()]
          }
        }
      }
    
      // PascalCase -> kebab-case для унификации
      _normalize(name) {
        return name
          .replace(/([A-Z])/g, (_, c, i) => (i === 0 ? c.toLowerCase() : `-${c.toLowerCase()}`))
      }
    
      listGlobal() {
        return [...this._global.keys()]
      }
    }
    
    // Базовые компоненты (псевдо-определения)
    const BaseButton = { name: 'BaseButton', template: '<button>...</button>' }
    const BaseInput = { name: 'BaseInput', template: '<input>...' }
    const BaseModal = { name: 'BaseModal', template: '<dialog>...</dialog>' }
    const UserCard = { name: 'UserCard', template: '<div class="user-card">...</div>' }
    const PaymentForm = { name: 'PaymentForm', template: '<form>...</form>' }
    
    // === Настройка глобального реестра ===
    console.log('=== Глобальная регистрация (app.component) ===')
    const registry = new ComponentRegistry()
    
    registry.registerGlobal('BaseButton', BaseButton)
    registry.registerGlobal('BaseInput', BaseInput)
    registry.registerGlobal('BaseModal', BaseModal)
    
    console.log('Глобальные компоненты:', registry.listGlobal())
    
    // === Локальная регистрация в компоненте ===
    console.log('\n=== Локальная регистрация (<script setup> import) ===')
    
    // Компонент UserProfile: импортирует UserCard локально
    const userProfileContext = registry.createContext({
      UserCard,  // только здесь доступен UserCard
    })
    
    console.log('Локальные компоненты UserProfile:', userProfileContext.listLocal())
    
    // === Разрешение компонентов ===
    console.log('\n=== Разрешение компонентов ===')
    
    // В UserProfile доступны: локальные + глобальные
    userProfileContext.resolve('UserCard')   // локальный
    userProfileContext.resolve('BaseButton') // глобальный
    userProfileContext.resolve('base-input') // kebab-case тоже работает
    userProfileContext.resolve('PaymentForm') // не зарегистрирован -> предупреждение
    
    // Компонент без локальных регистраций — только глобальные
    console.log('\n=== Контекст без локальных компонентов ===')
    const simpleContext = registry.createContext()
    simpleContext.resolve('BaseModal')   // глобальный
    simpleContext.resolve('UserCard')    // не найден
    
    // === Паттерн: плагин ===
    console.log('\n=== Паттерн: регистрация через плагин ===')
    function UIPlugin(appRegistry) {
      appRegistry.registerGlobal('BaseButton', BaseButton)
      appRegistry.registerGlobal('BaseInput', BaseInput)
      console.log('  UIPlugin установлен')
    }
    
    const newRegistry = new ComponentRegistry()
    UIPlugin(newRegistry)
    console.log('После установки UIPlugin:', newRegistry.listGlobal())