在 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: 檔案類型
預覽上傳的圖片
方法 1
- 使用
FileReader
來解析上傳的File
物件 - 使用
readAsDataURL
方法,轉換成 URL(base64 編碼)的字串 - 使用
onload
方法,在檔案轉換完成後觸發 callback function,結果可用reader.result
取得 - 賦予值給
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)
步驟:
- 創建 URL
- 賦予值給
previewBase64Url
,放入圖片的 src - 執行
revokeObjectURL
- 執行時機:
- 當前已有預覽 URL 時
- 當圖片加載完成後
- 在元件銷毀時
<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%
- 使用
FileReader
的readAsDataURL
方法將檔案轉換為 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 中
參考圖片
上傳大型檔案
- 通常會將檔案分割,分批上傳,提高上傳效率
- 若遇到上傳錯誤,先前上傳過的分割檔案,不需要重複上傳
步驟:
- 切分檔案:調用
slice
方法,將大檔案切分成多個小塊(Blob),每塊固定大小 - 使用第三方套件:
spark-md5
幫上傳檔案產生hash
值,提供給後端辨識 - 逐塊上傳:將
chunk
使用append
方法加入formData
,分批上傳到後端 - 監控進度:對每個檔案塊的上傳進度進行監控,並更新總體上傳進度
<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>
提供檔案哈希值的目的
- 確保數據完整性:
- 哈希值可以用來驗證檔案在傳輸或儲存過程中是否保持不變
- 如果檔案的任何部分被修改,哈希值將會改變
- 實現斷點續傳:
- 當檔案傳輸中斷時,後端可以利用已有的哈希值快速確認已上傳的部分
- 允許在斷點處續傳,而不是重新上傳整個檔案
- 避免重複上傳:
- 通過比較哈希值,可以確認伺服器是否已存在相同的檔案,避免不必要的重複上傳
後端實現斷點續傳的步驟
- 前端開始上傳:
- 前端開始上傳檔案的所有塊,即使是之前已經上傳過的
- 後端接收檔案塊:
- 後端檢查每個接收到的檔案塊
- 後端根據檔案的哈希值識別檔案,並查看該檔案的上傳記錄(例如,已上傳的塊號)
- 判斷和處理檔案塊:
- 如果後端發現某個塊已經上傳(例如,前 50 個塊),它會跳過這些塊
- 當後端接收到第 51 個塊時,它識別出這是一個新的、尚未上傳的塊,並開始處理和儲存這個塊及其後的塊
- 檔案重組:
- 當所有的塊都上傳完成後,後端會按照塊號順序將它們重組成完整檔案
參考資料
# Web # 檔案處理