大家應該都碰過處理文件的需求,不管是上傳圖片、讀取電腦的文件或是從網站下載文件... 等,多少都已經和 File API 打過交道了。今天想跟大家分享的是 File API 的核心概念,還有 FileReader、Blob 物件的功用,以及一些實際範例。
File API 可以讓網頁讀寫我們本地端的文件,也能對文件進行操作,且無須伺服器端的介入。Web API 包含以下幾個部分:
我們可以透過 <input type="file"> 處理上傳的文件,並取得 File 的物件屬性。以下是 File 的主要屬性:
name:文件名稱size:文件大小 (單位: Byte)type:文件類型lastModified:最後修改時間上傳文件的按鈕
<input type="file" id="fileInput" />
上傳文件後,使用 event.target.files 得到文件的相關資訊
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', (event) => {
  const file = event.target.files[0];
  console.log(file);
});
這就是我們取得的內容

FileReader 顧名思義,就是用來讀文件的,他可以將文件轉為 Text、ArrayBuffer,或是 Data Url,以下是主要方法:
readAsText():將文件讀取為 TextreadAsArrayBuffer():將文件讀取為 ArrayBufferreadAsDataURL():將文件讀取為 Data URLabort():中止讀取操作我們先建立一個 FileReader
const reader = new FileReader()
再用這個 reader 來讀取文件,並轉成想要的格式。
const fileInput = document.getElementById('fileInput');
const reader = new FileReader();
fileInput.addEventListener('change', (event) => {
  const file = event.target.files[0];
  // 用你想要的讀取方法,來讀取 file
  reader.readAsText(file);
});
reader.onload = (event) => {
  console.log('文件', event.target.result);
};
reader.onerror = (error) => {
  console.error('讀取文件失敗', error);
};
readAsText() 適合讀取與處理文件,例如 CSV、JSON、XML 和 TXT...等等
⬇️ reader.readAsText(file)

可以再將讀取來的資料進一步解析
reader.onload = (event) => {
  console.log('文件', event.target.result);
  const text = event.target.result;
  console.log('文件內容', text);
  // 解析 JSON 文件
  try {
    const data = JSON.parse(text);
    console.log('解析後的數據', data);
  } catch (error) {
    console.error('無法解析 JSON 文件', error);
  }
}

ArrayBuffer 是二進制的數據結構,他有點像是「容器」,用來儲存二進制原始數據,如果有需要,我們可以用一些方法(如 DataView) 來操作這些數據。
⬇️ reader.readAsArrayBuffer(file)

假設我想要偵測使用者上傳的圖片格式為何?這時候就能利用 DavaView 來操作 ArrayBuffer() 取得資料。
先寫一個檢查圖片格式的函式
function checkImageFormat(view) {
  // JPEG 文件的前兩個 byte 應該是 0xFFD8
  const jpegMarker = view.getUint16(0);
  if (jpegMarker === 0xFFD8) {
    return 'JPEG';
  }
  // GIF 文件的前兩個字節應該是 0x4749
  const gifMarker = view.getUint16(0);
  if (gifMarker === 0x4749) {
    return 'GIF';
  }
  // PNG 文件的前八個字節應該是 0x89504E470D0A1A0A
  const pngMarker = view.getUint32(0) === 0x89504E47 && view.getUint32(4) === 0x0D0A1A0A;
  if (pngMarker) {
    return 'PNG';
  }
  return '未知格式';
}
在 reader.onload() 呼叫函式,並顯示回傳結果
reader.onload = (event) => {
  const arrayBuffer = event.target.result;
  // 建立 DataView 實例
  const view = new DataView(arrayBuffer)
  // 使用 checkImageFormat() 取得圖片格式
  const format = checkImageFormat(view);
  console.log('圖片格式:', format);
}
上傳圖片後,就可以在 console 取得圖片的格式了
readAsDataURL() 在處理上傳圖片時很常用到。它可以將圖片轉換成 base64 格式,並直接在網頁上顯示圖片,而不需要讀取圖片的 URL。這樣不需等待圖片上傳或從伺服器讀取,顯示速度更快且更安全。
⬇️ reader.readAsDataUrl(file)

