Axios/Vue - предотвращение выполнения axios.all()

В моем приложении, аутентифицирующем пользователя, я вызываю функцию fetchData. Если токен пользователя станет недействительным, приложение запустит axios.all() и мой перехватчик выдаст много ошибок.

Как предотвратить axios.all() после первой ошибки? И показать только одно уведомление пользователю?

interceptors.js

export default (http, store, router) => {
    http.interceptors.response.use(response => response, (error) => {
        const {response} = error;

        let message = 'Ops. Algo de errado aconteceu...';

        if([401].indexOf(response.status) > -1){
            localforage.removeItem('token');

            router.push({
                name: 'login'
            });

            Vue.notify({
                group: 'panel',
                type: 'error',
                duration: 5000,
                text: response.data.message ? response.data.message : message
            });
        }

        return Promise.reject(error);
    })
}

auth.js

const actions = {
    fetchData({commit, dispatch}) {
        function getChannels() {
            return http.get('channels')
        }

        function getContacts() {
            return http.get('conversations')
        }

        function getEventActions() {
            return http.get('events/actions')
        }

        // 20 more functions calls

        axios.all([
            getChannels(),
            getContacts(),
            getEventActions()
        ]).then(axios.spread(function (channels, contacts, eventActions) {
            dispatch('channels/setChannels', channels.data, {root: true})
            dispatch('contacts/setContacts', contacts.data, {root: true})
            dispatch('events/setActions', eventActions.data, {root: true})
        }))
    }
}

Ответ 1

РЕДАКТИРОВАТЬ: @tony19 ответ гораздо лучше, поскольку он позволяет отменять запросы, ожидающие обработки после первой ошибки, и не требует дополнительной библиотеки.


Одним из решений было бы назначить уникальный идентификатор (я буду использовать пакет uuid/v4 в этом примере, не стесняйтесь использовать что-то еще) для всех запросов, которые вы используете одновременно:

import uuid from 'uuid/v4'

const actions = {
    fetchData({commit, dispatch}) {
        const config = {
            _uuid: uuid()
        }

        function getChannels() {
            return http.get('channels', config)
        }

        function getContacts() {
            return http.get('conversations', config)
        }

        function getEventActions() {
            return http.get('events/actions', config)
        }

        // 20 more functions calls

        axios.all([
            getChannels(),
            getContacts(),
            getEventActions()
        ]).then(axios.spread(function (channels, contacts, eventActions) {
            dispatch('channels/setChannels', channels.data, {root: true})
            dispatch('contacts/setContacts', contacts.data, {root: true})
            dispatch('events/setActions', eventActions.data, {root: true})
        }))
    }
}

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

export default (http, store, router) => {
    // Here, you create a variable that memorize all the uuid that have
    // already been handled
    const handledErrors = {}
    http.interceptors.response.use(response => response, (error) => {
        // Here, you check if you have already handled the error
        if (error.config._uuid && handledErrors[error.config._uuid]) {
            return Promise.reject(error)
        }

        // If the request contains a uuid, you tell 
        // the handledErrors variable that you handled
        // this particular uuid
        if (error.config._uuid) {
            handledErrors[error.config._uuid] = true
        }

        // And then you continue on your normal behavior

        const {response} = error;

        let message = 'Ops. Algo de errado aconteceu...';

        if([401].indexOf(response.status) > -1){
            localforage.removeItem('token');

            router.push({
                name: 'login'
            });

            Vue.notify({
                group: 'panel',
                type: 'error',
                duration: 5000,
                text: response.data.message ? response.data.message : message
            });
        }

        return Promise.reject(error);
    })
}

Дополнительное примечание, вы можете упростить вашу функцию fetchData до этого:

const actions = {
    fetchData({commit, dispatch}) {
        const config = {
            _uuid: uuid()
        }

        const calls = [
            'channels',
            'conversations',
            'events/actions'
        ].map(call => http.get(call, config))

        // 20 more functions calls

        axios.all(calls).then(axios.spread(function (channels, contacts, eventActions) {
            dispatch('channels/setChannels', channels.data, {root: true})
            dispatch('contacts/setContacts', contacts.data, {root: true})
            dispatch('events/setActions', eventActions.data, {root: true})
        }))
    }
}

Ответ 2

В качестве альтернативы отмене Axios, вы можете использовать Bluebird Promise Cancellation, что проще.

Преимущества новой отмены по сравнению со старой отменой:

  • .cancel() является синхронным.
  • для отмены работы не требуется установочный код
  • сочетаются с другими функциями bluebird, такими как Promise.all

Вот демо. Я добавил некоторые записи в axios.get(...).then(...) для отслеживания завершения каждого вызова.

Закомментируйте строку promises.forEach(p => p.cancel()) чтобы убедиться, что без отмены безошибочные вызовы будут выполняться до конца.

//for demo, check if fetch completes 
const logCompleted = (res) => console.log('Promise completed, '${res.config.url}'') 

function getChannels() {
  return axios.get("https://reqres.in/api/users?page=1&delay=5").then(logCompleted)
}
function getContacts() {
  return axios.get("https://reqres.in/api/users?page=2").then(logCompleted)
}
function getEventActions() {
  return axios.get("https://httpbin.org/status/401").then(logCompleted)
}

Promise.config({ cancellation: true }); // Bluebird config
window.Promise = Promise; // axios promises are now Bluebird flavor

const promises = [getChannels(), getContacts(), getEventActions()];
Promise.all(promises)
  .then(([channels, contacts, eventActions]) => {
    console.log('Promise.all.then', { channels, contacts, eventActions });
  })
  .catch(err => {
    console.log('Promise.all.catch, '${err.message}'')
    promises.forEach(p => p.cancel());
  })
  .finally(() => console.log('Promise.all.finally'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/bluebird/latest/bluebird.core.min.js"></script>

Ответ 3

Поднятый ответ предлагает решение, которое требует ожидания завершения всех ответов, зависимости от uuid и некоторой сложности вашего перехватчика. Мое решение избегает всего этого и обращается к вашей цели прекратить выполнение Promise.all().

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

fetchData({ dispatch }) {
  const source = axios.CancelToken.source();

  // wrapper for GET requests
  function get(url) {
    return axios.get(url, {
        cancelToken: source.token // watch token for cancellation
      }).catch(error => {
        if (axios.isCancel(error)) {
          console.warn('canceled ${url}, error: ${error.message}')
        } else {
          source.cancel(error.message) // mark cancellation for all token watchers
        }
      })
  }

  function getChannels() {
    return get('https://reqres.in/api/users?page=1&delay=30'); // delayed 30 secs
  }
  function getContacts() {
    return get('https://reqres.in/api/users?page=2'); // no delay
  }
  function getEventActions() {
    return get('https://httpbin.org/status/401'); // 401 - auth error
  }

  ...
}

В вашем перехватчике вы также игнорировали бы ошибки от отмены запроса:

export default (http, store, router) => {
  http.interceptors.response.use(
    response => response,
    error => {
      if (http.isCancel(error)) {
        return Promise.reject(error)
      }

      ...

      // show notification here
    }
}

демонстрация