使用 Web 應用程式中的檔案

注意:此功能在 Web Workers 中可用。

使用 File API,Web 內容可以要求使用者選擇本地檔案,然後讀取這些檔案的內容。此選擇可以透過使用 HTML <input type="file"> 元素或透過拖放來完成。

訪問選定的檔案

考慮此 HTML

html
<input type="file" id="input" multiple />

File API 使得訪問包含表示使用者選擇檔案的 File 物件的 FileList 成為可能。

input 元素的 multiple 屬性允許使用者選擇多個檔案。

使用經典 DOM 選擇器訪問第一個選定的檔案

js
const selectedFile = document.getElementById("input").files[0];

在 change 事件上訪問選定的檔案

也可以(但不是強制性的)透過 change 事件訪問 FileList。你需要使用 EventTarget.addEventListener() 新增 change 事件監聽器,如下所示

js
const inputElement = document.getElementById("input");
inputElement.addEventListener("change", handleFiles);
function handleFiles() {
  const fileList = this.files; /* now you can work with the file list */
}

獲取有關選定檔案資訊

DOM 提供的 FileList 物件列出了使用者選擇的所有檔案,每個檔案都指定為 File 物件。你可以透過檢查檔案列表的 length 屬性的值來確定使用者選擇了多少個檔案

js
const numFiles = fileList.length;

可以透過將列表作為陣列訪問來檢索單個 File 物件。

File 物件提供了三個屬性,其中包含有關檔案的有用資訊。

name

檔案的名稱作為只讀字串。這只是檔名,不包含任何路徑資訊。

size

檔案大小(以位元組為單位),作為只讀 64 位整數。

type

檔案的 MIME 型別作為只讀字串,如果無法確定型別,則為 ""

示例:顯示檔案大小

以下示例展示了 size 屬性的一種可能用法

html
<form name="uploadForm">
  <div>
    <input id="uploadInput" type="file" multiple />
    <label for="fileNum">Selected files:</label>
    <output id="fileNum">0</output>;
    <label for="fileSize">Total size:</label>
    <output id="fileSize">0</output>
  </div>
  <div><input type="submit" value="Send file" /></div>
</form>
js
const uploadInput = document.getElementById("uploadInput");
uploadInput.addEventListener("change", () => {
  // Calculate total size
  let numberOfBytes = 0;
  for (const file of uploadInput.files) {
    numberOfBytes += file.size;
  }

  // Approximate to the closest prefixed unit
  const units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
  const exponent = Math.min(
    Math.floor(Math.log(numberOfBytes) / Math.log(1024)),
    units.length - 1,
  );
  const approx = numberOfBytes / 1024 ** exponent;
  const output =
    exponent === 0
      ? `${numberOfBytes} bytes`
      : `${approx.toFixed(3)} ${units[exponent]} (${numberOfBytes} bytes)`;

  document.getElementById("fileNum").textContent = uploadInput.files.length;
  document.getElementById("fileSize").textContent = output;
});

使用 click() 方法使用隱藏檔案輸入元素

你可以隱藏公認的難看的 <input> 檔案元素,並提供自己的介面來開啟檔案選擇器並顯示使用者選擇的檔案。你可以透過將輸入元素樣式設定為 display:none 並呼叫 <input> 元素的 click() 方法來完成此操作。

考慮此 HTML

html
<input type="file" id="fileElem" multiple accept="image/*" />
<button id="fileSelect" type="button">Select some files</button>
css
#fileElem {
  display: none;
}

處理 click 事件的程式碼可能如下所示

js
const fileSelect = document.getElementById("fileSelect");
const fileElem = document.getElementById("fileElem");

fileSelect.addEventListener("click", (e) => {
  if (fileElem) {
    fileElem.click();
  }
});

你可以根據需要設定 <button> 的樣式。

使用 label 元素觸發隱藏檔案輸入元素

為了在不使用 JavaScript(click() 方法)的情況下開啟檔案選擇器,可以使用 <label> 元素。請注意,在這種情況下,輸入元素不能使用 display: none(或 visibility: hidden)隱藏,否則標籤將無法透過鍵盤訪問。請改用視覺隱藏技術

考慮此 HTML

html
<input
  type="file"
  id="fileElem"
  multiple
  accept="image/*"
  class="visually-hidden" />
<label for="fileElem">Select some files</label>

以及此 CSS

