В своей предыдущей статье я описал способ создания компонентов высшего порядка в Vue.js. Я получил много отличных отзывов, которые вдохновили меня на написание этой статьи.

Прежде чем начать искать ответ на вопрос: Нужны ли нам компоненты высшего порядка в Vue.js? Я хотел бы задать еще один вопрос: какие проблемы решают компоненты высшего порядка? Я считаю, что ответ на этот вопрос имеет решающее значение.

Для меня есть две основные проблемы, которые решают HOC:

  • повторение кода: они позволяют разделять общие функции между разными компонентами,
  • организация кода: они позволяют расширить функциональность компонента на уровне создания экземпляра, а не на уровне объявления.

Vue.js предоставляет нам два решения для решения проблемы повторения кода и улучшения организации кода: миксины и слоты с ограниченной областью видимости. В этой статье я собираюсь сравнить эти два подхода с подходом компонентов более высокого порядка, который я описал в предыдущей статье.

Подход 0. Компонент более высокого порядка

Этот пост основан на примере из статьи Компоненты высшего порядка в Vue.js. Ниже для справки приведен код приложения из предыдущей статьи.

Во-первых, основной компонент приложения:

Во-вторых, компоненты CommentsList и BlogPost:

И, наконец, withSubscription компонент высшего порядка:

withSubscription Компонент высшего порядка выполняет четыре обязанности:

  1. получить данные из внешнего источника данных
  2. передать полученные данные обернутому компоненту
  3. добавить слушателя изменений к внешнему источнику данных при монтировании компонента
  4. удалить прослушиватель изменений из внешнего источника данных при уничтожении компонента

Подход 1: миксин

Миксин, который выполняет те же обязанности, что и withSubscription компонент высшего порядка, выглядит намного проще, чем HOC.

Внутри handleChange метода вызывается метод selectData, однако в миксине отсутствует его реализация. В подходе HOC selectData был передан в качестве аргумента withSubscription Компоненту более высокого порядка, потому что его реализация отличается в BlogPost и CommentsList. При использовании миксина я должен реализовать этот метод внутри обоих компонентов.

Ниже приведены компоненты BlogPost и CommentsList с прикрепленным миксином:

App компонент выглядит намного проще, вместо того, чтобы оборачивать компоненты HOC, я просто использую компоненты BlogPost и CommentsList.

Вышеупомянутая реализация миксина решает одну из двух проблем, решаемых HOC. Это позволяет извлечь общие функции и избежать повторения кода между компонентами. Однако это не решает вторую проблему. С миксином я не могу создать BlogPost или CommentsList без функциональности миксина. Я всегда могу передать опору для отключения функций миксина, но что, если мне нужна логика миксина только в одном BlogPost экземпляре, а в остальных 100 экземплярах она мне не нужна? Я не хочу, чтобы в моем компоненте была логика, которую я не использую в большинстве случаев.

Более того, мне трудно отлаживать, когда реализация метода находится внутри файла, отличного от файла, внутри которого он вызван. С подходом миксина я определил selectData внутри компонентов, но назвал его внутри миксина. Для меня это противоречит идее однофайловых компонентов, идее, которая делает Vue таким крутым. С HOC я также определил selectData в другом файле, из которого он был вызван, однако я использовал реквизиты для передачи этого метода, поэтому, по крайней мере, я знал, откуда он.

Подход 2. Mixin ++

Основным недостатком подхода 1 является то, что компонент и миксин всегда вместе. Я не могу создать два экземпляра компонента: один с миксином, а другой без него.

Что ж, это не совсем так, во Vue я могу прикрепить миксин к компоненту «по запросу», их необязательно связывать вместе во всех случаях. Ниже приведен пример того, как это сделать.

Во-первых, необходимо изменить компоненты BlogPost и CommentsList. Я больше не хочу включать в них миксин.

Mixin исчез, но метод selectData должен остаться. Применяются те же правила, что и в первом подходе - определение CommentsList#selectData метода отличается от определения BlogPost.

Чтобы использовать withSubscription миксин по запросу, мне пришлось изменить App компонент.

Когда я хочу использовать компонент с миксином, я вызываю Vue.extend(%mixin%).extend(%component%) внутри определения компонента.

