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

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

А иногда это приводит и к ошибкам. Например, у нас есть две страницы - страница соревнований и страница турниров, обе страницы имеют id в URL и данные запрашиваются по этому id. Когда пользователь быстро переходит со страницы турнира на страницу соревнования, то id турнира может подмениться на id соревнования, и тогда получится, что запрос пойдет на несуществующий для турнира id и упадет ошибка.

Когда я столкнулся с этой проблемой, то первой мыслью было использовать AbortController для отмены таких “висящих” запросов, и я решил загуглить как это правильно сделать, но сразу же был крайне удивлен тем, что советы на stackoverflow или различные статьи как-то очень странно и скудно освещали эту проблему, иногда предлагая абсолютно нерабочие решения, поэтому и решено было написать эту статью, чтобы показать конкретный рабочий пример.

Для тех, кто с этим не знаком, AbortController - это, простыми словами, интерфейс, который позволяет управлять отменой http запросов со стороны фронтенда.

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

// abortController.js

let controller = new AbortController()

export const getControllerSignal = () => controller.signal

export const abortController = () => controller.abort()

export const reinitController = () => {
  controller = new AbortController()
}

Пройдемся по всему по порядку:

  • signal нам нужен для передачи его в запрос, чтобы в дальнейшем мы могли этим запросом управлять и отменять его

  • метод abort() нужен собственно для отмены запроса, в который мы передали signal

  • и важный момент - для того чтобы запросы прерывались не навсегда после первого раза, а только в нужный момент, после его отмены нам нужно заново проинициализировать наш контроллер, для этого и нужен метод reinitController, который просто вызывает new AbortController()

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

// apiHelper.js

import { getControllerSignal } from '@/services/abortController'

export const callPrivateApi = async(method, url, ...args) => {
  try {
    const { data } = await privateApi[method](url, ...[...args, { signal: getControllerSignal() }])
    ...
  } catch ({ response }) {
    ...
  }
}

У нас проект на Vue.js, поэтому далее будет пример кода именно для него.

В файле роутера воспользуемся хуком beforeEach, который будет срабатывать при каждой смене страницы, в нем нам нужно будет отменить запросы, а также заново проинициализировать контроллер:

// router.js

import { abortController, reinitController } from '@/services/abortController'

...

router.beforeEach(() => {
  abortController()
  reinitController()
})

Вот и все! Теперь все незаконченные (pending) запросы будут прерываться при смене страницы, не вызывая ненужных ошибок и не используя зря сеть пользователей сайта с плохим интернетом.

Выглядеть эти запросы будут вот так, со статусом canceled:

Комментарии (6)


  1. nin-jin
    08.07.2023 08:57

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


    1. sadiolem Автор
      08.07.2023 08:57
      +1

      Есть какие-то предложения?


      1. Carduelis
        08.07.2023 08:57
        +5

        Использовать его фреймворк, очевидно же


      1. Carduelis
        08.07.2023 08:57

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


        1. sadiolem Автор
          08.07.2023 08:57

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

          То что прерванный запрос мог бы пригодиться на другой странице для нас слишком edge кейс пока что) Но замечание хорошее, кому-то это стоит учитывать.


      1. nin-jin
        08.07.2023 08:57
        +8

        Реактивная зависимость, которая, пока жива, загружает и мемоизирует данные. А когда все подписки от неё пропадают, отменяет - запрос, если он ещё не завершился.