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