Students Save 30%! Learn & create with unlimited courses & creative assets Students Save 30%! Save Now
Advertisement
  1. Code
  2. Vue
Code

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

by
Difficulty:AdvancedLength:LongLanguages:

Ukrainian (українська мова) 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 можна було би виконати наступним чином:

При використанні пропів, будь ласка, впевніться, що ви розумієте різницю між їх літеральним (* адресна, числова або символьна константа, включена безпосередньо до операторів чи команд програми) та динамічним варіантами. Проп динамічний, якщо ми прив'язуємо його до змінної (например, 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 рідко використовуються, і на практиці нам слід уникати їх використання або використовувати з обережністю.

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

Пропи та події передаються в одному напрямку. Пропи спускаються донизу, події піднімаються угору. Проте завдяки сумісному використанню пропів та подій ми можемо забезпечити ефективну комунікацію з вище- та нижчерозташованими компонентами дерева компонентів, у результаті чого реалізується двостороння прив'язка даних (* зв'язування даних з їх джерелом). Саме за цим механізмом власне працює 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.