在 Vue 當中上傳檔案的方法

HTML 表單上傳

  • 創建 Form 表單
  • method 設定為post
  • 指定編碼格式:enctype="multipart/form-data"
<script setup>const getFile = (e) => {  console.log(e.target.files[0])}</script><template>  <div>    <form action="/upload" method="post" enctype="multipart/form-data">      選擇檔案:<input type="file" name="file" @change="getFile" />      <input type="submit" value="上傳檔案" />    </form>  </div></template>
  • 因為只有上傳一個檔案,獲取files陣列中的第一筆
  • 上傳圖片後,可以在console.log中看到一個File物件
  • name: 檔案名稱
  • size: 檔案大小
  • type: 檔案類型file-img

預覽上傳的圖片

方法 1

  1. 使用FileReader來解析上傳的File物件
  2. 使用readAsDataURL方法,轉換成 URL(base64 編碼)的字串
  3. 使用onload方法,在檔案轉換完成後觸發 callback function,結果可用reader.result取得
  4. 賦予值給previewBase64Url,放入圖片的 src
<script setup>import { ref } from 'vue'const previewBase64Url = ref('')const getFile = (e) => {  console.log(e.target.files[0])  const file = e.target.files[0]  const reader = new FileReader()  if (file) {    reader.readAsDataURL(file)  }  reader.onload = () => {    console.log(reader.result)    previewBase64Url.value = reader.result  }}</script><template>  <main class="flex flex-col items-center justify-center">    <div class="mb-20">      <form action="/upload" method="post" enctype="multipart/form-data">        選擇檔案:<input type="file" name="file" @change="getFile" />        <input type="submit" value="上傳檔案" class="block cursor-pointer font-bold" />      </form>    </div>    <div>      預覽圖片:      <img :src="previewBase64Url" alt="preview-img" />    </div>  </main></template>

方法 2

  • 使用URL.createObjectURL方法創建一個指向該檔案的 URL
  • 接受一個File物件或是Blob物件
  • 不再需要時,需使用revokeObjectURL釋放記憶體空間

語法:

objectURL = URL.createObjectURL(blob)

步驟:

  1. 創建 URL
  2. 賦予值給previewBase64Url,放入圖片的 src
  3. 執行revokeObjectURL
  • 執行時機:
    1. 當前已有預覽 URL 時
    2. 當圖片加載完成後
    3. 在元件銷毀時
