web-dev-qa-db-fra.com

Axios / Vue - Empêchez axios.all () de continuer à s'exécuter

Dans mon application, authentifiant l'utilisateur, j'appelle la fonction fetchData. Si le jeton utilisateur devient invalide, l'application exécutera axios.all() et mon intercepteur retournera beaucoup d'erreurs.

Comment empêcher axios.all() de continuer à fonctionner après la première erreur? Et n'afficher qu'une seule notification à l'utilisateur?

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})
        }))
    }
}
17
Caio Kawasaki

EDIT: réponse de @ tony19 est beaucoup mieux car il permet d'annuler les requêtes toujours en attente après la première erreur, et ne nécessite aucune bibliothèque supplémentaire.


Une solution serait d'attribuer un identifiant unique (j'utiliserai le uuid/v4 package dans cet exemple, n'hésitez pas à utiliser autre chose) pour toutes les requêtes que vous utilisez en même temps:

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})
        }))
    }
}

Ensuite, dans votre intercepteur, vous pouvez choisir de gérer l'erreur une seule fois en utilisant cet identifiant unique:

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);
    })
}

Remarque supplémentaire, vous pouvez simplifier votre fonction fetchData en:

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})
        }))
    }
}
6
Hammerbot

Comme alternative à Axios cancel, vous pouvez utiliser Bluebird Promise Cancellation qui est plus simple.

Les avantages de la nouvelle annulation par rapport à l'ancienne annulation sont:

  • .cancel () est synchrone.
  • aucun code d'installation requis pour que l'annulation fonctionne
  • compose avec d'autres fonctionnalités de Bluebird, comme Promise.all

Voici une démo. J'ai ajouté une connexion dans axios.get(...).then(...) pour suivre si chaque appel se termine.

Mettez en commentaire la ligne promises.forEach(p => p.cancel()) pour vérifier que sans annulation, les appels sans erreur se termineront.

//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>

Pourquoi ça marche

Promise.all () au lieu de axios.all ()

En regardant ce vieux problème axios Supprimer axios.all Et axios.spread # 1042 peut voir

Axios utilise Promise.all sous le capot ...

et ça

axios.all([getUserAccount(), getUserPermissions()])
  .then(axios.spread(function (acct, perms) {
    // Both requests are now complete
}));

peut être remplacé par ce

Promise.all([getUserAccount(), getUserPermissions()])
  .then(function ([acct, perms]) {
    // Both requests are now complete
});

afin que nous puissions passer directement au travail avec Promises et avoir toujours les mêmes fonctionnalités.


Les promesses échouent rapidement

De MDN nous voyons

Promise.all est rejeté si l'un des éléments est rejeté. Par exemple, si vous transmettez quatre promesses qui se résolvent après un délai d'attente et une promesse qui rejette immédiatement, Promise.all rejettera immédiatement.

donc dans ce modèle

Promise.all(...)
.then(...)
.catch(...);

.catch() se déclenchera lorsque la première promesse échouera (contrairement à then() qui attend que toutes les promesses soient terminées).


Composer Promise.all Et .cancel()

Le modèle est assez simple, il suffit d'annuler toutes les promesses dans .catch() (qui est appelée à la première erreur).

Réf cette question pour plus de détails Arrêtez d'autres promesses lorsque Promise.all () rejette


Substitution de Bluebird dans Vue store

Il s'agit d'une implémentation de base dans Vuex.

yarn add bluebird
import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";
import Promise from 'bluebird';
Vue.use(Vuex);

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

export default new Vuex.Store({
  actions: {
    fetchData({ dispatch }) {
      function getChannels() {
        return axios.get("https://reqres.in/api/users?page=1&delay=5");
      }
      function getContacts() {
        return axios.get("https://reqres.in/api/users?page=2");
      }
      function getEventActions() {  // 401 - auth error
        return axios.get("https://httpbin.org/status/401");
      }

      const promises = [getChannels(), getContacts(), getEventActions()];
      Promise.all(promises)
        .then(([channels, contacts, eventActions]) => {
          dispatch("channels/setChannels", channels.data, { root: true });
          dispatch("contacts/setContacts", contacts.data, { root: true });
          dispatch("events/setActions", eventActions.data, { root: true });
        })
        .catch(err => {
          promises.forEach(p => p.cancel());
        })
    }
  }
});
2
Richard Matsen

La réponse positive propose une solution qui nécessite d'attendre toutes les réponses pour terminer, une dépendance à uuid, et une certaine complexité dans votre intercepteur. Ma solution évite tout cela et répond à votre objectif de terminer l'exécution de Promise.all().

Axios prend en charge annulation de la demande , vous pouvez donc envelopper vos demandes GET avec un gestionnaire d'erreurs qui annule immédiatement les autres demandes en attente:

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
  }

  ...
}

Dans votre intercepteur, vous ignorez également les erreurs d'annulation de demande:

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

      ...

      // show notification here
    }
}

démo

1
tony19