Событийно-ориентированная разработка на основе браузерного расширения

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

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

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

Chrome поставляется со встроенными обработчиками событий для таких распространенных событий, как нажатие на кнопку и переход по новому URL. В react-приложении для этого можно использовать Redux. Однако иногда нужно запускать события в более сложных сценариях, например при перенаправлении на сторонний сайт, ожидании рендеринга элемента пользовательского интерфейса на сайте и при входе/выходе пользователя из системы.

В компании Eleos столкнулись именно с такой проблемой, поэтому пришлось придумать собственное решение.

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

Решение для регистратора/эмиттера

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

Рассматриваемый подход включает два компонента.

  1. Класс Event Emitter для обработки подписки и эмиссии событий.
  2. Компонент или класс, который регистрируется для одного или нескольких событий, и когда это событие происходит, запускается действие.

Итак, углубимся в то, как все это будет работать.

Наглядно представить этот процесс поможет следующее изображение:

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

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

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

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

Как хранить события? Можно создать объект на объекте window.top, где каждый ключ будет именем события. Это позволит отслеживать, какие компоненты зарегистрированы для каких событий, а значением будет массив, состоящий из объектов, как описано выше.

Примечание: можно также поместить внутрь объекта некоторые другие свойства для целей отладки (например, URI, контекст страницы и т. д.).

Теперь рассмотрим примеры кода. Допустим, у нас есть компонент (это может быть обычный JS-объект, React-компонент, JS- или TS-файл), и нам нужно вызвать определенное действие, когда зарегистрированное событие будет отправлено.

import { eventEmitter } from './eventEmitter';
export default class Example {
constructor() {
...
this.registerEventListeners();
...
}
registerEventListeners() {
eventEmitter.subscribe('login', 'class-id', () => {
// какие-либо действия с вашей стороны
});
}
}

Итак, как видно на приведенном выше примере, все довольно просто. Импортируем объект eventEmitter, чтобы использовать метод subscribe, подписываемся на событие 'login' под определенным ID и привязываем обратный вызов, который выполнится при отправке события.

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

...
eventEmitter.emit('login');
...

Нужно лишь эмиттировать событие с конкретным именем, на которое мы подписались.

Итак, теперь мы знаем, как подписываться на события и отправлять их, но как будет выглядеть эмиттер события?

Посмотрим. Помните, как мы обрабатываем объект событий?

export default class EventEmitter {
...
subscribe(name, id, cb) {
if (!window.top.events[name]) {
// в случае, если у нас еще нет ключа события для нашего объекта
window.top.events[name] = [];
}
// добавить новую подписку
window.top.events[name].push({id,func: cb});
}
...
}

И опять все довольно просто: получаем id, имя и обратный вызов, проверяем, есть ли значение в объекте events под именем события, создаем массив, если его нет, и просто “выталкиваем” объект.

Последнее  —  реализация функции эмиссии события:

emit(name, ...args) {
if (!window.top.events[name]) return; // предотвращает нулевые случаи
// просмотр всех подписок по имени события
window.top.events[name].forEach((s) => {
console.log(`Emitting event for event id: ${id}`);
try {
// выполнение обратного вызова
s.func.call(...args);
} catch (e) {
console.log(`Event failed to emit func ${e}`);
}
});
},

Примечание: передаем дополнительный аргумент args на случай, если понадобится передать дополнительные аргументы для вызова.

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

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

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Idan Biton: Event Driven Development on Browser Extension

Предыдущая статья8 инструментов для предпринимателей, похожих на ChatGPT
Следующая статьяИз финансов в разработку: как стать инженером-программистом