平時業務中下載檔案方式常見的有倆種:

第一種,直接訪問伺服器的檔案地址,自動下載檔案;

第二種 ,伺服器返回blob檔案流,再對檔案流進行處理和下載。

一般小檔案適用於第一種下載方案,不佔用過多伺服器資源,而對於體積龐大的檔案,常常使用檔案流的方式進行傳輸,如圖:

檔案流傳輸成功後通過程式碼可以立即發起瀏覽器下載該檔案流:

這種方式也有弊端,在檔案流傳輸過程中,使用者無法感知檔案流的傳輸狀態(進度),會造成一些困擾(無法確定當前下載操作是否已經生效)。針對這種情況,我們可以在頁面顯示檔案流的狀態和傳輸進度,提高頁面互動性和友好性。

接下來是具體實現:

封裝js方法:

  1. /**
  2. * @param {Object} data: {url: 檔案地址, download: 檔名稱}
  3. */
  4. export function downLoadAll(data) {
  5. let downProgress = {};
  6. let uniSign = new Date().getTime() + ''; // 可能會連續點選下載多個檔案,這裡用時間戳來區分每一次下載的檔案
  7. axios.get(
  8. data.url,
  9. { responseType: 'blob', headers: { "Content-Type": "application/json; charset=utf-8" },
  10. onDownloadProgress (progress) {
  11. downProgress = Math.round(100 * progress.loaded / progress.total) // progress物件中的loaded表示已經下載的數量,total表示總數量,這裡計算出百分比
  12. store.commit('caseInformation/SET_PROGRESS', {path: uniSign, 'progress': downProgress}) // 將此次下載的檔名和下載進度組成物件再用vuex狀態管理
  13. }}).then( (res)=>{ // 檔案流傳輸完成後,開啟檔案下載
  14. if(data.downLoad){
  15. jsFileDownload(res.data,data.downLoad+'.'+data.url.replace(/.+\./,"")); // jsFileDownLoad是用來下載檔案流的,下載外掛:npm i js-file-download,import引入:import jsFileDownLoad from 'js-file-download'
  16. } else {
  17. jsFileDownload(res.data, data.url.split('/')[data.url.split('/').length-1]);
  18. }
  19. }).catch((e)=>{
  20. this.$message.error('該檔案無法下載')
  21. })
  22. }

store中的caseInfomation.js:

  1. ...
  2.  
  3. const state = {
  4. progressList: [], // 檔案下載進度列表
  5. ...
  6. }
  7.  
  8. const mutations = {
  9. SET_PROGRESS: (state, progressObj)=>{ // 修改進度列表
  10. if(state.progressList.length){ // 如果進度列表存在
  11. if(state.progressList.find(item=>item.path == progressObj.path)){ // 前面說的path時間戳是唯一存在的,所以如果在進度列表中找到當前的進度物件
  12. state.progressList.find(item=>item.path == progressObj.path).progress = progressObj.progress // 改變當前進度物件的progress
  13. }
  14. }else{
  15. state.progressList.push(progressObj) // 當前進度列表為空,沒有下載任務,直接將該進度物件新增到進度陣列內
  16. }
  17. },
  18. DEL_PROGRESS: (state, props) => {
  19. state.progressList.splice(state.progressList.findIndex(item=>item.path == props), 1) // 刪除進度列表中的進度物件
  20. },
  21. ...
  22. }

頁面顯示進度彈框程式碼,downLoadNotice.vue:

  1. <template>
  2.  
  3. </template>
  4.  
  5. <script>
  6. import { mapState } from 'vuex'
  7.  
  8. export default {
  9. name: 'downLoadNotice',
  10. computed: {
  11. ...mapState({
  12. 'progressList': state => state.caseInformation.progressList
  13. })
  14. },
  15. data() {
  16. return {
  17. notify: {} // 用來維護下載檔案進度彈框物件
  18. }
  19. },
  20. watch: { // 監聽進度列表
  21. progressList: {
  22. handler(n) {
  23. let data = JSON.parse(JSON.stringify(n))
  24. data.forEach(item => {
  25. const domList = [...document.getElementsByClassName(item.path)]
  26. if (domList.find(i => i.className == item.path)) { // 如果頁面已經有該進度物件的彈框,則更新它的進度progress
  27. domList.find(i => i.className == item.path).innerHTML = item.progress + '%'
  28. } else {
  29. if (item.progress === null) { // 此處容錯處理,如果後端傳輸檔案流報錯,刪除當前進度物件
  30. this.$store.commit('caseInformation/DEL_PROGRESS', item.path)
  31. return
  32. }// 如果頁面中沒有該進度物件所對應的彈框,頁面新建彈框,並在notify中加入該彈框物件,屬性名為該進度物件的path(上文可知path是唯一的),屬性值為$notify(element ui中的通知元件)彈框物件
  33. this.notify[item.path] = this.$notify.success({
  34. // title: 'Info',
  35. dangerouslyUseHTMLString: true,
  36. message: `<p style="width: 100px;">正在下載<span class="${item.path}" style="float: right">${item.progress}%</span></p>`, // 顯示下載百分比,類名為進度物件的path(便於後面更新進度百分比)
  37. showClose: false,
  38. duration: 0
  39. })
  40. }
  41. console.log(item.progress + '%', '-------------------------->')
  42.  
  43. if (item.progress == 100) { // 如果下載進度到了100%,關閉該彈框,並刪除notify中維護的彈框物件
  44. this.notify[item.path].close()
  45. // delete this.notify[item.path] 上面的close()事件是非同步的,這裡直接刪除會報錯,利用setTimeout,將該操作加入非同步佇列
  46. setTimeout(() => {
  47. delete this.notify[item.path]
  48. }, 1000)
  49. this.$store.commit('caseInformation/DEL_PROGRESS', item.path)// 刪除caseInformation中state的progressList中的進度物件
  50. }
  51. })
  52. },
  53. deep: true
  54. }
  55. }
  56. }
  57. </script>
  58.  
  59. <style scoped>
  60.  
  61. </style>

以上程式碼我們可以封裝到mixins資料夾中,在頁面內用mixins混入:

頁面中觸發下載操作:

  1. downLoad(item){
  2. let downData = {
  3. url: `ipdoc${item.url}`,
  4. downLoad: item.fileName
  5. }
  6. this.$commonUtils.downLoadAll(downData) // 下載
  7. },

最終頁面的效果:

最後注意一點,以上的下載進度並不是真正下載的檔案,而是檔案流,檔案流下載完成後,再通過上文的js-file-download外掛下載真正的檔案!

腳踏實地行,海闊天空飛