Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Vue
Code

Шаблоны проектирования для коммуникации между компонентами Vue.js

by
Difficulty:AdvancedLength:LongLanguages:

Russian (Pусский) translation by AlexBioJS (you can also view the original English article)

(* шаблон проектирования – обобщенное описание способа решения определённого класса задач. Здесь и далее примеч. пер.). Как разработчики мы хотим создавать код, которым легко управлять и который удобно сопровождать, а также легче отлаживать и тестировать. Для этого мы заимствуем установившиеся практики, известные как шаблоны. Шаблоны – проверенные алгоритмы и архитектуры, которые помогают нам эффективно и предсказуемо решать конкретные задачи.

В этом руководстве мы рассмотрим наиболее распространенные шаблоны проектирования для коммуникации между компонентами Vue.js, а также «подводные камни», которых следует избегать. Все мы знаем, что в реальной жизни нет одного единственного способа решения всех проблем. Подобно этому при создании приложений Vue.js отсутствует универсальный шаблон для всех сценариев разработки. Каждый шаблон имеет свои преимущества и недостатки и предназначен для конкретных ситуаций. Разработчикам, использующим Vue.js, обязательно нужно знать все наиболее распространенные шаблоны, чтобы мы могли выбрать подходящий шаблон для конкретного проекта. Тогда в нашем приложении коммуникация будет происходить эффективно и так, как нам нужно.

Почему подходящий вариант коммуникации между компонентами важен?

Когда мы создаем приложение при помощи фреймворка, работающего на основе компонентов, вроде Vue.js, мы стремимся сделать компоненты нашего приложения настолько изолированными, насколько это возможно. За счет этого они ставятся пригодными для повторного использования, удобными в сопровождении и для тестирования. Для того чтобы добавить для компонента возможность повторного использования, нам необходимо оформить его в более абстрактной и изолированной (или слабосвязанной) форме, и в таком виде мы можем добавить в наше приложение или удалить его, не нарушая работу функциональных возможностей приложения.

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

Краткий обзор коммуникации между компонентами Vue.js

Во Vue.js имеется два главных типа коммуникации между компонентами:

  1. Непосредственная коммуникация между родительскими и дочерними компонентами, основанная на строгих отношениях типа родитель – потомок и потомок – родитель.
  2. Опосредованная коммуникация, при которой один компонент может «обращаться» к любому другому, независимо от типа их отношений.

В следующих разделах мы разберем оба типа вместе с соответствующими примерами.

Непосредственная коммуникация типа родитель – потомок

Стандартной моделью коммуникации между компонентами, которую Vue.js изначально поддерживает, является модель типа родитель – потомок, реализованная за счет пропов (* пользовательские атрибуты, которые вы можете зарегистрировать для компонента. При передаче значения этому атрибуту оно становится свойством образца того компонента) и пользовательских (* пользователей фреймворка) событий. На схеме ниже вы можете ознакомиться с визуальным представлением того, как эта модель выглядит в действии.

Как вы видите, родительский компонент может общаться только с его непосредственными дочерними компонентами, а дочерние компоненты могут общаться только непосредственно с их родительскими компонентами. В этой модели невозможна коммуникация между сиблингами (* потомство одних родителей) или опосредованная коммуникация.

В следующих разделах мы реализуем приведенные на схеме выше компоненты в серии практических примеров.

Коммуникация типа родитель – потомок

Давайте предположим, что имеющиеся у нас компоненты являются частью игры. В большинстве игр отображается счет очков где-то в интерфейсе. Представьте, что у нас в компоненте Parent A имеется переменная score, и мы хотим отобразить ее значение в компоненте Child A. Что ж, как нам это сделать?

Для передачи данных от родительского компонента его дочерним компонентам во Vue.js используются пропы. Передача пропа осуществляется в три шага:

  1. Регистрация пропа в дочернем компоненте, например так: props: ["score"]
  2. Использование зарегистрированного пропа в шаблоне родительского компонента, например так: <span>Score: {{ score }}</span>
  3. Привязывание пропа к переменной score (в шаблоне родительского компонента), например так: <child-a :score="score"/>