css
.visually-hidden {
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

input.visually-hidden:is(:focus, :focus-within) + label {
  outline: thin dotted;
}

無需新增 JavaScript 程式碼來呼叫 fileElem.click()。在這種情況下,你也可以根據需要設定標籤元素的樣式。你需要為隱藏輸入欄位在其標籤上的焦點狀態提供視覺提示,無論是如上所示的輪廓,還是背景色或盒陰影。(截至撰寫本文時,Firefox 不顯示 <input type="file"> 元素的此視覺提示。)

使用拖放選擇檔案

你還可以讓使用者將檔案拖放到你的 Web 應用程式中。

第一步是建立一個拖放區。你的內容的哪個部分將接受拖放可能會因應用程式的設計而異,但讓元素接收拖放事件很簡單

js
let dropbox;

dropbox = document.getElementById("dropbox");
dropbox.addEventListener("dragenter", dragenter);
dropbox.addEventListener("dragover", dragover);
dropbox.addEventListener("drop", drop);

在此示例中,我們將 ID 為 dropbox 的元素轉換為我們的拖放區。這是透過新增 dragenterdragoverdrop 事件的監聽器來完成的。

在我們的案例中,我們實際上不需要對 dragenterdragover 事件做任何事情,因此這些函式都很簡單。它們只是停止事件傳播並阻止預設操作發生

js
function dragenter(e) {
  e.stopPropagation();
  e.preventDefault();
}

function dragover(e) {
  e.stopPropagation();
  e.preventDefault();
}

真正的魔法發生在 drop() 函式中

js
function drop(e) {
  e.stopPropagation();
  e.preventDefault();

  const dt = e.dataTransfer;
  const files = dt.files;

  handleFiles(files);
}

在這裡,我們從事件中檢索 dataTransfer 欄位,從中拉出檔案列表,然後將其傳遞給 handleFiles()。從這一點開始,無論使用者是使用 input 元素還是拖放,檔案處理都是相同的。

示例:顯示使用者選擇影像的縮圖

假設你正在開發下一個偉大的照片共享網站,並希望在使用者實際上傳影像之前使用 HTML 顯示影像的縮圖預覽。你可以像前面討論的那樣建立輸入元素或拖放區,並讓它們呼叫一個函式,例如下面的 handleFiles() 函式。

js
function handleFiles(files) {
  for (const file of files) {
    if (!file.type.startsWith("image/")) {
      continue;
    }

    const img = document.createElement("img");
    img.classList.add("obj");
    img.file = file;
    preview.appendChild(img); // Assuming that "preview" is the div output where the content will be displayed.

    const reader = new FileReader();
    reader.onload = (e) => {
      img.src = e.target.result;
    };
    reader.readAsDataURL(file);
  }
}

在這裡,我們處理使用者選擇的檔案的迴圈會檢視每個檔案的 type 屬性,以檢視其 MIME 型別是否以 image/ 開頭。對於每個影像檔案,我們都會建立一個新的 img 元素。CSS 可用於建立任何漂亮的邊框或陰影並指定影像的大小,因此無需在此處完成。

每個影像都添加了 CSS 類 obj,使其易於在 DOM 樹中查詢。我們還為每個影像添加了一個 file 屬性,指定影像的 File;這將使我們以後能夠獲取影像進行實際上傳。我們使用 Node.appendChild() 將新的縮圖新增到文件的預覽區域。

接下來,我們建立 FileReader 以非同步載入影像並將其附加到 img 元素。建立新的 FileReader 物件後,我們設定其 onload 函式,然後呼叫 readAsDataURL() 在後臺開始讀取操作。當影像檔案的全部內容載入後,它們將轉換為 data: URL,並將其傳遞給 onload 回撥。我們對該例程的實現將 img 元素的 src 屬性設定為載入的影像,從而導致影像出現在使用者螢幕上的縮圖中。

使用物件 URL

DOM URL.createObjectURL()URL.revokeObjectURL() 方法允許你建立簡單的 URL 字串,這些字串可用於引用任何可以使用 DOM File 物件引用的資料,包括使用者計算機上的本地檔案。

當你有想要從 HTML 透過 URL 引用的 File 物件時,你可以像這樣為其建立一個物件 URL

js
const objectURL = window.URL.createObjectURL(fileObj);

物件 URL 是一個標識 File 物件的字串。每次你呼叫 URL.createObjectURL() 時,即使你已經為該檔案建立了一個物件 URL,也會建立一個唯一的物件 URL。這些都必須釋放。雖然它們在文件解除安裝時會自動釋放,但如果你的頁面動態使用它們,你應該透過呼叫 URL.revokeObjectURL() 顯式釋放它們

js
URL.revokeObjectURL(objectURL);

示例:使用物件 URL 顯示影像

此示例使用物件 URL 顯示影像縮圖。此外,它還顯示其他檔案資訊,包括它們的名稱和大小。

呈現介面的 HTML 如下所示

html
<input type="file" id="fileElem" multiple accept="image/*" />
<a href="#" id="fileSelect">Select some files</a>
<div id="fileList">
  <p>No files selected!</p>
</div>
css
#fileElem {
  display: none;
}

