10 строк кода, которые уменьшат боль от вашего проекта на Vue +13





… или знакомство с плагинами Vue JS на примере интегрированной шины событий


Пара слов о…


Всем привет! Сразу оговорюсь. Я очень люблю VueJS, активно пишу на нем уже больше 2-х лет и не считаю, что разработка на нем может причинить боль хоть в какой-то значимой степени :)
С другой стороны, мы всегда пытаемся найти универсальные решения, которые помогут тратить меньше времени на механическую работу и больше – на то, что действительно интересно. Иногда решение оказывается особенно удачным. Одним из таких я хочу поделиться с вами. 10 строк, о которых пойдет речь (спойлер: в конце их окажется немного больше), родились в процессе работы над проектом Cloud Blue – Connect, который представляет собой достаточно крупное приложение на 400+ компонентов. Найденное нами решение уже интегрировано в самые разные точки системы и вот уже более полугода ни разу не требовало правок, поэтому его смело можно считать успешно проверенным на устойчивость.

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

Немного о том как компоненты Vue общаются между собой


Пожалуй, первый вопрос, который возникает у человека, написавшего свой первый компонент, касается того, как он получит данные для работы и как, в свою очередь, передаст данные, полученные им, «наружу». Принцип взаимодействия, принятый во фреймворке Vue JS, называется…

Однонаправленный поток данных


Если коротко, этот принцип звучит как «свойства — вниз, события — вверх». То есть для получения данных снаружи («сверху») мы регистрируем внутри компонента специальное свойство, в которое фреймворк при необходимости записывает наши данные, полученные «снаружи». Для того же, чтобы передать данные «наверх», внутри компонента в нужном месте мы вызываем специальный метод фреймворка $emit, который передает наши данные в обработчик родительского компонента. При этом во Vue JS мы не можем просто «транслировать» событие вверх на неограниченную глубину (как например в Angular 1.x). Оно «всплывает» только на один уровень, до непосредственного родителя. То же касается и событий. Чтобы передать их на следующей уровень, для каждого из них также нужно зарегистрировать специальный интерфейс – свойства и события, которые передадут наше «сообщение» дальше.

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

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

Хотя преимущества этого подхода понятны, у него есть и очевидные недостатки, а именно высокая связанность компонентов. Проще говоря для того, чтобы нам поместить какой-то компонент в структуру, нужно обложить его необходимыми интерфейсами, чтобы управлять его состоянием. Для того, чтобы уменьшить эту связанность, чаще всего используют «инструменты управления состояниями». Пожалуй, самый популярный инструмент для Vue это…

Vuex (сторы)


Продолжая нашу аналогию с офисным зданием, Vuex стор – это внутренняя почтовая служба. Представим, что на каждом этаже офиса есть окно выдачи и приема посылок. На пятом этаже передают документ №11 на подпись, а на втором периодически спрашивают: «Есть ли документы на подпись?», подписывают имеющиеся и отдают их обратно. На пятом так же спрашивают: «А есть ли подписанные?». При этом работники могут переехать на другие этажи или в другие помещения – принцип работы не поменяется, пока почта работает.

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

Казалось бы, на этом все проблемы уже решены. Но в какой-то момент в нашем метафорическом здании один сотрудник хочет позвать другого на обед… или сообщить о какой-то ошибке. И здесь начинается странное. Само по себе сообщение не требует передачи как таковой. Но для того, чтобы воспользоваться почтой надо что-то передать. Тогда наши сотрудники придумывают шифр. Один зеленый шарик – идем на обед, два красных кубика – произошла ошибка приложения E-981273, три желтые монетки – проверь почту и так далее.

Нетрудно догадаться, что с помощью этой неуклюжей метафоры я описываю ситуации, когда нам нужно обеспечить реакцию нашего компонента на событие, произошедшее в другом компоненте, которое само по себе никак не связано с потоком данных. Завершено сохранение нового элемента – требуется переспросить коллекцию. Произошла ошибка 403 Unauthorized – требуется запустить выход пользователя из системы и так далее. Обычная (и далеко не лучшая) практика в таком случае – создание флагов внутри стора или косвенная интерпретация хранимых данных и их изменений. Это быстро приводит к загрязнению как самого стора, так и логики компонентов вокруг него.

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

