Компоненты в Javascript

Недавно я изо всех сил пытался создать приложение с ванильным javascript, следуя методологии компонентов. То есть -

  1. Один компонент должен сосредоточиться на одном разделе пользовательского интерфейса.
  2. Один компонент не должен напрямую обращаться к HTML другому.
  3. Компонент инкапсулирует свои HTML, CSS и JavaScript.
  4. У каждого компонента есть способ взаимодействия с другими.

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

Из-за этих преимуществ все основные фреймворки или библиотеки, такие как React, Vue, Angular, используют его.

Если мы рассмотрим React, он следует всем этим правилам -

  1. Мы создаем компоненты класса, специфичные для одного раздела пользовательского интерфейса.
  2. Прямой доступ к DOM запрещен.
  3. У каждого компонента есть метод рендеринга, который отвечает за HTML компонента. Ни один компонент напрямую не изменяет чужой html.
  4. Мы используем Redux, чтобы при изменении состояния одного компонента можно было обновлять и другие части приложения, или компоненты.

Так как же все это сделать в ванильном javascript?

Первые три соображения связаны с дизайном и очень просты с es2017. Сегодня я хочу остановиться на четвертом требовании. Как заставить компоненты взаимодействовать друг с другом.

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

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

Таким образом, один из способов сделать это - предоставить некоторые методы компонента List, например updateList, и вызвать эту функцию из компонента Search всякий раз, когда пользователь что-то вводит. Но в этом есть серьезная проблема. Мы плотно соединили оба компонента. Если, скажем, мы вносим некоторые изменения в метод updateList, это может нарушить функциональность поиска. Таким образом, ответственность была разделена между двумя. И это проблема. Еще одна вещь: пока мы говорим только о двух компонентах. В нашем приложении, вероятно, будет гораздо больше компонентов, и если мы будем зависеть друг от друга, отладка кода станет кошмаром. Как этого избежать?

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

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

Событие происходит, когда его публикует один компонент. Важно, чтобы опубликованное событие было уникальным для этого компонента. То есть только один компонент может опубликовать определенное событие и никто другой.

В нашем примере мы можем сделать это -

  1. Компонент Search публикует событие, скажем, searchComp / search, когда пользователь вводит поисковый запрос.
  2. Компонент List подписывается на это событие. Таким образом, каждый раз, когда пользователь отправляет поисковый запрос, компонент List узнает об этом и может обновлять список.

Давайте посмотрим на код, чтобы понять, как мы на самом деле это делаем.

Примечание. Я использую es2017 с операторами импорта и деструктуризацией объектов.

// searchComponent.js
import { publishEvent } from './utilities/eventBus';
const searchInputElem = document.getElementById('searchInput');
const searchSubmitElem = document.getElementById('searchSubmit');
searchSubmitElem.addEventListener('click', function () {
  publishEvent('searchComp/search', {
    query: searchInputElem.value
  })
})
--------------------------------------------------------------------
// listComponent.js
import { subscribeEvent } from './utilities/eventBus';
subscribeEvent('searchComp/search', function ({ query }) {
  console.log(`you entered ${query}`);
  //do something else
})

Достоинства такого подхода -

  1. В будущем, если будут добавлены новые функции, нам просто нужно будет публиковать новые события и добавлять на них подписчиков.
  2. Издателю события не нужно ничего знать о компонентах, которые на него подписались.

Реализация шины событий

Как вы можете видеть в предыдущем фрагменте кода, утилита eventBus предоставляет два метода subscribeEvent и publishEvent для обработки событий.

Посмотрим, как реализована эта утилита -

// utilities/eventBus
const events = {};
export function subscribeEvent(event, listener) {
  if (!events[event]) {
    events[event] = [];
  }
  if (!events[event].includes(listener)) {
    events[event].push(listener);
  }
}
export function publishEvent(event, payload = {}) {
  const listeners = events[event];
  if (!listeners) {
    return;
  }
  for (let i = 0, l = listeners.length; i < l; i += 1) {
    listeners[i](payload);
  }
}

Объяснение -

  1. events - это объект, ключи которого представляют событие.
  2. Каждый ключ имеет массив слушателей в качестве значения.
  3. Когда происходит событие, выбирается соответствующий ключ из объекта events, а затем вызываются все слушатели.

Куда пойти отсюда -

  1. Вы можете добавить unsubscribeEvent метод, чтобы компонент мог прекратить прослушивание события.
  2. Вы можете создать компонент предварительной загрузки, который подписывается на события serverAction / start и serverAction / finish и соответственно отображает / скрывает.
  3. Ознакомьтесь с Code Runner, чтобы увидеть полный проект, использующий этот шаблон.