PJCHENder 未整理筆記

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

2017-10-06

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

@(JavaScript)[javascript, webAPIs]

keywords: file upload, FileList, FileAPI

關聯文章:

  • [TypedArray, ArrayBuffer, DataView](/Users/pjchen/Projects/Notes/source/_posts/JavaScript/[JS] TypedArray, ArrayBuffer 和 DataView.md)
  • [Blob 和 FileReader](/Users/pjchen/Projects/Notes/source/_posts/WebAPIs/[Web] Blob 和 FileReader.md)

🔖 File 物件 (透過 <input type="file" />)是一種特殊形式的 Blob ,並且在任何 Blob 可以使用的脈絡下都可以使用,它繼承了 Blob Class 的所有屬性和方法;和 Blob 不同之處在於,File Object 有實際參照到檔案系統中的檔案

HTML Input File

使用 <input type="file" /> 取得使用者想要上傳的檔案:

  • multiple 屬性可以一次上傳多個檔案
  • accept 屬性可以限制上傳檔案的類型
1
<input type="file" id="file-uploader" data-target="file-uploader" accept="image/*" multiple="multiple"/>

⚠️ 基於安全性的理由,瀏覽器並不允許使用程式的方式指定 <input type="file">value,必須要是使用者自行透過瀏覽器對話框選擇的檔案

限制可上傳的檔案類型 Accept Attribute

1
2
3
4
5
6
7
8
9
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 物件

1
2
3
4
5
6
const fileUploader = document.querySelector('#file-uploader');

fileUploader.addEventListener('change', (e) => {
e.target.files; // FileList object
e.target.files[0]; // File Object (Special Blob)
});

:exclamation: e.target.files 會是一個 Typed Array,裡面可以取得使用者所有想要上傳的檔案,陣列裡都是該檔案的 File 物件,是一種特殊的 Blob 物件,而不是一般的物件。

因為這裡只有上傳一個檔案,所以使用 e.target.files[0] 即可取得使用者想要上傳的檔案。這的 File Object 是一個 Blob 物件而不是一般的物件,但從中可以透過 name, size, type, lastModifiedDate 取得該檔案的資訊。

imgur

File Object

File @ MDN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// <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

1
2
3
4
5
// <input id="fileItem" type="file" multiple>
const files = document.getElementById('fileItem').files;
files instanceof FileList // true

files.length // 檔案數目

透過 AJAX 上傳檔案

FormData

keywords: FormData()

透過下面的方式,可以將欲上傳的檔案 append 到 FormData() 上:

1
2
let form = new FormData();
form.append("product[photos][]", e.target.files[i], optional<'filename'>)

接著透過 fetch API 或其他方式把檔案送到後端:

1
2
3
4
5
6
7
8
9
10
11
// 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()

:exclamation: 透過 new Uint8Array() 轉換出來的陣列會是一個「類陣列(Typed Array)」,可以透過 Array.from() 等方式轉換成真正的陣列。

範例程式碼

JavaScript

See the Pen File Upload with JavaScript by PJCHEN (@PJCHENder) on CodePen.

#### JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 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 取得該檔案。

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;

:exclamation: 注意:這裡的 <img> 元素不能直接用 innerHTML 的方式帶進去。

常用函式

returnFileSize

:exclamation: 注意:在有些裝置上(Mac)檔案大小是除以 1000。Google Cloud Storage 則是用 1024。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 在有些裝置上(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

1
2
3
4
5
function validFileType(file) {
const acceptFileTypes = ["image/jpeg", "image/png"];
const isValidFileType = acceptFileTypes.includes(fileObject.type);
return isValidFileType;
}

表單清空

1
e.target.value = '';

參考資料

說明如何透過如何使用 input file, drag and drop, preview

Drag and Drop

API

相關閱讀

掃描二維條碼,分享此文章