Представьте следующий сценарий: вы разрабатываете расширение Chrome, основной функцией которого является прослушивание изменений на странице, вызванных пользователем. Для каждого изменения есть определенное действие, которое нужно вызвать, например рендеринг компонента пользовательского интерфейса и вызов API.
Как лучше всего решить эту задачу? Первая мысль — создать классы и в них статические функции, используемые в любом месте кода. Внутри этих функций можно будет вызывать и возвращать все, что нужно. Хотя это один из вариантов решения проблемы, он может оказаться обременительным, особенно если у вас предусмотрены межклассовые действия и данные, которыми необходимо обмениваться. В таких случаях возможны трудности с поддержкой и отладкой.
Лучшим решением в плане гибкости и организованности будет создание механизма обработки событий, который поддерживает инициирование событий на основе поведения пользователя.
Chrome поставляется со встроенными обработчиками событий для таких распространенных событий, как нажатие на кнопку и переход по новому URL. В react-приложении для этого можно использовать Redux. Однако иногда нужно запускать события в более сложных сценариях, например при перенаправлении на сторонний сайт, ожидании рендеринга элемента пользовательского интерфейса на сайте и при входе/выходе пользователя из системы.
В компании Eleos столкнулись именно с такой проблемой, поэтому пришлось придумать собственное решение.
Имейте в виду, что в этой статье рассматривается конкретное решение вышеупомянутой проблемы. Существует множество подходов к достижению желаемого результата, и каждый из них имеет свои плюсы и минусы.
Решение для регистратора/эмиттера
Если вы не знакомы с событийно-управляемой разработкой, настоятельно рекомендую почитать об этом, прежде чем погружаться в решение. Твердое понимание того, чего мы пытаемся достичь, очень важно.
Рассматриваемый подход включает два компонента.
- Класс Event Emitter для обработки подписки и эмиссии событий.
- Компонент или класс, который регистрируется для одного или нескольких событий, и когда это событие происходит, запускается действие.
Итак, углубимся в то, как все это будет работать.
Наглядно представить этот процесс поможет следующее изображение:
Компоненты должны подписаться (зарегистрироваться) на определенное событие, а эмиттер событий отвечает за обработку этих подписок и отправку (эмиссию) событий. Но что значит “эмиттировать событие”? В конце процесса есть действие, которое необходимо вызвать в ответ на событие.
Теперь отступим еще на один слой и подумаем, какие параметры нужны для подписки на определенное событие.
- Имя события, на которое нужно подписаться.
- Идентификатор компонента, чтобы знать, какой компонент зарегистрирован на определенное событие.
- Обратный вызов — функция, которая будет запущена при наступлении события.
Итак, для каждого события, на которое нужно зарегистрироваться, необходимо предоставить вышеуказанные три свойства.
Как хранить события? Можно создать объект на объекте 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. У нас есть класс, который отвечает за регистрацию и эмиссию событий, и есть другие компоненты, которые могут быть зарегистрированы на определенное событие. Когда событие отправляется, вызывается функция эмиссии, которая отвечает за выполнение обратного вызова, прикрепленного к событию.
Обратите внимание, что это решение является базовым и может не справиться с некоторыми проблемами, вызванными, например, переполнением событий. В таком случае можно создать буфер событий, который будет контролировать количество событий, отправленных в определенный промежуток времени. Необходимо также добавить условия для обработки нулевых случаев или событий с пустым именем. Данное решение является основой реализации и может быть оптимизировано в соответствии с конкретными потребностями.
Читайте также:
- Как создавать доступные веб-приложения для дальтоников с помощью Chrome DevTools
- 5 инструментов Chrome DevTools, упрощающих разработку
- Как создать Chrome-расширение для приложения с прогнозом погоды
Читайте нас в Telegram, VK и Дзен
Перевод статьи Idan Biton: Event Driven Development on Browser Extension