Давайте разберем полный пример, чтобы лучше понять, что в действительности происходит:

Пример на CodePen

Проверка правильности данных, передаваемых в пропы

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

При использовании пропов, пожалуйста, убедитесь, что вы понимаете разницу между их литеральным (* литерал –  представление в программе какого-либо числового (numeric literal) или символьного (string literal) значения) и динамическим вариантами. Проп динамический, если мы привязываем его к переменной (например, v-bind:score="score" или its shorthand :score="score"), и соответственно значение пропа будет изменяться в зависимости от значения переменной. Если мы просто задаем значение без привязывания, то оно будет проинтерпретировано буквально, и результат будет статическим. В нашем случае, если мы пишем score="score", то будет отображено score вместо 100. Это литеральный проп. Вам следует всегда учитывать это тонкое различие.

Обновление пропа дочернего компонента

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

Мы создали метод changeScore(), при помощи которого должен будет обновиться счет очков после нажатия кнопки Change Score. Похоже, что при этом счет обновляется корректно, однако в консоли выводится следующее предупреждение Vue:

[предупреждение Vue]: избегайте непосредственного изменения значения пропа, поскольку значение будет перезаписываться каждый раз при повторном вычислении значений характеристик родительского компонента.  Вместо этого используйте свойство объекта, возвращаемого функцией data, или свойством computed (* от англ. вычисленный, рассчитанный; используется при реализации какой-либо сложной логики, чтобы шаблоны оставались простыми и описательными), значение которого определяется на основании значения пропа. Проп, значение которого было изменено: "score"

Как вы видите, Vue сообщает нам, что значение пропа будет перезаписываться при повторном вычислении значений характеристик родительского компонента. Давайте проверим, так ли это, за счет симулирования этой ситуации при помощи встроенного метода $forceUpdate():

Пример на CodePen

Теперь, когда мы изменяем счет и затем нажимаем кнопку Rerender Parent, то видим, что счет возвращается к изначальному значению, заданному в родительском компоненте. Так что Vue говорит правду!

Тем не менее, помните, что массивы и объекты, заданные в дочерних компонентах, будут влиять на родительские компоненты, поскольку они не копируются, а передаются по ссылке.

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

Изменение значения пропа при помощи свойства объекта, возвращаемого функцией data

Первым способ заключается в том, чтобы заменить переменную score на локальное свойство (localScore) объекта, возвращаемого функцией data, которое мы можем использовать в методе changeScore() и в шаблоне:

Пример на CodePen

Теперь, если мы нажмем кнопку Rerender Parent снова, после того как изменили счет, то увидим, что в этот раз счет остается тем же.

Изменение значения пропа при помощи свойства сomputed

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

Пример на CodePen

Здесь мы создали метод doubleScore() объекта computed, при помощи которого значение переменной score родительского компонента помножается на два, и затем результат отображается в шаблоне. Очевидно, что после нажатия Rerender Parent не будет никаких побочных эффектов.

Коммуникация типа потомок – родитель

Теперь давайте посмотрим, как компоненты могут общаться в обратном направлении.

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

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

  1. В дочернем компоненте генерируется событие, описывающее изменение, которое мы хотим сделать, следующим образом: this.$emit('updatingScore', 200).
  2. В родительском компоненте регистрируется обработчик для сгенерированного события следующим образом: @updatingScore="updateScore".
  3. После возникновения события проп будет обновлен при помощи назначенного метода следующим образом: this.score = newValue.

Давайте разберем полный пример, чтобы лучше понять, как это происходит:

Пример на CodePen

Мы используем встроенный метод $emit() для генерирования события. Метод принимает два аргумента. В качестве первого аргумента выступает событие, которое хотим сгенерировать, а в качестве второго – новое значение.

Модификатор .sync