Шина событий


С технической точки зрения шина событий – это объект, который позволяет с помощью одного специального метода запускать «событие» и подписываться на него с помощью другого. Иначе говоря, при подписании на событие «eventA» этот объект сохраняет внутри своей структуры переданную функцию-обработчик, которую он вызовет, когда где-то в приложении будет вызван метод запуска с ключом «eventA». Для подписания или запуска достаточно получить к нему доступ через импорт или по ссылке, и готово.

Метафорически в нашем «здании» шина – это общие чаты в мессенджере. Компоненты подписываются на «общий чат», в который другие компоненты отправляют сообщения. Как только в «чате» появится «сообщение», на которое подписался компонент, запустится обработчик.

Существует множество разных способов создать шину событий. Ее можно написать самостоятельно или можно воспользоваться готовыми решениями – тем же RxJS, который предоставляет огромный функционал для работы с целыми потоками событий. Но чаще всего при работе с VueJS используют, как ни странно, сам VueJS. Экземпляр Vue, созданный через конструктор (new Vue()), предоставляет прекрасный и лаконичный интерфейс событий, описанный в официальной документации.

Здесь мы вплотную подходим к следующему вопросу…

Чего же мы хотим?


А хотим мы встроить в наше приложение шину событий. Но у нас есть два дополнительных требования:

  1. Она должна быть легко доступна в каждом компоненте. Отдельный импорт в каждый из десятков компонентов нам кажется избыточным.
  2. Она должна быть модульной. Мы не хотим держать в голове все имена событий, чтобы избежать ситуации, когда событие «item-created» запускает обработчики со всего приложения. Поэтому мы хотим, чтобы можно было легко отделить небольшой фрагмент дерева компонентов в отдельный модуль и транслировать его события внутри него, а не снаружи.

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

Давайте для начала зарегистрируем наш плагин. Для этого прямо перед точкой инициализации нашего Vue приложения (перед вызовом Vue.$mount()) поместим следующий блок:

Vue.use({   
  install(vue) { }, 
});

Фактически плагины Vue это способ расширения функционала фреймворка на уровне всего приложения. Интерфейс плагинов реализует несколько способов встроиться в компонент, но сегодня мы познакомимся с интерфейсом mixin. Этот метод принимает объект, который расширяет дескриптор каждого компонента перед началом жизненного цикла в приложении. (Код компонентов, который мы пишем, является скорее не самим компонентом, а описанием его поведения и инкапсуляцией определенной части логики, которая в процессе жизненного цикла используется фреймворком на разных его этапах. Инициализация плагина находится за пределами жизненного цикла компонента, предваряя его, поэтому мы говорим «дескриптор», а не компонент, чтобы подчеркнуть, что внутрь mixin секции плагина будет передан именно тот код, который написан в нашем файле, а не какая-то сущность, которая является продуктом работы фреймворка).

Vue.use({
  install(vue) {     
    vue.mixin({}); // <--
  }, 
});

Именно этот пустой объект будет содержать расширения для наших компонентов. Но для начала еще одна остановка. В нашем случае мы хотим создать интерфейс для доступа к шине на уровне каждого компонента. Давайте добавим к нашему дескриптору поле ‘$broadcast’ (англ. — «эфир»), оно будет хранить ссылку на нашу шину. Для этого воспользуемся Vue.prototype:

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null; // <--
    vue.mixin({}); 
  }, 
});

Теперь нам нужно создать саму шину, но сначала давайте вспомним про требование модульности и примем, что в дескрипторе компонента мы будем объявлять новый модуль полем «$module» с каким-то текстовым значением (оно нам понадобится немного позже). Если поле $module будет задано в самом компоненте, создадим для него новую шину, если же нет — передадим ссылку на родительскую через поле $parent. При этом обратим внимание, что поля дескриптора будут нам доступны через поле $options.

Поместим создание нашей шины на как можно более ранний этап – в хук beforeCreate.

Vue.use({
  install(vue) { 
    vue.prototype.$broadcast = null; 
    vue.mixin({
      beforeCreate() {  // <--
        if (this.$options.$module) {  // <--
         
 	} else if (this.$parent && this.$parent.$broadcast) {  // <--
         
        } 
      }, 
    }); 
  }, 
});