接著是 Web API 的第三個部分:Blob。Blob (Binary Large Object) 代表一個不可變的、原始資料的類似檔案的物件。可以用來存放圖片、影片等多種格式的資料,並且可以用多種方法進行操作。我們通常會用 Blob 處理二進制數據以及建立新的文件。
Blob 有兩個常用的方法:
slice():建立一個新的 Blob 物件stream():返回一個 ReadableStream,用於讀取 Blob 內容以下是使用這兩個方法的程式碼範例
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async function (event) {
  const file = event.target.files[0];
  const reader = new FileReader();
  reader.readAsDataURL(file);
  // 使用 Blob 來處理文件
  const blob = new Blob([file], { type: file.type });
  console.log('blob', blob)
  // 使用 slice() 方法
  const sliceBlob = blob.slice(0, blob.size / 2);
  console.log('sliceBlob', sliceBlob);
  // 使用 stream() 方法
  const stream = blob.stream();
  const streamReader = stream.getReader();
  const { value } = await streamReader.read();
  console.log('Stream chunk:', value);
});
線上範例網址:https://mukiwu.github.io/web-api-demo/file-blob.html
我們可以用 URL.createObjectURL() 方法建立一個特定物件的 URL,用於在網站顯示本機圖片,或提供文件下載。
前面段落分享了使用 Web API 建立 File 或 Blob Object,而 URL.createObjectURL() 的用途,就是能幫這些 File / Blog Object 產生一個臨時的 URL,用於 HTML DOM,例如 <img> 或 <a>,以便在網頁上顯示本地圖片或提供文件下載。這種方式避免了將文件直接上傳到伺服器,可以加快響應速度,也能減輕伺服器負擔。
最常見的一個實作,就是在使用者上傳圖片時,顯示圖片的預覽畫面。
首先準備一個 <input type="file /> 以及一個 <img />的預覽框
<input type="file" id="fileInput" />
<img id="preview" src="" alt="Image preview" style="display:none;" />
上傳圖片後,用 createObjectURL() 產生一個臨時的 url,並且用 <img src="" /> 引入這個 url
const fileInput = document.getElementById('fileInput');
const img = document.getElementById('preview');
fileInput.addEventListener('change', async function (event) {
  const file = event.target.files[0];
  const imgURL = URL.createObjectURL(file);
  // 將 createObjectURL 產生的 URL 放入 <img />
  img.src = imgURL;
  img.style.display = 'block';
});
如此就能在上傳後,直接顯示預覽的圖片,而且不需要經過後端伺服器唷

線上範例網址:https://mukiwu.github.io/web-api-demo/file-objurl.html
相信大家對 File API 的構造與方法有了一定的了解,接下來分享幾個我們比較常用的範例,整合了前面段落提到的各種方法,相信大家接下來再看這些程式碼,會更了解他們的用途。
常見的做法是使用「chunk」概念,將大檔案拆分成一個個小區塊,然後分批上傳到伺服器。這種方法可以加快響應時間,並降低上傳失敗的風險。
我們可以使用 Blob.slice() 幫我們分割檔案,並逐塊上傳
// 設定 chunk 的大小是 1MB,也就是每 1MB 會拆成一個小塊
const CHUNK_SIZE = 1024 * 1024;
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async function (event) {
  const file = event.target.files[0];
  uploadFile(file)
});
// 重點函式
function uploadFile(file) {
  const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
  let currentChunk = 0;
  function uploadNextChunk() {
    const start = currentChunk * CHUNK_SIZE;
    const end = Math.min(file.size, start + CHUNK_SIZE);
    const chunk = file.slice(start, end);
    const formData = new FormData();
    formData.append('file', chunk, file.name);
    formData.append('chunk', currentChunk);
    formData.append('totalChunks', totalChunks);
    // 模擬上傳
    setTimeout(() => {
      console.log(`上傳 chunk ${currentChunk + 1}/${totalChunks}:`, chunk);
      currentChunk++;
      if (currentChunk < totalChunks) {
        uploadNextChunk();
      } else {
        console.log('文件上傳完成');
      }
    }, 500); // 模擬網絡延遲
  }
  uploadNextChunk();
}
可以透過 console.log() 看到上傳的檔案大小與總數量

也能不經由伺服器,直接透過網站讓使用者下載文件,使用 Blob() 以及 createObjectURL()
<button onclick="generateAndDownloadFile()">下載檔案</button>
function generateAndDownloadFile() {
  const content = '這是你的文件內容:HELLO, world.';
  const blob = new Blob([content], { type: 'text/plain' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'download.txt';
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
}
我將範例一和範例二合併在同一份檔案中。
這是線上範例網址:https://mukiwu.github.io/web-api-demo/file-objurl.html
還記得我們在前面的文章有分享過 Drag and Drop API 嗎?我們還能結合這個 API 做到拖放上傳檔案的功能唷!
<style>
  img {
    max-width: 100%;
  }
  .drag-over-yellow {
    background-color: yellow;
  }
  .drop-zone {
    height: 300px;
    width: 300px;
    border: 1px solid black;
    margin: 10px;
    padding: 10px;
  }
</style>
<div id="drop-zone" class="drop-zone">放置區域</div>
監聽 drop 事件,將處理檔案的邏輯寫在裡面
const dropZone = document.getElementById('drop-zone');
dropZone.addEventListener('dragover', (event) => {
  event.preventDefault();
});
dropZone.addEventListener('dragenter', (event) => {
  // 可以多加一層判斷,如果是檔案,拖曳區域才會變色
  if (event.dataTransfer.items && event.dataTransfer.items[0].kind === 'file') {
    event.target.classList.add('drag-over-yellow');
  }
});
dropZone.addEventListener('dragleave', (event) => {
  event.target.classList.remove('drag-over-yellow');
});
dropZone.addEventListener('drop', (event) => {
  event.preventDefault();
  event.target.classList.remove('drag-over-yellow');
  // 取得 file 後,再用 createObjectURL() 顯示預覽圖片
  const file = event.dataTransfer.files[0];
  if (file) {
    const imgURL = URL.createObjectURL(file);
    const img = document.createElement('img');
    img.src = imgURL;
    dropZone.innerHTML = '';
    dropZone.appendChild(img);
  }
});

線上範例網址:https://mukiwu.github.io/web-api-demo/file-drag.html
原本以為 File API 相對簡單,篇幅應該很短,沒想到寫完居然爆字數 XDD。
也希望透過這篇的介紹,能讓不熟悉的朋友,對 File API 有進一步的了解,有任何問題都歡迎留言討論唷。