a標籤與Blob下載檔案的區別和獲取檔案下載進度

南风晚来晚相识發表於2024-12-06

檔案下載的幾種方式。

大家都做過檔案下載,無非就是透過a標籤給定一個href。
使用者點選下載按鈕。
或者使用Blob的方式進行下載。
這兩種是很常見的,也是我們平時做使用最多的方式。
那麼我們知道這2種方式有什麼區別呢?
如果不清楚,也彆著急下面我們一起來探索下:

node + express + cors 搭建環境

// 引入express
const express=require("express");
// 引入路徑模組
const path=require("path")
// 處理跨域的外掛,需要下載一下這個模組
const cors = require('cors')
// 檔案下載相關的介面 
const fileListRouter = require('./routes/fileList'); 
const app=express();
// 處理跨域
app.use(cors())
app.use(express.static(path.join(__dirname, '/public')));
// 下載檔案的路由路徑
app.use('/fileList', fileListRouter);

//服務埠
app.listen(3000,function () {
  console.log("127.0.0.1:3000")
});

檔案下載的思路分析

我們首先需要知道這個被下載檔案的具體地址。
然後我們需要檢查這個檔案是否存在(假設存在就具有訪問許可權)。
如果存在的話,使用非同步的方式進行讀取。
然後給響應頭設定檔案型別、檔名、告訴瀏覽器是是接下載還是展示。
最後一步是把檔案內容以二進位制的形式寫到響應體中,併傳送出去。

node提供一個介面實現檔案下載

const express = require('express');
const path = require('path');  
const fs = require('fs');
const router = express.Router();
router.get('/download', (req, res) => {
  // 獲取傳參物件資訊
  const queryParams = req.query;
  console.log('傳遞的引數物件', queryParams)
  // 獲取下載檔案的名稱,需要注意filename = 檔案+檔案型別,否者會出現404
  const filename = queryParams.fileName
  // 從當前所在的目錄(__dirname)開始,進入到名為 allFIle 的子目錄, 最後定位到名為 filename變數的檔案
  const filePath = path.join(__dirname, 'allFIle', filename); 
  console.log('檔案路徑', filePath)
  // 檢查檔案是否存在  
  fs.access(filePath, fs.constants.F_OK, (err) => { 
    // 如果失敗,err 是一個物件。成功的話,err是null
    if (err) {  
      return res.status(404).send('File not found');  
    } 
    // 'application/octet-stream':一種通用的二進位制資料的MIME型別,表示 "任意二進位制資料"。
    // 當我們不確定檔案的MIME型別,或想要強制瀏覽器將響應作為檔案下載而不是直接開啟時,通常會使用這個型別。
    const mimeType = 'application/octet-stream';
    // 用於指示內容該以何種形式展示,直接在瀏覽器中開啟(內聯顯示)還是作為附件下載 。
    // 這裡的值 attachment; filename="${filename}"告訴瀏覽器將響應的內容作為附件下載,
    res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
    // 設定HTTP響應頭Content-Type,表示響應的內容是任意二進位制資料
    res.setHeader('Content-Type', mimeType);
    // 建立可讀流並傳遞給響應物件
    const fileStream = fs.createReadStream(filePath); 
    fileStream.pipe(res);    
  });  
});  
module.exports = router;

上面程式碼中的fs.access

在 Node.js 中,fs.access 是一個用於檢查檔案或目錄是否存在
以及是否具有特定許可權的非同步檔案系統方法。
該方法允許我們驗證當前程序是否對檔案或目錄具有讀取、寫入或執行許可權。

fs.access的基本語法

fs.access(path, mode, callback)
path: 檔案或目錄的路徑
mode(可選):要檢查的許可權,可以使用或 | 運算子進行組合
  fs.constants.F_OK:檢查檔案是否存在。
  fs.constants.R_OK:檢查是否具有讀取許可權。
  fs.constants.W_OK:檢查是否具有寫入許可權。
  fs.constants.X_OK:檢查是否具有執行許可權。
如果沒有提供 mode,則預設檢查 fs.constants.F_OK
callback(Function):回撥函式,回撥函式中有一個引數err。
如果操作成功,err 為 null;
如果操作失敗,err 是一個 Error 物件。

