Отмена незавершенных запросов

В современных одностраничных приложениях (SPA) управление асинхронными запросами становится критически важным. При использовании Page.js навигация между страницами может инициировать новые HTTP-запросы или другие асинхронные операции. Если пользователь покидает страницу до завершения этих операций, оставшиеся запросы могут привести к неконсистентному состоянию приложения, утечкам памяти или конфликтам данных.

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


Использование AbortController для отмены fetch-запросов

Для отмены HTTP-запросов через fetch стандартным инструментом является AbortController. Его использование обеспечивает полное управление жизненным циклом запроса.

import page from 'page';

let currentController;

page('/users', async (ctx, next) => {
  if (currentController) {
    currentController.abort(); // Отмена предыдущего запроса
  }

  currentController = new AbortController();
  const { signal } = currentController;

  try {
    const response = await fetch('/api/users', { signal });
    const data = await response.json();
    renderUsers(data);
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log('Запрос был отменен');
    } else {
      console.error(err);
    }
  }
});

Ключевые моменты:

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

Отмена нескольких параллельных запросов

Если страница инициирует несколько асинхронных операций одновременно, полезно использовать коллекцию контроллеров.

let controllers = [];

page('/dashboard', async () => {
  controllers.forEach(c => c.abort());
  controllers = [];

  const urls = ['/api/stats', '/api/notifications', '/api/messages'];
  
  urls.forEach(url => {
    const controller = new AbortController();
    controllers.push(controller);

    fetch(url, { signal: controller.signal })
      .then(res => res.json())
      .then(data => renderData(url, data))
      .catch(err => {
        if (err.name === 'AbortError') {
          console.log(`Запрос ${url} отменен`);
        } else {
          console.error(err);
        }
      });
  });
});

Особенности подхода:

  • Каждому запросу соответствует отдельный AbortController.
  • При переходе на новую страницу все активные контроллеры вызывают abort().
  • Позволяет избежать ситуации, когда устаревшие данные перезаписывают актуальные.

Интеграция с middleware Page.js

Для более системного подхода полезно создавать middleware, которое автоматически управляет отменой запросов.

function cancelPreviousRequests() {
  let controller;

  return (ctx, next) => {
    if (controller) controller.abort();
    controller = new AbortController();
    ctx.signal = controller.signal;
    return next();
  };
}

page('/profile', cancelPreviousRequests(), async (ctx) => {
  try {
    const response = await fetch('/api/profile', { signal: ctx.signal });
    const data = await response.json();
    renderProfile(data);
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log('Запрос к профилю отменен');
    }
  }
});

Преимущества:

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

Отмена асинхронных функций вне fetch

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

function cancellableTimeout(ms) {
  let timer;
  const promise = new Promise((resolve, reject) => {
    timer = setTimeout(resolve, ms);
  });
  promise.cancel = () => clearTimeout(timer);
  return promise;
}

let currentTimeout;

page('/loading', async () => {
  if (currentTimeout) currentTimeout.cancel();
  currentTimeout = cancellableTimeout(5000);

  try {
    await currentTimeout;
    console.log('Завершение операции');
  } catch (err) {
    console.log('Операция отменена');
  }
});

Особенности метода:

  • Создание кастомных отменяемых промисов.
  • Возможность интеграции с любой асинхронной логикой.
  • Гибкость при управлении жизненным циклом операций.

Практические рекомендации

  1. Всегда связывать асинхронные операции с конкретным маршрутом – это предотвращает устаревшие данные при быстрой навигации.
  2. Использовать AbortController как стандарт для fetch-запросов.
  3. Для сторонних библиотек, которые не поддерживают отмену, необходимо создавать обертки или проверять состояние маршрута перед обновлением интерфейса.
  4. Централизованное middleware помогает унифицировать подход к отмене и делает код более читаемым и поддерживаемым.
  5. Обрабатывать ошибки отмены отдельно от остальных, чтобы исключить ложные сообщения об ошибках.

Эффективное управление незавершенными запросами в Page.js обеспечивает стабильность интерфейса, предотвращает утечки памяти и гарантирует, что пользователь всегда видит актуальные данные.