Axios/Vue - 防止 axios.all() 继续执行

Posted

技术标签:

【中文标题】Axios/Vue - 防止 axios.all() 继续执行【英文标题】:Axios/Vue - Prevent axios.all() to keep executing 【发布时间】:2019-07-13 14:30:20 【问题描述】:

在我的应用程序中,为了验证用户身份,我调用了 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)
        ))
    

【问题讨论】:

在Wait until all ES6 promises complete, even rejected promises 看到这个answer。您也可以将AsyncGeneratorAsyncIterator 替换为.all(),参见Run multiple recursive Promises and break when requested;另见Jquery Ajax prevent fail in a deferred sequential loop。 axio.all() 使用Promise.all()。您能否证明Promise.all() 在第一次异常后继续执行或拒绝Promise?为什么.catch() 没有链接到.then() 来处理错误? axios.all执行任何东西,它不能“停止”任何东西。它只是建立一个等待其他承诺的承诺。您正在立即调用getChannels()getContacts()getEventActions(),当您从它们那里收到第一个错误时,它们都已经在运行了。 您最好的选择可能是发出一个请求来检查用户令牌是否有效,并且只有在成功时才运行其他请求。而不是依赖拦截器。 嗨,我可以看到你重新打开了关于这个问题的赏金。我的回答怎么没有回答你的问题?您还有其他我的回答没有满足的要求吗? 【参考方案1】:

编辑:@tony19's answer 更好,因为它允许在第一次错误后取消仍待处理的请求,并且不需要任何额外的库。


一种解决方案是为您同时使用的所有请求分配一个唯一标识符(我将在此示例中使用 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】:

upvoted answer 提出了一个解决方案,该解决方案需要等待 所有 响应完成,依赖于 uuid,并且您的拦截器有些复杂。我的解决方案避免了所有这些,并解决了您终止 Promise.all() 执行的目标。

Axios 支持request cancelation,因此您可以使用错误处理程序包装您的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
    

demo

【讨论】:

这是我最喜欢的一个,因为它只使用 Axios 功能,而且正如您在此处所说的,赞成的答案等待所有承诺完成。但是我不明白应该如何调用拦截器中最初失败的通知。是吗? @Hammerbot 当调用取消令牌时,它会为该令牌上的所有其他挂起请求引发错误,这会导致调用拦截器的错误处理程序。 嗬,好的,我明白了。通知显示是因为第一个错误在第一次取消之前通过拦截器。这绝对应该是经过验证的答案。我正在编辑我的答案以提及这一点。【参考方案3】:

作为 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>

为什么会起作用

Promise.all() 代替 axios.all()

看这个axios老问题Remove axios.all and axios.spread #1042可以看

Axios 在后台使用 Promise.all...

还有这个

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

可以用这个替换

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

所以我们可以切换到直接使用 Promises 并且仍然具有相同的功能。


承诺很快就会失败

从MDN我们看到

如果任何元素被拒绝,Promise.all 将被拒绝。例如,如果您传入四个在超时后解析的 Promise 和一个立即拒绝的 Promise,那么 Promise.all 将立即拒绝。

所以在这个模式中

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

.catch() 将在第一个 Promise 失败时触发(与 then() 相比,它一直等到所有 Promise 完成)。


组成Promise.all.cancel()

模式非常简单,只需取消 .catch() 中的所有承诺(在第一个错误时调用)。

详情请参考此问题Stop other promises when Promise.all() rejects


在 Vue 商店中替换 Bluebird

这是 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());
        )
    
  
);

【讨论】:

以上是关于Axios/Vue - 防止 axios.all() 继续执行的主要内容,如果未能解决你的问题,请参考以下文章

axios.all,如何配置axios等待时间来缓解挂机?

Axios.all 在 Nestjs 中可用吗?

axios.all 的动态使用

根据选项将 axios.get URL 传递给 axios.all

无法从 getServerSideProps 返回 axios.all 数据

如何使用 axios all 将 axios 响应数据传递给 vue 3