簡單使用 fs.access

const fs = require('fs');
// 檢查這個檔案是否存在或者可讀。
fs.access('./readme.md', fs.constants.F_OK | fs.constants.R_OK, (err) => {
  if (err) {
    console.error('檔案不存在或不可讀');
  } else {
    console.log('檔案存在且可讀');
  }
});

方式1:前端 a標籤下載

<template>
  <div>
    <el-button>
      <a href='http://127.0.0.1:3000/fileList/download?fileName=shipin.mp4'>透過a標籤直接下載</a>
    </el-button>
  </div>
</template>

方式2:透過Blob的方式下載

<template>
  <div>
    <el-button type="primary" @click="fileDownHandler">
      透過blob來下載
    </el-button>
  </div>
</template>
<script>
import axios from 'axios'
  export default {
    methods: {
      fileDownLoad(fileData, fileName, callBack) {
        // 建立Blob例項  fileData 接受的是一個Blob
        // 等待伺服器把所有的資料都傳輸到瀏覽器的記憶體後,然後再把記憶體變為 blob 格式
        let blob = new Blob([fileData], {
          type: 'application/octet-stream',
        })
        if (!!window.ActiveXObject || 'ActiveXObject' in window) {
          window.navigator.msSaveOrOpenBlob(blob, fileName)
          callBack()
        } else {
          // 建立a標籤
          const link = document.createElement('a')
          // 隱藏a標籤
          link.style.display = 'none'
          // 在每次呼叫 createObjectURL() 方法時,都會建立一個新的 URL 指定源 object的內容
          // 或者說(link.href 得到的是一個地址,你可以在瀏覽器開啟。指向的是檔案資源)
          link.href = URL.createObjectURL(blob)
          console.log('link.href指向的是檔案資源', link.href)
          //設定下載為excel的名稱
          link.setAttribute('download', fileName)
          document.body.appendChild(link)
          // 模擬點選事件
          link.click()
          // 移除a標籤
          document.body.removeChild(link)
          // 回撥函式,表示下載成功
          callBack() 
        }
      },
      fileDownHandler(){
        axios.get('http://127.0.0.1:3000/fileList/download?fileName=shipin.mp4', { responseType: 'blob' }).then((response) => {
          console.log('檔案下載返回來的資料', response)
          this.fileDownLoad(response.data, '影片檔案', ()=>{
            console.log('下載成功')
          })
        }).catch(function (error) {
            console.log(error);
        });
      }
    }
  }
</script>

a標籤的下載方式:

資料從服務端不斷流向瀏覽器
瀏覽器會不會等待伺服器把這個檔案的所有資料都傳輸完後再觸發下載呢?
答案是:不會。等會我們可以透過來驗證一下
瀏覽器只要確認這個響應是成功的。
它就不會去等待全部資料都傳輸過來,才觸發下載。
而是直接去觸發下載行為。
這樣資料就像流水一般,從伺服器經過瀏覽器流向了檔案。
資料從伺服器==>經過瀏覽器 ===>檔案。
這個過程瀏覽器不會儲存這些資料
這樣的好處:哪怕這個檔案有很大(幾十或者上百個G)對瀏覽器的記憶體都不會造成什麼影響。
也就是說:資料經過瀏覽器流向檔案(從網路到磁碟)這一過程對瀏覽器的記憶體幾乎沒怎麼佔用。
總結:透過a標籤的形式來下載大檔案是非常友好的。


blob的下載方式

瀏覽器會等待伺服器把所有的資料都傳輸完成後。
把所有的資料都放入瀏覽器的記憶體中
然後生成一個blob物件
然後建立一個本地的url地址
建立a標籤,透過a標籤,下載保留在瀏覽器中的所有資料
這樣會出現一個問題。
當檔案較大的時候,會出現卡死的現象。
出現卡死的現象的原因:
瀏覽器會等待伺服器中把所有的資料都傳輸完畢後,才進行下載。

a標籤和 Blob 下載的區別