И наконец давайте заполним логические ветви. В случае, если дескриптор содержит объявление нового модуля, создадим новый экземпляр шины, если нет, возьмем ссылку из $parent.

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null; 
    vue.mixin({
      beforeCreate() { 
        if (this.$options.$module) {
          this.$broadcast = new Vue();  // <--
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$broadcast = this.$parent.$broadcast;  // <--
        } 
      }, 
    }); 
  }, 
});

Отбрасываем объявление плагина, считаем… 1, 2, 3, 4 … 10 строчек, как я и обещал!

А можем еще лучше?


Конечно, можем. Этот код легко расширяется. Например, в нашем случае мы решили помимо $broadcast добавить интерфейс $rootBroadcast, который дает доступ к единой для всего приложения шине. События, которые пользователь запускает на шине $broadcast, дублируются на шине $rootBroadcast таким образом, чтобы можно было подписаться либо на все события конкретного модуля (в этом случае первым аргументом в обработчик будет передано имя события), либо на все события приложения вообще (тогда имя модуля будет передано в обработчик первым аргументом, имя события — вторым, а данные, переданные с событием, будут передаваться следующими аргументами). Такая конструкция позволит нам наладить взаимодействие между модулями, а также вешать единый обработчик на события разных модулей.

// This one emits event  
this.$broadcast.$emit(‘my-event’, ‘PARAM_A’); 
// This is standard subscription inside module 
this.$broadcast.$on(‘my-event’, (paramA) => {…}); 
// This subscription will work for the same event 
this.$rootBroadcast.$on(‘my-event’, (module, paramA) => {…}); 
// This subscription will also work for the same event 
this.$rootBroadcast.$on(‘*’, (event, module, paramA) => {…});

Давайте посмотрим, как нам этого добиться:

Во-первых, создадим единую шину, к которой будет организован доступ через $rootBroadcast, и само поле со ссылкой:

const $rootBus = new Vue(); // <--

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus; // <--
        if (this.$options.$module) {
          this.$broadcast = new Vue(); 
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$broadcast = this.$parent.$broadcast; 
        } 
      }, 
    }); 
  }, 
});

Теперь нам необходима принадлежность к модулю в каждом компоненте, поэтому давайте расширим определение модульности вот так:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        if (this.$options.$module) {
          this.$module = this.$options.$module;  // <--
          this.$broadcast = new Vue(); 
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;  // <--
          this.$broadcast = this.$parent.$broadcast; 
        } 
      }, 
    }); 
  }, 
});

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

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        if (this.$options.$module) {
          this.$module = this.$options.$module;
          this.$broadcast = { $bus: new Vue() };  // <--
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;
          this.$broadcast = { $bus: this.$parent.$broadcast.$bus };  // <--
        } 
      }, 
    }); 
  }, 
});