Такой подход выглядит действительно многообещающим. Он решает те же проблемы, что и HOC, я могу разделять логику между компонентами, и я могу «прикрепить» эту логику к компоненту только тогда, когда мне нужно ее использовать. К сожалению, это не идеально. Прежде всего, мне пришлось объявить метод selectData внутри компонентов BlogPost и CommentsList, хотя в большинстве случаев он мне может не понадобиться. Я могу представить, что через некоторое время я могу забыть, что метод selectData вызывается миксином, и в результате я могу удалить это, а затем приземлится в аду отладки. Более того, миксин withSubscription предоставляет данные компоненту - fetchedData. Мне нужно добавить резервное значение в данные компонента на случай, если я хочу использовать компонент без примеси. Без отката я получу сообщение об ошибке в консоли о том, что я ссылаюсь на свойство, которое не определено в экземпляре.

export default {
    data() {
        return {
            fetchedData: ''
        }
    },
    methods: {
        handleChange() {
            this.fetchedData = this.selectData(...)
        }
    },
    ....
}

Подход 3. Слоты с заданной областью

Поначалу мне было очень сложно разобраться в слотах с ограниченным диапазоном. Официальная документация оказалась не очень полезной. Прежде чем я начну объяснять, как преобразовать HOC в Scoped Slot, я вкратце объясню, в чем заключается идея Scoped Slots.

Слоты с заданной областью дают возможность передавать свойства компоненту, который отображается внутри слота другого компонента. То, как мы определяем слоты с ограниченной областью видимости, не сильно отличается от определения стандартных слотов. Представим, что у нас есть ComponentParent, внутри которого есть слот с ограниченным объемом. Более того, этот компонент содержит вычисляемое свойство под названием someComputed. Слот с ограниченным объемом определяется следующим образом:

<slot :data="someComputed"></slot>

Это очень похоже на то, что между компонентами проходит регулярный реквизит. Сложная часть - это использование слота с ограниченной областью видимости. Представим, что у нас есть еще один компонент, ComponentChild. Мы хотим отрендерить ComponentChild внутри ComponentParent слота и передать значение someComputed из ComponentParent в ComponentChild.

Использование слота с ограниченной областью видимости выглядит следующим образом

<component-parent>
  <component-child :slot-scope="parentScope"  :parentData="parentScope.data"/>
</component-parent>

и вот что происходит в приведенном выше примере:

  • ComponentChild отображается внутри слота, объявленного в ComponentParent
  • ComponentChild разделяет область видимости ComponentParent и хранит ссылку на эту область внутри свойства parentScope
  • ComponentChild считывает свойство data из parentScope. Значение parentScope.data - это значение вычисляемого свойства someComputed из ComponentParent

Имея это очень простое определение слотов с ограниченной областью видимости, я могу перейти к описанию того, как заменить компонент более высокого порядка слотом с ограниченной областью видимости.

Во-первых, компоненты CommentsList и BlogPost - они выглядят точно так же, как и в подходе с компонентом более высокого порядка, и это здорово. Нет необходимости добавлять какой-либо дополнительный код внутри определений компонентов, чтобы использовать слот с заданной областью.

Во-вторых, определение компонента с Scoped Slot, давайте назовем компонент: WithSubscription:

Компонент содержит все общие функции:

  • он присоединяет слушателей изменений при создании компонента и отключает их при уничтожении компонента
  • он обновляет данные при каждом изменении внутри DataSource
  • он вызывает метод selectData, который передается как опора и может отличаться в каждом сценарии использования
  • он передает fetchedData компоненту, который он отображает

И, наконец, использование:

Приведенный выше код лишь немного сложнее, чем код из примера в начале этого абзаца.

Компоненты BlogPost и CommentsList отображаются внутри слота с заданной областью действия. Компонент WithSubscription принимает в качестве опоры метод selectData, который отличается для каждого компонента. Затем каждый компонент получает доступ к fetchedData из компонента WithSubscription через область, называемую withSubscriptionScope.

Резюме

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

Они лучше, чем компоненты более высокого порядка? Сложно сказать. Это другой подход к решению одной и той же проблемы.

Они лучше, чем компоненты более высокого порядка внутри приложения Vue.js? Возможно - да. Решение Scoped Slots предоставляется Vue.js, HOC, с другой стороны, представляют собой настраиваемую реализацию, которая может сломаться при изменении внутренних компонентов Vue.js.

Слоты с ограниченным доступом решают еще одну проблему. Они предотвращают конфликты имен. Разработчик должен объявить имя области видимости, поэтому легко понять, откуда берется каждая опора.

Если вы хотите узнать больше о конфликте имен в HOC, я рекомендую видео Майкла Джексона: Use a Render Prop! и статья Чан Вана: Решение проблем компонентов высшего порядка без выброса ребенка вместе с водой из ванны.

Вы можете найти код из этой статьи в репозитории на github. Вы можете найти там 4 ветки, по одной для каждого подхода, описанного в статье.