Во Vue вам предлагается модификатор .sync, при помощи которого вышеописанный прием выполняется подобным образом, и мы, возможно, захотим его использовать в качестве краткой формы в некоторых случаях. В этом случае мы используем метод $emit() немного по-другому. В качестве аргумента события мы передаем update:score следующим образом: this.$emit('update:score', 200). Затем при привязывании пропа score мы добавляем модификатор .sync следующим образом: <child-a :score.sync="score"/>. В компоненте Parent A мы удаляем метод updateScore() и код для регистрации события (@updatingScore="updateScore"), поскольку они нам более не нужны.

Пример на CodePen

Почему бы не воспользоваться this.$parent и this.$children для непосредственной коммуникации типа родитель – потомок?

Во Vue имеется два метода API, которые предоставляют нам непосредственный доступ к родительским и дочерним компонентам: this.$parent и this.$children. По началу может показаться заманчивым использовать их в качестве более быстрой и простой альтернативы пропам и событиям, однако нам не следует этого делать. Это считается плохой практикой, или анти-шаблоном, поскольку при их использовании между родительским и дочерним компонентами образуется тесная связь. В результате получаются неподатливые и ненадежные компоненты, с которыми тяжело работать и которые тяжело отлаживать. Эти методы API редко используются, и на практике нам следует избегать их использования или использовать с осторожностью.

Двухсторонняя коммуникация между компонентами

Пропы и события передаются в одном направлении. Пропы опускаются вниз, события поднимаются вверх. Однако за счет совместного использования пропов и событий мы можем обеспечить эффективную коммуникацию с выше- и нижерасположенными компонентами дерева компонентов, в результате чего реализуется двухсторонняя привязка данных (* механизм уведомления, связывающий поля свойств (property) управляющего элемента, помещенного в контейнер, с источником (source) данных, напр., полем БД; например, связывание данных с их источником). Именно по этому механизму собственно работает v-model.

Опосредованная коммуникация

Паттерн коммуникации типа родитель – потомок быстро становится неудобным и непрактичным по мере усложнения нашего приложения. Проблема системы, работающей на основе пропов и событий, заключается в том, что в ней нет промежуточных ступеней и она тесно привязана к дереву компонентов. События во Vue не всплывают (* от внутреннего элемента вверх через родителей), в отличие от нативных событий, и поэтому нам необходимо повторно генерировать их, пока не достигнем целевого компонента. В результате наш код переполняется множеством обработчиков и генераторов событий. Поэтому в более сложных приложениях нам стоит задуматься об использовании шаблона опосредованной коммуникации.

Давайте рассмотрим схему ниже:

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

В следующих разделах мы разберем наиболее распространенные варианты реализации опосредованной коммуникации.

Глобальный автобус событий

Глобальный автобус событий – образец Vue, который мы используем для генерирования и прослушивания событий. Давайте разберемся с ним на практике.

Пример на CodePen

Ниже перечислены шаги, которые необходимо выполнить для создания и использования автобуса событий:

  1. Объявление нашего автобуса событий, в качестве которого выступает новый образец Vue, следующим образом: const eventBus = new Vue ()
  2. Генерирование события в компоненте, который является его источником, следующим образом: eventBus.$emit('updatingScore', 200)
  3. Прослушивание сгенерированного события в целевом компоненте следующим образом: eventBus.$on('updatingScore', this.updateScore)

В примере кода выше мы удаляем @updatingScore="updateScore" из дочернего элемента и используем вместо этого зацепку жизненного цикла created Vue (* каждый экземпляр проходит серию шагов инициализации при создании. Также при этом запускаются зацепки – место в программе, куда можно подсоединить дополнительный код (обычно для расширения её функциональных возможностей), за счет которых у пользователя появляется возможность добавления своего собственного кода на определенных стадиях) для прослушивания события updatingScore. После возникновения события будет выполнен метод updateScore(). Также мы можем передать метод для обновления данных в виде анонимной функции:

При помощи шаблона "глобальный автобус событий" может решиться до некоторой степени проблема переполнения кода множеством обработчиков и генераторов событий, однако возникают новые проблемы. Данные приложения могут быть изменены бесследно из любой части приложения. Из-за этого усложняется процесс отладки и тестирования приложения. Для более сложных приложений, в которых дела могут быстро выйти из-под контроля, нам стоит подумать об использовании специального шаблона управления состоянием, например Vuex, за счет которого мы получим возможность более тонко гранулированного (* метафорическое определение (обозначение) процесса или системы для работы с небольшими объектами, например, отдельными битами и байтами, а не с относительно большими объектами, например файлами или записями) контроля, полезные возможности отслеживания изменений и отладки, а также улучшиться структура и организация кода.

Vuex

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

Здесь я дам вам краткое объяснение того, чем является Vuex, а также проиллюстрирую его в контексте рассматриваемого нами в этом руководстве примера. Если вы хотите глубже изучить Vuex, то я вам рекомендую взглянуть на мое специально посвященное этой теме руководство о создании сложных приложений при помощи Vuex.

Давайте теперь рассмотрим следующую схему:

Как видите, приложение Vuex состоит из четырех отдельных частей:

  • State (* Состояние) – место, где мы храним данные нашего приложения.         
  • Getters (* Геттеры) – методы для получения состояния хранилища и передачи его компонентам.
  • Mutations (* Мутации (скачкообразные изменения)) – фактические и единственные методы для изменения состояния.
  • Actions (* Действия) – методы для выполнения асинхронного кода и запуска мутаций.

Давайте создадим простое хранилище и посмотрим, как все это работает на практике.

Пример на CodePen

В хранилище у нас имеется следующее:

  • Переменная score, значение которой задано в объекте состояния.
  • Мутация incrementScore(), при помощи которой значение счета будет увеличено на переданное значение.
  • Геттер score(), при помощи которого значение переменной score будет получено из состояния и передано компонентам.
  • Действие incrementScoreAsync(), в котором будет использована мутация incrementScore() для увеличения значение счета по истечении переданного периода времени.

В образце Vue мы используем вместо пропов свойства объекта computed для получения значения счета при помощи геттеров. Затем для изменения счета в компоненте Child A мы используем мутацию store.commit('incrementScore', 100). В компоненте Parent B мы используем действие store.dispatch('incrementScoreAsync', 3000).

Внедрение зависимости

Перед тем как завершить, давайте разберем еще один шаблон. Он используется, главным образом, при работе с библиотеками общедоступных компонентов, однако его стоит упомянуть для полноты картины.

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

Давайте посмотрим, как это выглядит в действии:

Пример на CodePen

За счет опции provide компонента GrandParent переменная score становится доступной для всех его дочерних компонентов. Каждый из них может получить доступ к ней за счет объявления свойства inject: ['score']. И, как вы видите, счет отображается во всех компонентах.

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

Почему бы не воспользоваться this.$root для опосредованной коммуникации?

Причины, по которым нам не следует использовать this.$root, подобны тем, что были указаны для описанных выше случаев с использованием this.$parent и this.$children – при этом компоненты становятся слишком тесно связанными. Использования любого из этих методов для реализации коммуникации между компонентами следует избегать.

Как выбрать подходящий шаблон?

Что же, теперь вы в курсе обо всех распространенных методах коммуникации между компонентами. Но как вам понять, какой больше всего подходит для вашего случая?

Выбор подходящего шаблона зависит от проекта, в котором вы работаете, или от приложения, которое вы хотите создать. Он зависит от сложности и типа вашего приложения. Давайте рассмотрим наиболее распространенные случаи:

  • При создании простых приложений все, что вам будет необходимо, – пропы и события.
  • При создании среднемасштабных приложений вам будут необходимы более гибкие пути коммуникации, например автобус событий и внедрение зависимости.
  • При создании сложных, крупномасштабных приложений вам точно понадобятся возможности Vuex как полнофункциональной системы управления состоянием.

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

Заключение

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

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.