И наконец добавим к объекту проксирующие методы — ведь теперь поле $broadcast не предоставляет прямого доступа к шине:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        if (this.$options.$module) {
          this.$module = this.$options.$module;
          this.$broadcast = { $bus: new Vue() };  
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;
          this.$broadcast = { $bus: this.$parent.$broadcast.$bus };
        } 
        // >>>
        this.$broadcast.$emit = (…attrs) => {
          this.$broadcast.$bus.$emit(…attrs);           
          const [event, …attributes] = attrs; 
          this.$rootBroadcast.$emit(event, this.$module, …attributes)); 
          this.$rootBroadcast.$emit(‘*’, event, this.$module, …attributes)
        };
        
        this.$broadcast.$on = (…attrs) => {           
          this.$broadcast.$bus.$on(…attrs);
        };
        // <<<
      }, 
    }); 
  }, 
});

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

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeDestroy() {                               // <--
        this.$broadcast.$off(this.$broadcastEvents);  // <--
      },

      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        this.$broadcastEvents = [];  // <--
        if (this.$options.$module) {
          this.$module = this.$options.$module;
          this.$broadcast = { $bus: new Vue() };  
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;
          this.$broadcast = { $bus: this.$parent.$broadcast.$bus };
        } 

        this.$broadcast.$emit = (…attrs) => {
          this.$broadcastEvents.push(attrs[0]);   // <--
          this.$broadcast.$bus.$emit(…attrs);           
          const [event, …attributes] = attrs; 
          this.$rootBroadcast.$emit(event, this.$module, …attributes)); 
          this.$rootBroadcast.$emit(‘*’, event, this.$module, …attributes)
        };
        
        this.$broadcast.$on = (…attrs) => {           
          this.$broadcast.$bus.$on(…attrs);
        };

        this.$broadcast.$off =: (...attrs) => {  // <--
          this.$broadcast.$bus.$off(...attrs);   // <--
        };
      }, 
    }); 
  }, 
});

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

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

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (16):

  1. reforms
    /#21278254

    Если честно, немного не понятно, зачем плодить модульные broadcast и сами $module, когда можно общаться через единую шину (в примере $rootBroadcast) с помощью сообщений, одним из обязательных параметров которого будет имя модуля. причем, если нет желание писать имя модуля в событии явно, его также можно задекларировать в прототипе вуя как $moduleName. Наличие класса типа ModuleEvent даст возможность понимать где какие события отсылаются/обрабатываются. Это может быть полезным при погружении и фикса багов.

    • ahinz
      /#21285862

      Добрый день. Спасибо за комментарий! Да, действительно такая возможность существует и реализована в $rootBroadcast как вы и заметили (события из модуля дублируются в нем, снабженные в качестве первого параметра именем модуля). Единственная причина такого дробления — некоторое удобство. Для работы исключительно в скоупе модуля нет необходимости держать в голове его название и добавлять его каждый раз. Наши задачи в 90% покрываются именно таким взаимодействием, поэтому решили что это приемлемо.

  2. PavlovM
    /#21279502

    А можно все это организовать через нативный window.postMessage, который не зависит от фреймворков и здорово спасает, если у вас их, вдруг, несколько на проект.

    • ahinz
      /#21285890

      Добрый день. Спасибо за комментарий! Честно говоря, не приходила в голову использовать postMessage для такой тривиальной задачи, но вероятно работать будет. Для нас ценность этого конкретного решения в интегрированности и максимально простом апи

  3. Almatyn
    /#21279726

    А хотим мы встроить в наше приложение шину событий.


    Средствами Vuex мы и так вроде имеем шину событий.
    Любой компонент может подписаться на изменения в любом store. Это помимо встроенной реактивности — что тоже уже шина событий.

    ИМХО может вам просто пересмотреть как вы организовали свое хранилище (Vuex store).

    • ahinz
      /#21285942

      Добрый день. Спасибо за ваш комментарий. Да, конечно Vuex подразумевает реактивность и в 95% случаев нам ее достаточно. Тем не менее есть ~5% случаев, в которых требуется именно реакция на событие а не реакция на изменение данных. Теоретически можно протащить флаги в стор для этого и как-то действовать через них, но им же нужно потом восстанавливать значение и в принципе следить за консистентностью дополнительно. Мы наступили пару раз на грабли с таким подходом и решили перейти на более безопасный в нашей парадигме паттерн Pub/Sub

  4. shurkandak
    /#21281078

    Зачем замусоривать прототип? Можно вполне элегантно через provide\inject.

    • ahinz
      /#21285968

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

  5. NerVik
    /#21281554 / +3

    Поздравляю вас, вы в одном решении использовали кажется все антипаттерны вью.
    Расширение прототипа — во вью3 оставят только как штуку для обратной совместимости легаси плагинов. Больше не рекомендуется к использованию.
    Во вью3 с новым композишн апи нельзя использовать миксины.
    Во вью3 удалят $on, $off, $once.
    Во вью3 удалят как минимум из доки упоминания $root, $parent

    • ahinz
      /#21286012

      Добрый день! Спасибо за ваш комментарий и за предупреждение. В нашем решениях мы исходим из реалий в которых работаем на текущий момент. Уверен, что в vue3 будет какой-то свой, предпочтительный на тот момент, способ предоставлять доступ к некоторым условно внешним объектам. Мы определенно воспользуемся им, чтобы реализовать замещающую функциональность.

  6. CyberAP
    /#21282322 / +1

    Пожалуйста, не делайте так в ваших приложениях. Для реализации Pub\Sub есть множество готовых решений и они не требуют какой-то специальной интеграции для Vue.
    Здесь действительно целый огород из плохих практик:


    1. Шина на Vue (готовится стать deprecated во Vue 3), считается плохой практикой и разработчики Vue это признали сами.
    2. Обращение к $parent — повышает связанность компонентов и работает неявно. Такой код будет очень сложно поддерживать потому что непонятно кто что вызывает.
    3. Миксины. Особенно глобальные миксины. О минусах миксинов писали много раз, но я повторю: конфликты имён, непонятно что добавляет миксин без чтения кода самого миксина, сложно определить кто пропатчил контекст компонента когда миксинов несколько.

    Для хранения глобального состояния и глобальной обработки событий используйте Vuex или Provide\Inject. Если вам нужна связь только по событиям используйте Pub\Sub (но я не смог придумать такой кейс если честно).

    • ahinz
      /#21286118

      Добрый день. Спасибо за то, что внимательно прочитали статью и за такой подробный комментарий. Есть о чем задуматься. Коротко про кейсы:
      0. Задача с Pub/Sub действительно довольно тривиальная и реализаций есть множество — тот же Rx например. Дело в том что конкретно это решение призвано решить очень утилитарную проблему максимально просто и быстро и быть максимально прозрачным в использовании — поэтому хотелось бы решить все без дополнительных зависимостей и желательно без доп документации
      1. На момент написания этого плагина шина на Vue не являлась плохой практикой. Да и в принципе шина событий это паттерн проектирования — ее плохое использование может быть плохой практикой, а сама по себе она просто инструмент. Как вы сами отметили существует много альтернативных решений — мы использовали конкретно Vue только из соображений зависимостей
      2. В целом это совершенно верно в случаях обычного кода и мы не допускаем такого в компонентах. Но в данном случае мы имеем дело с генеричным кодом. Обращение к $parent здесь служит для оценки контекста и по факту именно резолвит связанность в ее прикладном понимании (грубо говоря тот оверхед, который необходим чтобы решение заработало в новом контексте). Опять же результат отработки кода консистентен не зависимо от результатов анализа $parent — событие все равно будет запущено или проксированно. Поэтому лично я не вижу в этом проблемы
      3. Совершенно согласен с вашим утверждением насчет миксинов. Не скажу что совсем нет случаев оправданных для их использования, но они единичны и использовать их надо аккуратно. В данном случае, отмечу снова, мы говорим не про код компонентов, а про генеричное расширение общее для всего приложения, поэтому нет необходимости держать в голове что-то специфичное конкретному контексту — только приложению целиком.

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

  7. Sarir
    /#21282888

    Всё круто, но вынести бы ещё сам объект с методом install (плагин) в отдельный файл и просто в main.js уже вызывать vue.use()

    • ahinz
      /#21286128

      Добрый день. Спасибо за комментарий! Безусловно можно так код организовать) мы пока не стали так делать потому что он довольно лаконичный. Но возможно в перспективе, если будем наращивать функционал, так и сделаем.

  8. Manyaka
    /#21286098

    Идея интересная: для событий бизнес-логики использовать Vuex, а для «реакции нашего компонента на событие, произошедшее в другом компоненте, которое само по себе никак не связано с потоком данных» использовать шину. Как будете дисциплинировать разработчиков придерживаться такого деления? Есть подозрение, что разработчики не смогут точно отделять одно от другого и будут использовать и Vuex и шину для всего.

    • ahinz
      /#21286232

      Добрый день! Спасибо за комментарий)
      Здесь вопрос организации процесса разработки конечно, поэтому за всех не могу говорить, могу только за свою команду)
      У нас с этим не возникло проблем по двум причинам:
      1. При внедрении плагина мы сразу озвучили какие случаи мы решаем с помощью шины, а какие нет. На практике каждое ее использование обсуждается и согласовывается, потому что случаев ее использования очень не много — примерно 5%. Дело просто в том, что эти 5% при других способах порождают неоправданно большой оверхэд если делать их через Vuex или однонаправленный поток. Если буквально — результат асинхронного действия как-то отражается на модели данных и это можно наблюдать через watch? Используй Vuex. Если нет — используй шину.
      2. Контроль на код-ревью)