跳至主要内容

[WebAPIs] 檔案上傳 Input File, File Upload, and FileList

keywords: file upload, FileList, FileAPI

關聯文章:

🔖 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 有實際參照到檔案系統中的檔案。具體來說,FileReaderURL.createObjectURL()createImageBitmap()、和XMLHttpRequest.send() 可以同時接受 FileBlob

在 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 取得該檔案的資訊。

imgur

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 來上傳檔案,步驟如下:

  1. 取得使用者上傳檔案:在 HTML 中建立 <input type="file" onChange={handleUpload} /> 來取得使用者上傳的檔案。
  2. 得到該檔案的 Blob:在 handleUploade.target.files 中可以取得該檔案的 Blob
  3. 轉成 ArrayBuffer:透過 FilerReader() 來轉成 ArrayBuffer 的格式。在 reader.onLoad 的時候,可以透過 reader.result 來取得 ArrayBuffer。
  4. 轉成 Uint8Array:接著透過 new Uint8Array() 把這個 ArrayBuffer 轉成陣列,但要特別注意,轉出來的是「類陣列(TypedArray)」而不是真正的陣列,因此在送出 AJAX 之前需要先轉成真正的陣列。
  5. 轉成真正的陣列:透過 Array.from() 把剛剛的 Uint8Array 轉成真正的陣列。
  6. 轉成 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

取得欲覽圖的方式可以透過 fileReadercreateObjectURL

方法一: 使用 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

Drag and Drop

API

相關閱讀