В своей предыдущей статье я описал способ создания компонентов высшего порядка в Vue.js. Я получил много отличных отзывов, которые вдохновили меня на написание этой статьи.
Прежде чем начать искать ответ на вопрос: Нужны ли нам компоненты высшего порядка в Vue.js? Я хотел бы задать еще один вопрос: какие проблемы решают компоненты высшего порядка? Я считаю, что ответ на этот вопрос имеет решающее значение.
Для меня есть две основные проблемы, которые решают HOC:
- повторение кода: они позволяют разделять общие функции между разными компонентами,
- организация кода: они позволяют расширить функциональность компонента на уровне создания экземпляра, а не на уровне объявления.
Vue.js предоставляет нам два решения для решения проблемы повторения кода и улучшения организации кода: миксины и слоты с ограниченной областью видимости. В этой статье я собираюсь сравнить эти два подхода с подходом компонентов более высокого порядка, который я описал в предыдущей статье.
Подход 0. Компонент более высокого порядка
Этот пост основан на примере из статьи Компоненты высшего порядка в Vue.js. Ниже для справки приведен код приложения из предыдущей статьи.
Во-первых, основной компонент приложения:
Во-вторых, компоненты CommentsList
и BlogPost
:
И, наконец, withSubscription
компонент высшего порядка:
withSubscription
Компонент высшего порядка выполняет четыре обязанности:
- получить данные из внешнего источника данных
- передать полученные данные обернутому компоненту
- добавить слушателя изменений к внешнему источнику данных при монтировании компонента
- удалить прослушиватель изменений из внешнего источника данных при уничтожении компонента
Подход 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 ветки, по одной для каждого подхода, описанного в статье.