<script setup>import { ref, onBeforeUnmount } from 'vue'const imageUrl = ref('')const previewImage = (event) => {  // 先撤銷之前的 URL (如果存在)  if (imageUrl.value) {    URL.revokeObjectURL(imageUrl.value)    imageUrl.value = ''  }  const file = event.target.files[0]  if (file) {    imageUrl.value = URL.createObjectURL(file)  }}const revokeObjectURL = () => {  if (imageUrl.value) {    URL.revokeObjectURL(imageUrl.value)  }}onBeforeUnmount(() => {  revokeObjectURL()})</script><template>  <main class="flex flex-col items-center justify-center">    <div class="mb-20">      <input type="file" @change="previewImage" />    </div>    <div v-if="imageUrl">      預覽圖片:      <img :src="imageUrl" alt="Image preview" @load="revokeObjectURL" />    </div>  </main></template>

非同步檔案上傳

使用場景:串接 API,把檔案傳遞給後端

方法 1:使用FormData來儲存檔案資料

  • 用於構造一組鍵值對,代表表單元素和它們的值
  • 可以輕鬆的把表單的資料轉換成可以透過 AJAX 傳送到後端的資料形式
  • 適合用在表單內有檔案上傳時的情境

上傳範例:

  • 雖然在 form 表單中沒有指定enctype,但在送出 API 請求時,Content-Type會自動帶入multipart/form-data
<script setup>import { reactive } from 'vue'const formData = reactive({  name: '',  image: null})const handleFileChange = (event) => {  formData.image = event.target.files[0]}const submitForm = () => {  const dataToSend = new FormData()  // 使用append方法,把表單的值加入formdata  dataToSend.append('name', formData.name)  if (formData.image) {    dataToSend.append('image', formData.image)  }  fetch('/submit-form', {    method: 'POST',    body: dataToSend  })    .then((response) => response.json())    .then((data) => console.log(data))    .catch((error) => console.error('Error:', error))}</script><template>  <form @submit.prevent="submitForm">    名稱:<input type="text" name="name" v-model="formData.name" /><br />    圖片:<input type="file" name="image" @change="handleFileChange" /><br />    <button type="submit">提交</button>  </form></template>

方法 2:使用 Base64 傳輸

  • 不推薦用於大型檔案:會使檔案的大小增加約 33%
  • 使用FileReaderreadAsDataURL 方法將檔案轉換為 Base64 編碼的字符串
  • 發出 API 請求的Content-Type使用application/json

上傳範例:

<template>  <div class="container">    <input type="file" @change="handleFileChange" />    <button @click="uploadFile">上傳檔案</button>  </div></template><script setup>import { ref } from 'vue'const file = ref(null)const handleFileChange = (event) => {  file.value = event.target.files[0]}const uploadFile = () => {  if (file.value) {    const reader = new FileReader()    reader.readAsDataURL(file.value)    // FileReader 完成讀取時,onload 事件被觸發,發送API請求    reader.onload = async () => {      try {        const data = await sendFile(reader.result)        console.log(data)      } catch (error) {        console.error('Error:', error)      }    }    reader.onerror = (error) => {      console.error('Error: ', error)    }  }}const sendFile = async (base64String) => {  try {    const response = await fetch('/upload', {      method: 'POST',      headers: { 'Content-Type': 'application/json' },      body: JSON.stringify({ file: base64String })    })    if (!response.ok) {      throw new Error(`HTTP error! status: ${response.status}`)    }    return await response.json()  } catch (error) {    console.error('Error sending file:', error)    throw error  }}</script>

網頁檔案處理常見格式

1. Blob 格式

  • Binary Large Object的縮寫,代表二進位檔案的資料內容
  • 是一個不可變的原始資料
  • <input type="file"> 取得的File物件也是一種Blob物件,繼承了Blob的屬性

特性

  • 可用來下載和上傳大型檔案,例如影片
  • 上傳較大的檔案時,可以做到切割,分批上傳

2. Base64 格式

  • 是一種將二進位資料編碼成 ASCII 字元的編碼方式
  • 可以透過文字的方式來呈現二進位資料
  • 不是一種加密方式

特性

  • Base64 將二進制數據轉換為文本格式,使其能夠被嵌入 JSON、XML 等文本中
  • 在不直接支持二進制數據的系統中傳輸圖片或其他檔案
  • 比原始二進制數據大約 33%
  • 將小型圖片直接嵌入到 CSS 或 HTML 中

參考圖片

上傳大型檔案

  • 通常會將檔案分割,分批上傳,提高上傳效率
  • 若遇到上傳錯誤,先前上傳過的分割檔案,不需要重複上傳

步驟:

  1. 切分檔案:調用slice方法,將大檔案切分成多個小塊(Blob),每塊固定大小
  2. 使用第三方套件:spark-md5幫上傳檔案產生hash值,提供給後端辨識
  3. 逐塊上傳:將chunk使用append方法加入formData,分批上傳到後端
  4. 監控進度:對每個檔案塊的上傳進度進行監控,並更新總體上傳進度
<template>  <div>    <input type="file" @change="handleFileChange" />    <button @click="uploadFile">上傳檔案</button>    <div v-if="uploadProgress >= 0">上傳進度:{{ uploadProgress }}%</div>  </div></template><script setup>import { ref } from 'vue'import SparkMD5 from 'spark-md5'const CHUNK_SIZE = 1024 * 1024 // 1MBconst file = ref(null)const uploadProgress = ref(-1)const handleFileChange = (event) => {  file.value = event.target.files[0]}// 當檔案太大時,執行時間可能會很久const generateHash = (file) => {  return new Promise((resolve, reject) => {    const chunks = Math.ceil(file.size / CHUNK_SIZE)    let currentChunk = 0    const spark = new SparkMD5.ArrayBuffer()    const reader = new FileReader()    reader.onload = (e) => {      spark.append(e.target.result) // 將每個chunk的二進制資料,追加到哈希計算      currentChunk++      if (currentChunk < chunks) {        loadNext()      } else {        resolve(spark.end()) // 完成處理,回傳完整的Hash值      }    }    reader.onerror = () => {      reject('檔案讀取錯誤')    }    const loadNext = () => {      const start = currentChunk * CHUNK_SIZE      const end = start + CHUNK_SIZE >= file.size ? file.size : start + CHUNK_SIZE      const chunk = file.slice(start, end)      reader.readAsArrayBuffer(chunk) // 讀取檔案的下一個chunk    }    loadNext()  })}const uploadChunk = async (chunk, chunkNumber, fileHash) => {  try {    console.log('hash', fileHash)    const formData = new FormData()    // 將當前分割的檔案,加入formData    formData.append(file.value.name, chunk)    // 將當前分割檔案的序號,加入到formData,提供給後端識別順序    formData.append('chunkNumber', chunkNumber)    // 將整個檔案的hash值,提供給後端辨認檔案    formData.append('fileHash', fileHash)    const response = await fetch('/upload-chunk', {      method: 'POST',      body: formData    })    if (!response.ok) {      throw new Error('Chunk upload failed')    }  } catch (error) {    console.error('檔案上傳出現錯誤', error)    throw error  }}const uploadFile = async () => {  if (!file.value) return  // 取得整個檔案的hash值  const fileHash = await generateHash(file.value)  const totalChunks = Math.ceil(file.value.size / CHUNK_SIZE)  let uploadedChunks = 0  let current = 0  while (current < file.value.size) {    const chunk = file.value.slice(current, current + CHUNK_SIZE)    await uploadChunk(chunk, current / CHUNK_SIZE, fileHash)    uploadedChunks++    current += CHUNK_SIZE    uploadProgress.value = Math.round((uploadedChunks / totalChunks) * 100)  }}</script>

提供檔案哈希值的目的

  1. 確保數據完整性:
  • 哈希值可以用來驗證檔案在傳輸或儲存過程中是否保持不變
  • 如果檔案的任何部分被修改,哈希值將會改變
  1. 實現斷點續傳:
  • 當檔案傳輸中斷時,後端可以利用已有的哈希值快速確認已上傳的部分
  • 允許在斷點處續傳,而不是重新上傳整個檔案
  1. 避免重複上傳:
  • 通過比較哈希值,可以確認伺服器是否已存在相同的檔案,避免不必要的重複上傳

後端實現斷點續傳的步驟

  1. 前端開始上傳:
  • 前端開始上傳檔案的所有塊,即使是之前已經上傳過的
  1. 後端接收檔案塊:
  • 後端檢查每個接收到的檔案塊
  • 後端根據檔案的哈希值識別檔案,並查看該檔案的上傳記錄(例如,已上傳的塊號)
  1. 判斷和處理檔案塊:
  • 如果後端發現某個塊已經上傳(例如,前 50 個塊),它會跳過這些塊
  • 當後端接收到第 51 個塊時,它識別出這是一個新的、尚未上傳的塊,並開始處理和儲存這個塊及其後的塊
  1. 檔案重組:
  • 當所有的塊都上傳完成後,後端會按照塊號順序將它們重組成完整檔案

參考資料

# Web # 檔案處理