這建立了我們的檔案 <input> 元素以及一個呼叫檔案選擇器的連結(因為我們保持檔案輸入隱藏以防止顯示不那麼吸引人的使用者介面)。這在使用 click() 方法使用隱藏檔案輸入元素一節中解釋,呼叫檔案選擇器的方法也是如此。

handleFiles() 方法如下

js
const fileSelect = document.getElementById("fileSelect"),
  fileElem = document.getElementById("fileElem"),
  fileList = document.getElementById("fileList");

fileSelect.addEventListener("click", (e) => {
  if (fileElem) {
    fileElem.click();
  }
  e.preventDefault(); // prevent navigation to "#"
});

fileElem.addEventListener("change", handleFiles);

function handleFiles() {
  fileList.textContent = "";
  if (!this.files.length) {
    const p = document.createElement("p");
    p.textContent = "No files selected!";
    fileList.appendChild(p);
  } else {
    const list = document.createElement("ul");
    fileList.appendChild(list);
    for (const file of this.files) {
      const li = document.createElement("li");
      list.appendChild(li);

      const img = document.createElement("img");
      img.src = URL.createObjectURL(file);
      img.height = 60;
      li.appendChild(img);
      const info = document.createElement("span");
      info.textContent = `${file.name}: ${file.size} bytes`;
      li.appendChild(info);
    }
  }
}

這首先獲取 ID 為 fileList<div> 的 URL。這是我們將插入檔案列表(包括縮圖)的塊。

如果傳遞給 handleFiles()FileList 物件為空,我們則將該塊的內部 HTML 設定為顯示“未選擇檔案!”。否則,我們開始構建檔案列表,如下所示

  1. 建立一個新的無序列表 (<ul>) 元素。
  2. 透過呼叫其 Node.appendChild() 方法,將新的列表元素插入到 <div> 塊中。
  3. 對於 files 表示的 FileList 中的每個 File
    1. 建立一個新的列表項 (<li>) 元素並將其插入到列表中。
    2. 建立一個新的影像 (<img>) 元素。
    3. 使用 URL.createObjectURL() 建立 blob URL,將影像的源設定為表示檔案的新物件 URL。
    4. 將影像的高度設定為 60 畫素。
    5. 將新的列表項附加到列表中。

這是上述程式碼的即時演示

請注意,我們不會在影像載入後立即撤銷物件 URL,因為這樣做會使影像無法進行使用者互動(例如右鍵單擊儲存影像或在新選項卡中開啟它)。對於長期執行的應用程式,當不再需要物件 URL 時(例如當影像從 DOM 中刪除時),應透過呼叫 URL.revokeObjectURL() 方法並傳入物件 URL 字串來顯式撤銷物件 URL 以釋放記憶體。

示例:上傳使用者選擇的檔案

此示例展示瞭如何讓使用者將檔案(例如使用上一個示例選擇的影像)上傳到伺服器。

注意: 通常,使用 Fetch API 而不是 XMLHttpRequest 傳送 HTTP 請求更好。但是,在這種情況下,我們想向用戶顯示上傳進度,而 Fetch API 仍不支援此功能,因此該示例使用 XMLHttpRequest

使用 Fetch API 跟蹤進度通知標準化的工作在 https://github.com/whatwg/fetch/issues/607

建立上傳任務

繼續上一個示例中構建縮圖的程式碼,回想一下每個縮圖影像都在 CSS 類 obj 中,並在 file 屬性中附加了相應的 File。這允許我們使用 Document.querySelectorAll() 選擇使用者選擇的所有要上傳的影像,如下所示

js
function sendFiles() {
  const imgs = document.querySelectorAll(".obj");

  for (const img of imgs) {
    new FileUpload(img, img.file);
  }
}

document.querySelectorAll 獲取文件中所有具有 CSS 類 obj 的元素的 NodeList。在我們的例子中,這些將是所有影像縮圖。一旦我們有了這個列表,遍歷它併為每個影像建立一個新的 FileUpload 例項就很容易了。每個例項都負責上傳相應的影像。

處理檔案的上傳過程

FileUpload 函式接受兩個輸入:一個影像元素和從中讀取影像資料的檔案。

js
function FileUpload(img, file) {
  const reader = new FileReader();
  this.ctrl = createThrobber(img);
  const xhr = new XMLHttpRequest();
  this.xhr = xhr;

  this.xhr.upload.addEventListener("progress", (e) => {
    if (e.lengthComputable) {
      const percentage = Math.round((e.loaded * 100) / e.total);
      this.ctrl.update(percentage);
    }
  });

  xhr.upload.addEventListener("load", (e) => {
    this.ctrl.update(100);
    const canvas = this.ctrl.ctx.canvas;
    canvas.parentNode.removeChild(canvas);
  });
  xhr.open(
    "POST",
    "https://demos.hacks.mozilla.org/paul/demos/resources/webservices/devnull.php",
  );
  xhr.overrideMimeType("text/plain; charset=x-user-defined-binary");
  reader.onload = (evt) => {
    xhr.send(evt.target.result);
  };
  reader.readAsBinaryString(file);
}

