2019年1月16日 星期三

[JS] 透過 JavaScript 處理檔案上傳(AJAX Upload byte / JSON / formData File)

HTML Input File

使用 <input type="file" /> 取得使用者想要上傳的檔案:
  • multiple 屬性可以一次上傳多個檔案
  • accept 屬性可以限制上傳檔案的類型
<input type="file" id="file-uploader" data-target="file-uploader" accept="image/*" multiple="multiple"/>

限制可上傳的檔案類型 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"

取得上傳檔案的基本資訊

keywords: fileInput.files
透過 e.target.files 屬性可以取得該檔案的 Blob 物件
const fileUploader = document.querySelector('#file-uploader');

fileUploader.addEventListener('change', (e) => {
  console.log(e.target.files); // get file object
});
❗️ e.target.files 會是一個陣列,裡面可以取得使用者所有想要上傳的檔案,陣列裡都是該檔案的 Blob 物件,而不是一般的物件。
因為這裡只有上傳一個檔案,所以使用 e.target.files[0] 即可取得使用者想要上傳的檔案。這的 File Object 是一個 Blob 物件而不是一般的物件,但從中可以透過 name, size, type 取得該檔案的資訊。
imgur

透過 AJAX 上傳檔案

FormData

keywords: FormData()
透過下面的方式,可以將欲上傳的檔案 append 到 FormData() 上:
let form = new FormData();
form.append("product[photos][]", e.target.files[i])
接著透過 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`, {
    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;

顯示預覽圖

取得欲覽圖的方式可以透過 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 curFile = curFiles[0]; // 透過 input 取得的 file object
const objectURL = URL.createObjectURL(curFile);
console.log('objectURL', objectURL);

常用函式

returnFileSize

function returnFileSize(number) {
  if (number < 1024) {
    return `${number}bytes`;
  } if (number > 1024 && number < 1048576) {
    return `${(number / 1024).toFixed(1)}KB`;
  } if (number > 1048576) {
    return `${(number / 1048576).toFixed(1)}MB`;
  }
}

validFileType

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

表單清空

e.target.value = '';

參考資料

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

Drag and Drop

API

相關閱讀

圖片來源:

  • jQuery File Upload Scripts