CODEGURU

Динамическая регистрация компонентов во Vue

Если вы используете однофайловые компоненты Vue, то вам, конечно же, знаком способ подключения компонента внутри другого компонента:

  • импортируем компонент;
  • регистрируем его в объекте компонентов родителя;
  • используем компонент в шаблоне
<template>
    <some-random-thing />
</template>
<script>
    import SomeRandomThing from './components/SomeRandomThing'
    export default {
        components: {
            SomeRandomThing,
        },
    }
</script>

Это привычный паттерн. Но что если нам понадобится динамическое отображение компонента? Давайте посмотрим как нам разнообразить отношения с компонентами.

Итак, у нас есть компонент Header. Допустим, информация в хедере может меняться в зависимости от каких-то условий. Далее, у нас есть компоненты UserInfo и CompanyInfo. И мы хотим показывать какой-то из них в зависимости от какого-то условия. Как это сделать?

Способ 1: старый добрый и проверенный

Этот способ мы обсуждали в самом начале. Именно его выберет большинство разработчиков.

<template>
  <div>
    <company-info v-if="isCompany" />
    <user-info v-else />
    ...
  </div>
</template>

<script>
import UserInfo from './components/UserInfo'
import CompanyInfo from './components/CompanyInfo'
export default {
    components: {
        UserInfo,
        CompanyInfo
    },
    props: {
        isCompany: { type: Boolean, default: false },
    },
}
</script>

Ничего необычного. Мы импортируем два компонента, регистрируем их, и показываем один из них в зависимости от значения входного параметра.

Вы наверняка постоянно так делаете. Но то же самое можно сделать немного лучше.

Способ 2: используем <component />

Component это встроенное во Vue определение компонента. Он работает как плейсхолдер для отображения любого компонента, имя которого передаётся через параметр :is.

<template>
  <div>
    <component :is="componentName" />
  </div>
</template>

<script>
import UserInfo from './components/UserInfo'
import CompanyInfo from './components/CompanyInfo'
export default {
    components: {
        UserInfo,
        CompanyInfo,
    },
    props: {
        isCompany: { type: Boolean, default: false },
    },
    computed: {
        componentName () {
            return this.isCompany ? 'company-info' : 'user-info'
        },
    },
}
</script>

Здесь мы используем <component /> и создали вычисляемое свойство с именем необходимого компонента, что позволило отказаться от громоздкой конструкции v-if/v-else.

Но это ещё не всё. Несмотря на использование <component />, нам по прежнему приходится импортировать и регистрировать компоненты UserInfo и CompanyInfo. Вот если бы можно было импортировать только необходимый компонент... И тут нам поможет динамический импорт

Способ 3: динамический импорт + <component /> (и бонусом разделение кода)

<template>
  <div>
    <component :is="componentInstance" />
  </div>
</template>

<script>
export default {
    props: {
        isCompany: { type: Boolean, default: false },
    },
    computed: {
        componentInstance () {
            const name = this.isCompany ? 'CompanyInfo' : 'UserInfo'
            return () => import(`./components/${name}`)
        }
    }
}
</script>

В этом варианте для импорта компонента мы используем функцию, которая возвращает Promise. И если всё идёт хорошо, то Promise разрешается и загружается только необходимый нам компонент, который мы передаём <component /> для рендеринга. Да, так можно. Согласно документации, свойство :is может содержать:

  • Имя компонента или
  • Объект компонента

А объект компонента это именно то, что нам нужно.

И вот мы существенно сократили наш код, нам больше не нужно вручную импортировать и регистрировать компоненты. Всё происходит динамически. Более подробную информацию о динамическом импорте смотрите в документации.

Таким образом мы немного упростили себе жизнь, сократив количество кода, и мимоходом уменьшили объём итогового файла приложения. Ведь динамический импорт он на то и динамический, что необходимый компонент будет подгружаться только при необходимости.