在 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 // 1MB
const 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 # 檔案處理