[WebAPIs] 檔案上傳 Input File, File Upload, and FileList
keywords: file upload
, FileList
, FileAPI
- FileList @ MDN WebAPIs
- File @ MDN
- Input File @ MDN
- File 物件, FileList 物件,FileReader 物件 @ 阮一峰
關聯文章:
- TypedArray, ArrayBuffer, DataView
- Blob 和 FileReader
🔖 File 物件 (透過 <input type="file" />
)是一種特殊形式的 Blob
,並且在任何 Blob 可以使用的脈絡下都可以使用,它繼承了 Blob Class 的所有屬性和方法;和 Blob
不同之處在於,File Object
有實際參照到檔案系統中的檔案。
HTML Input File
使用 <input type="file" />
取得使用者想要上傳的檔案:
multiple
屬性可以一次上傳多個檔案accept
屬性可以限制上傳檔案的類型
<input
type="file"
id="file-uploader"
data-target="file-uploader"
accept="image/*"
multiple="multiple"
/>
⚠️ 基於安全性的理由,瀏覽器並不允許使用程式的方式指定
<input type="file">
的value
,必須要是使用者自行透過瀏覽器對話框選擇的檔案。
限制可上傳的檔案類型 Accept Attribute
accept="image/png" accept=".png" accept="image/png, image/jpeg" accept=".png, .jpg, .jpeg"
accept="image/*"
accept=".doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
取得上傳檔案的基本資訊(FileList)
FileList @ MDN WebAPIs
File 物件 (透過 <input type="file" />
)是一種特殊形式的 Blob
,並且在任何 Blob 可以使用的脈絡下都可以使用,它繼承了 Blob class 的所有屬性和方法;和 Blob
不同之處在於,File Object
有實際參照到檔案系統中的檔案。具體來說,FileReader
、URL.createObjectURL()
、createImageBitmap()
、和XMLHttpRequest.send()
可以同時接受 File 和 Blob 。
在 Web 中仍無法直接建立 File 物件,但可以透過使用者從 <input>
元素中選擇的檔案於 FileList 物件中取得,或使用者透過拖曳的方式得到的 DataTransfer 物件。
當使用者透過 <input type="file" />
的對話框選擇檔案後,透過監聽 change
事件中的 e.target.files
可以取得 FileList 物件,FileList 物件是一種 TypedArray ,裡面含有 File 物件,而 File 物件是一個特殊的 Blob 物件:
const fileUploader = document.querySelector('#file-uploader');
fileUploader.addEventListener('change', (e) => {
e.target.files; // FileList object
e.target.files[0]; // File Object (Special Blob)
});
❗
e.target.files
會是一個 Typed Array,裡面可以取得使用者所有想要上傳的檔案,陣列裡都是該檔案的 File 物件,是一種特殊的 Blob 物件,而不是一般的物件。
因為這裡只有上傳一個檔案,所以使用 e.target.files[0]
即可取得使用者想要上傳的檔案。這的 File Object 是一個 Blob 物件而不是一般的物件,但從中可以透過 name
, size
, type
, lastModifiedDate
取得該檔案的資訊。
File Object
File @ MDN
// <input id="fileItem" type="file">
const file = document.getElementById('fileItem').files[0];
file instanceof File; // true
// new File(bits, name [, options])
// - bits 是二進位元串(ArrayBuffer, ArrayBufferView, Blob, DOMString object),以 UTF-8
// - name 檔案名稱或檔案路徑
// - options { type: 'MIME_TYPE', lastModified: Date.now }
var file = new File(['foo'], 'foo.txt', {
type: 'text/plain',
});
// 可以用的屬性包含
file.name; // 檔案名稱
file.type; // 檔案 MIME Type
file.size; // 檔案大小(單位是位元組 byte)
file.lastModified; // 最後修改時間
瀏覽器也有提供原生的 File API,File 物件用來表示一個檔案,可以用它來讀取檔案訊息,它繼承自 Blob 物件,算是一種特殊的 Blob,所以和 Blob 有關的方法均可以使用在 File 物件上。
FileList Object
// <input id="fileItem" type="file" multiple>
const files = document.getElementById('fileItem').files;
files instanceof FileList; // true
files.length; // 檔案數目
透過 AJAX 上傳檔案
FormData
keywords: FormData()
透過下面的方式,可以將欲上傳的檔案 append 到 FormData()
上:
let form = new FormData();
form.append("product[photos][]", e.target.files[i], optional<'filename'>)
接著透過 fetch API 或其他方式把檔案送到後端:
// fetchAPI
fetch('https://api.endpoint.io', {
method: 'POST',
body: form,
});
// jQuery
$.ajax({
processData: false,
data: form,
});
JSON
另一種方式是透過 JSON 來上傳檔案,步驟如下:
- 取得使用者上傳檔案:在 HTML 中建立
<input type="file" onChange={handleUpload} />
來取得使用者上傳的檔案。 - 得到該檔案的 Blob:在
handleUpload
的e.target.files
中可以取得該檔案的 Blob。 - 轉成 ArrayBuffer:透過
FilerReader()
來轉成 ArrayBuffer 的格式。在reader.onLoad
的時候,可以透過reader.result
來取得 ArrayBuffer。 - 轉成 Uint8Array:接著透過
new Uint8Array()
把這個 ArrayBuffer 轉成陣列,但要特別注意,轉出來的是「類陣列(TypedArray
)」而不是真正的陣列,因此在送出 AJAX 之前需要先轉成真正的陣列。 - 轉成真正的陣列:透過
Array.from()
把剛剛的 Uint8Array 轉成真正的陣列。 - 轉成 JSON 格式:如果直接對 Uint8Array 執行
JSON.stringify()
會得到錯誤的結果,記得要先使用Array.from()
才可以使用JSON.stringify()
❗ 透過
new Uint8Array()
轉換出來的陣列會是一個「類陣列(Typed Array)」,可以透過Array.from()
等方式轉換成真正的陣列。
範例程式碼
JavaScript
See the Pen File Upload with JavaScript by PJCHEN (@PJCHENder) on CodePen.
JSX
// FileUploader.js
import React from 'react';
async function handleUpload(e) {
// STEP 2: 得到該檔案的 Blob, i.e., e.target.files
const arrayBuffer = await getArrayBuffer(e.target.files[0]);
console.log('arrayBuffer', arrayBuffer);
const response = await uploadFile(arrayBuffer);
console.log('response', response);
}
function getArrayBuffer(file) {
return new Promise((resolve, reject) => {
// STEP 3: 轉成 ArrayBuffer, i.e., reader.result
const reader = new FileReader();
reader.addEventListener('load', () => {
resolve(reader.result);
});
reader.readAsArrayBuffer(file);
});
}
function uploadFile(arrayBuffer) {
return fetch(`https://api.foobar.io/v1/icon`, {
headers: {
version: 1,
'content-type': 'application/json',
Authorization: localStorage.getItem('token'),
},
method: 'POST',
// STEP 6:使用 JSON.stringify() 包起來送出
body: JSON.stringify({
appId: 3,
format: 'png',
// STEP 4:轉成 Uint8Array(這是 TypedArray)
// STEP 5:透過 Array.from 轉成真正的陣列
icon: Array.from(new Uint8Array(arrayBuffer)),
}),
})
.then((res) => {
if (!res.ok) {
throw res.statusText;
}
return res.json();
})
.then(({ data }) => console.log('data', data))
.catch((err) => console.log('err', err));
}
const FileUploader = () => {
// STEP 1: 建立上傳表單
return <input type="file" onChange={handleUpload} />;
};
export default FileUploader;
顯示預覽圖(Preview Image)
Example_Using_object_URLs_to_display_images @ Firefox Using files from web application
取得欲覽圖的方式可以透過 fileReader
或 createObjectURL
:
方法一: 使用 fileReader
在 onload
中的 callback,可以透過 e.target.result
取得該檔案。
const curFile = curFiles[0]; // 透過 input 取得的 file object
const reader = new FileReader();
reader.onload = function (e) {
console.log('file:', e.target.result);
};
// 使用 readAsDataURL 將圖片轉成 Base64
reader.readAsDataURL(curFile);
方法二:使用 createObjectURL
const handlePreviewImage = (e) => {
const uploader = document.querySelector('[data-target="image-uploader"]');
if (!uploader) return;
uploader.addEventListener('change', showPreviewImage, false);
};
function showPreviewImage(e) {
if (e.target.files.length === 0) return;
const preview = document.querySelector('[data-target="image-preview"]');
preview.innerHTML = '';
const img = new Image();
img.src = window.URL.createObjectURL(e.target.files[0]); // 取得檔案
// 注意:這裡的 `<img>` 元素不能直接用 `innerHTML` 的方式帶進去。
preview.appendChild(img);
}
export default handlePreviewImage;
❗ 注意:這裡的
<img>
元素不能直接用innerHTML
的方式帶進去。
常用函式
returnFileSize
❗ 注意:在有些裝置上(Mac)檔案大小是除以 1000。Google Cloud Storage 則是用 1024。
// 在有些裝置上(Mac)檔案大小是除以 1000
function returnFileSize(number) {
if (number < 1024) {
return `${number}bytes`;
}
if (number > 1024 && number < 1048576) {
return `${(number / 1024).toFixed(2)}KB`;
}
if (number > 1048576 && number < 1073741824) {
return `${(number / 1048576).toFixed(2)}MB`;
}
if (number > 1073741824) {
return `${(number / 1073741824).toFixed(2)}GB`;
}
}
validFileType
function validFileType(file) {
const acceptFileTypes = ['image/jpeg', 'image/png'];
const isValidFileType = acceptFileTypes.includes(fileObject.type);
return isValidFileType;
}
表單清空
e.target.value = '';
參考資料
說明如何透過如何使用 input file, drag and drop, preview
- Input File @ MDN
- Using files from web applications @ MDN
Drag and Drop
- DataTransfer @ MDN
- HTML Drag and Drop API @ MDN
API
- File Reader @ MDN
相關閱讀
- Ant Design Upload Component @ Ant Design
- [Rails] AJAX 小技巧 @ PJCHENder HackMD
- [Rails] carrierwave uploader @ PJCHENder HackMD