function createThrobber(img) {
  const throbberWidth = 64;
  const throbberHeight = 6;
  const throbber = document.createElement("canvas");
  throbber.classList.add("upload-progress");
  throbber.setAttribute("width", throbberWidth);
  throbber.setAttribute("height", throbberHeight);
  img.parentNode.appendChild(throbber);
  throbber.ctx = throbber.getContext("2d");
  throbber.ctx.fillStyle = "orange";
  throbber.update = (percent) => {
    throbber.ctx.fillRect(
      0,
      0,
      (throbberWidth * percent) / 100,
      throbberHeight,
    );
    if (percent === 100) {
      throbber.ctx.fillStyle = "green";
    }
  };
  throbber.update(0);
  return throbber;
}

上面顯示的 FileUpload() 函式建立一個進度條,用於顯示進度資訊,然後建立一個 XMLHttpRequest 來處理資料上傳。

在實際傳輸資料之前,需要執行幾個準備步驟

  1. XMLHttpRequest 的上傳 progress 監聽器設定為使用新的百分比資訊更新進度條,以便隨著上傳的進行,進度條將根據最新資訊進行更新。
  2. XMLHttpRequest 的上傳 load 事件處理程式設定為將進度條進度資訊更新為 100%,以確保進度指示器實際達到 100%(以防在過程中出現粒度問題)。然後它會刪除進度條,因為它不再需要。這會導致進度條在上傳完成後消失。
  3. 透過呼叫 XMLHttpRequestopen() 方法開啟上傳影像檔案的請求,以開始生成 POST 請求。
  4. 上傳的 MIME 型別透過呼叫 XMLHttpRequest 函式 overrideMimeType() 設定。在這種情況下,我們使用的是通用 MIME 型別;你可能需要或根本不需要設定 MIME 型別,具體取決於你的用例。
  5. FileReader 物件用於將檔案轉換為二進位制字串。
  6. 最後,當內容載入後,呼叫 XMLHttpRequest 函式 send() 來上傳檔案的內容。

非同步處理檔案上傳過程

此示例在伺服器端使用 PHP,在客戶端使用 JavaScript,演示了檔案的非同步上傳。

php
<?php
if (isset($_FILES["myFile"])) {
  // Example:
  move_uploaded_file($_FILES["myFile"]["tmp_name"], "uploads/" . $_FILES["myFile"]["name"]);
  exit;
}
?><!doctype html>
<html lang="en-US">
  <head>
    <meta charset="UTF-8" />
    <title>dnd binary upload</title>
  </head>
  <body>
    <div>
      <div
        id="dropzone"
        style="margin:30px; width:500px; height:300px; border:1px dotted grey;">
        Drag & drop your file here
      </div>
    </div>
    <script>
      function sendFile(file) {
        const uri = "/index.php";
        const xhr = new XMLHttpRequest();
        const fd = new FormData();

        xhr.open("POST", uri, true);
        xhr.onreadystatechange = () => {
          if (xhr.readyState === 4 && xhr.status === 200) {
            alert(xhr.responseText); // handle response.
          }
        };
        fd.append("myFile", file);
        // Initiate a multipart/form-data upload
        xhr.send(fd);
      }

      const dropzone = document.getElementById("dropzone");
      dropzone.addEventListener("dragover", (event) => {
        event.stopPropagation();
        event.preventDefault();
      });

      dropzone.addEventListener("drop", (event) => {
        event.preventDefault();

        const filesArray = event.dataTransfer.files;
        for (let i = 0; i < filesArray.length; i++) {
          sendFile(filesArray[i]);
        }
      });
    </script>
  </body>
</html>

示例:使用物件 URL 顯示 PDF

物件 URL 不僅可以用於影像!它們還可以用於顯示嵌入式 PDF 檔案或瀏覽器可以顯示的任何其他資源。

在 Firefox 中,要使 PDF 嵌入在 iframe 中(而不是作為下載檔案建議),必須將首選項 pdfjs.disabled 設定為 false

html
<iframe id="viewer"></iframe>

這是 src 屬性的更改

js
const objURL = URL.createObjectURL(blob);
const iframe = document.getElementById("viewer");
iframe.setAttribute("src", objURL);

// Later:
URL.revokeObjectURL(objURL);

示例:將物件 URL 與其他檔案型別一起使用

你可以以相同的方式操作其他格式的檔案。以下是如何預覽上傳的影片

js
const video = document.getElementById("video");
const objURL = URL.createObjectURL(blob);
video.src = objURL;
video.play();

// Later:
URL.revokeObjectURL(objURL);

另見