Перейти к содержанию

События Web3-кошелька: подписки и очистка обработчиков

Оглавление курса «Web3-интерфейс: реестр в браузере» · Урок 14 из 30.

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

Предполагаются контроллер чтения и отдельные контексты аккаунта и сети. Здесь не добавляем новую возможность контракта: улучшаем обычную инженерную границу веб-интерфейса. Листинги не запускались. Ожидаемое поведение рассматривается по последовательности создания, удаления и повторного создания карточки курса 17.

Кто владеет подпиской

Подписку создаёт конкретный экран или контроллер. Он же должен сохранить функцию очистки. Передача провайдера глобальному модулю без понятного владельца обычно оставляет вопрос: когда снимать обработчики и кто отменяет применение результата запроса? Для небольшого проекта достаточно функции mount, которая возвращает dispose.

Очистка имеет два разных эффекта. Снятие обработчика прекращает получение будущих событий. Инвалидация поколения запрещает применение уже начатого асинхронного ответа. Одного из этих действий недостаточно для другого: снятый обработчик не отменит HTTP-запрос, а увеличение поколения не удалит подписку, которая продолжит создавать новые запросы.

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

Контроллер экрана

Ниже общий модуль mount-library.js. Он использует готовые createReader, accountContext и chainContext; конкретный рендер состояния передаётся параметром. Код показывает владение ресурсами, не повторяя реализацию предыдущих модулей.

export function mountLibrary({ root, provider, read, render }) {
  let disposed = false;
  const reader = createReader(read, state => {
    if (!disposed) render(root, state);
  });
  const invalidate = () => reader.invalidate();
  const accounts = accountContext(provider, invalidate);
  const chain = chainContext(provider, invalidate);
  const refreshButton = root.querySelector('#refresh');
  const refresh = () => reader.refresh();
  refreshButton.addEventListener('click', refresh);
  return {
    refresh,
    dispose() {
      if (disposed) return;
      disposed = true;
      refreshButton.removeEventListener('click', refresh);
      accounts.dispose();
      chain.dispose();
      reader.dispose();
    },
  };
}

Импорты четырёх модулей добавляются в начале реального файла: они определены в предыдущих уроках. Это фрагмент объединения, а не самостоятельный файл без зависимостей. При отсутствии кошелька экран чтения создаёт контроллер без контекстов провайдера; публичный просмотр не становится зависимым от расширения только ради подписок.

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

Очистка и отправленная операция

Удаление экрана не отменяет транзакцию в сети. Если операция уже получила хеш, её история хранится отдельно от DOM. Контроллер страницы прекращает рисовать receipt, но сервис наблюдения может продолжить работу и сохранить результат. При следующем открытии экрана данные восстанавливаются из журнала операций.

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

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

Источник события и текущий экран

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

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

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

Временные таймеры

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

В маленькой лаборатории можно обойтись ручным обновлением до уроков о событиях. Не надо добавлять интервал «на всякий случай» и затем искать, почему запросы продолжаются после перехода. Сначала определяется нужное наблюдение, затем его периодичность и условие завершения. Например, подтверждённый receipt заканчивает конкретное ожидание, но не останавливает всю историю реестра.

Для упражнения нарисуйте последовательность mount → запрос → dispose → ответ → mount. Ожидается, что ответ первого контроллера не рисует второй экран, а каждое нажатие новой кнопки запускает только одно чтение. Это критерий будущей ручной проверки; его исполнение здесь не проводилось. Методы подписки и очистки описаны в EIP-1193, а DOM-слушатели — в removeEventListener.

Ошибка во время создания

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

В маленьком листинге такая логика сокращена ради видимости основного маршрута. При превращении его в проект следует явно обработать отсутствие кнопки, недоступный провайдер и ошибку создания подписки. Нельзя считать Markdown-фрагмент полной библиотекой управления ресурсами только по наличию dispose. Описанный механизм задаёт основу, а конкретные гарантии проверяются будущим самостоятельным запуском.

Оглавление серии