AbortController для отмены запросов при смене страницы (SPA)
- воскресенье, 9 июля 2023 г. в 00:00:11
Существует довольно распространенная проблема, когда пользователь заходит на страницу, для этой страницы начинают запрашиваться различные данные, но вдруг пользователь быстро переходит на другую страницу, а запросы с предыдущей страницы продолжают выполняться, все еще используя ресурсы сети и выполняя ненужную работу.
Особенно неприятно это становится тогда, когда у пользователя медленный интернет и каждый лишний запрос еще больше замедляет работу вебсайта. К слову, про оптимизацию 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
: