【axios】你的进度条准确吗

1、axios监听进度

上传和下载操作在前端中是非常常见的,当我们想知道上传或下载的进度时也不难,axios已经实现了监听进度的方法

import axios from 'axios'
// 上传请求
axios.post('/api/v1/upload', { data: xxx
  },
  { // onUploadProgress回调可以获取进度
    onUploadProgress(e) { const complete = e.loaded / e.total * 100
    }
	}
)
// 或者
axios({ method: 'POST',
  data: { data: xxx
  },
  onUploadProgress(e) { const complete = e.loaded / e.total * 100
  }
})

翻一下axios的源码,看看它是如何实现的

在/lib/adapters/xhr.js文件中,可以看到这么一段代码

let request = new XMLHttpRequest()
// Not all browsers support upload events
if (typeof config.onUploadProgress === 'function' && request.upload) { request.upload.addEventListener('progress', progressEventReducer(config.onUploadProgress));
}

axios是基于XMLHttpRequest来实现的

其中,config就是我们传给的axios的参数,如果是上传操作并且有传递onUploadProgress函数的话

就监听XMLHttpRequest的progress事件,然后周期性地触发回调函数progressEventReducer

再看一下progressEventReducer的实现:

function progressEventReducer(listener, isDownloadStream) { return e => { const loaded = e.loaded;
    const total = e.lengthComputable ? e.total : undefined;
    ...
    const data = { loaded,
      total,
      progress: total ? (loaded / total) : undefined,
      ...
    };
    data[isDownloadStream ? 'download' : 'upload'] = true;  // 区别上传或是下载
    listener(data);
  };
}

在progressEventReducer中,就会获取

  • loaded:已上传的buffer数据流
  • total:总的buffer数据流

    最终会将数据传给listener,即一开始传给axios的onUploadProgress回调,所以我们可以通过e.loaded / e.total来获取进度

    2、不准确的进度条

    获取进度条很简单,但是在实际使用中还是碰到了问题

    - 计算出来的进度与实际上传进度不符合

    - 可以看出:进度条已经走完,但是接口一直在pending中

    这个问题还是比较严重的,明明显示完成了上传,但是文件就是没有出来

    而且文件越大,差异就越明显

    原因

    造成这个原因,其实跟TCP协议发送数据的方式有关

    • 在客户端,会有一个send buffer,即数据缓存区
    • 这个buffer缓存区存储的就是等待发送的数据,tcp协议层会在适当的时候选取一部分数据发送出去
    • 当我们上传文件时,数据会被不断地write到这个缓存区;每次写入时就会被侦听到,然后调用一次onUploadProgress,e.loaded其实表示的就是写入到buffer缓存区的数据
    • 但是此时的数据可能还没有被发送出去,仍然在缓存区中放着;加上发送数据也需要时间
    • 等到数据全部到达Server的时候,才会执行response回调,这时候上传操作才算完成;但客户端那边早就显示发送完毕了,时差也就出来了

      3、模拟进度条

      如果解决这个问题呢?有一个比较保守的做法:用一个亦真亦假的进度条

      在进度达到一定值的时候,开始人为干涉;每次让它走一点,给人一种一直在上传的感觉,直到上传完成,赋值为100即可

      这里就使用vue3的方式实现一下:

      import { ref } from 'vue'
      /**
       * 模拟进度条
       * @returns
       */
      export const useProgress = (name: string) => { const progress = ref(0)
        const timer = ref(null)
        const onUploadProgress = (e: ProgressEvent) => { const complete = (e.loaded / e.total) * 100
          // 超过80 开始干涉
          if (complete >= 80) { if (timer.value) return
            timer.value = window.setInterval(() => { progress.value += (100 - progress.value) * 0.2  // 每次增加剩下的20%
              
              // 超过99 不再变化(此时也接近上传完成了)
              if (progress.value > 99) { timer.value && window.clearInterval(timer.value)
              }, 1000)
            } else { // 在80之前都按照axios计算出的进度来显示
            progress.value = complete
          }
      	}
        
        return { progress,
          onUploadProgress
        }
      }
      

      可以在请求完成的时候,将progress设置为100即可

      具体的阈值可以根据情况自行设置

      其实,大名鼎鼎的NProgress库也是这么做的,我们平时使用它的姿势是这样的:

      NProgress.start()  // 开启进度条
      NProgress.done()  // 进度条结束
      

      可是,又没有监听接口,仅仅只是开启进度条,它怎么能预测我们的进度呢?

      答案是:它也是模拟进度条

      在其源码中,有这么一段代码:

      当我们没有传amount的时候,会自动根据当前进度条的status调整每次的进度amount

      当进度staus超过0.994的时候,就会一直停在0.994这个状态

      直到我们执行NProgress.done()时,会直接调用NProgress.set(1),进度条走完

      参考:

      https://github.com/axios/axios

      https://github.com/rstacruz/nprogress