1,在下載過程中,離開當前頁或者屬性頁面。
a標籤下載不會中斷,會繼續下載。Blob會中斷下載。
2,在下載過程中,覽器會不會等待伺服器把該檔案的所有資料都傳輸完後,才觸發下載?
a標籤下載不會等待所有的資料傳輸完畢後才觸發下載,瀏覽器只要確認這個響應是成功的。就會馬上觸發下載。
Blob會等待所有的資料都傳輸完畢後才觸發下載。
3,a標籤的下載無法攜帶token進行鑑權(但可以透過cookie鑑權),Blob可以進行鑑權

axios中的onDownloadProgress函式的出場

在做檔案下載的時候
如果檔案較大產品希望可以顯示一個進度條
那麼axios支援嗎?
透過檢視文件,它有一個onDownloadProgress函式
用於獲取請求的進度結果
但是需要後端配合,在響應頭中設定Content-Length屬性,
然後再onDownloadProgress函式中兩個非常重要的欄位total和loaded
total表示當前檔案的大小,單位是byte(位元組)
loaded表示當前獲取的檔案進度
然後我們就能計算出當前檔案的下載進度了
如果不設定Content-Length,則total的值是0或者undefined


node提供檔案下載的介面並設定Content-Length

router.get('/downFile', (req, res) => {
  const filename = 'shipin.mp4'
  // 從當前所在的目錄(__dirname)開始,進入到名為 allFIle 的子目錄, 最後定位到名為 filename變數的檔案
  const filePath = path.join(__dirname, 'allFIle', filename); 
  console.log('檔案路徑', filePath)
  // 一個非同步方法,用於獲取檔案或目錄的詳細資訊,包括檔案大小、建立時間、修改時間、許可權等。  
  fs.stat(filePath,  (err, stats) => { 
    // 如果失敗,err 是一個物件。成功的話,err是null
    if (err) {  
      return res.status(404).send('File not found');  
    } 
    const fileSize = stats.size;
    console.log('fileSize', fileSize)
    // 檔案型別
    const mimeType = 'video/mp4';
    res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
    // 設定HTTP響應頭Content-Type,表示響應的內容是影片
    res.setHeader('Content-Type', mimeType);
    // 設定檔案大小,如果要獲取響應進度,這個屬性必須設定
    res.setHeader('Content-Length', fileSize);
    // 建立可讀流並傳遞給響應物件
    const fileStream = fs.createReadStream(filePath); 
    fileStream.pipe(res);    
  });  
});

onDownloadProgress函式的使用

export function fileDownShowProgress(params, callBack) {
  return httpRequest({
    method: 'get',
    url: '/fileList/downFile',
    baseURL: 'http://127.0.0.1:3000',
    isAbort: true, // 這個屬性是我封裝的取消請求,可以忽略
    params: params,
    responseType: 'blob',
    onDownloadProgress: function (progressEvent) {
      console.log('progressEvent引數:', progressEvent)
      // 計算下載進度百分比
      const percentNum = Math.round((progressEvent.loaded * 100) / progressEvent.total);
      callBack(percentNum)
    }
  })
}
<template>
  <div class="down-page">
    <el-button @click="fileDownShowProgressApi">檔案下載-顯示進度</el-button>
    <div class="set-with" v-if="hiddenFlag">
      <el-progress :percentage="percentNum"></el-progress>
    </div>
  </div>
</template>
<script>
import {fileDownShowProgress} from '@/request/api.js'
export default {
  data(){
    return {
      percentNum:0,
      hiddenFlag:false
    }
  },
  methods:{
    fileDownShowProgressApi(){
      fileDownShowProgress({},(percentNum)=>{
        console.log('進度值', percentNum)
        this.percentNum = percentNum
        if(this.percentNum <= 0 || this.percentNum >=100){
          this.hiddenFlag = false
        }else{
          this.hiddenFlag = true
        }
      }).then(res=>{
        console.log('返回來的資料', res)
        this.fileDownLoad(res, '影片檔案', ()=>{
          console.log('下載成功')
        })
      }).catch(err=>{
        // 有可能超時或其他異常情況
        this.hiddenFlag= false
        this.percentNum = 0
        console.log('err:', err)
      })
    },

    fileDownLoad(fileData, fileName, callBack) {
        ...省略上面有這一部分的程式碼...
        callBack() 
      }
    },
  }
}


相關文章