使用 Web 應用程式中的檔案
注意:此功能在 Web Workers 中可用。
使用 File API,Web 內容可以要求使用者選擇本地檔案,然後讀取這些檔案的內容。此選擇可以透過使用 HTML <input type="file"> 元素或透過拖放來完成。
訪問選定的檔案
考慮此 HTML
<input type="file" id="input" multiple />
File API 使得訪問包含表示使用者選擇檔案的 File 物件的 FileList 成為可能。
input 元素的 multiple 屬性允許使用者選擇多個檔案。
使用經典 DOM 選擇器訪問第一個選定的檔案
const selectedFile = document.getElementById("input").files[0];
在 change 事件上訪問選定的檔案
也可以(但不是強制性的)透過 change 事件訪問 FileList。你需要使用 EventTarget.addEventListener() 新增 change 事件監聽器,如下所示
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 屬性的值來確定使用者選擇了多少個檔案
const numFiles = fileList.length;
可以透過將列表作為陣列訪問來檢索單個 File 物件。
File 物件提供了三個屬性,其中包含有關檔案的有用資訊。
示例:顯示檔案大小
以下示例展示了 size 屬性的一種可能用法
<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>
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
<input type="file" id="fileElem" multiple accept="image/*" />
<button id="fileSelect" type="button">Select some files</button>
#fileElem {
display: none;
}
處理 click 事件的程式碼可能如下所示
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
<input
type="file"
id="fileElem"
multiple
accept="image/*"
class="visually-hidden" />
<label for="fileElem">Select some files</label>
以及此 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 應用程式中。
第一步是建立一個拖放區。你的內容的哪個部分將接受拖放可能會因應用程式的設計而異,但讓元素接收拖放事件很簡單
let dropbox;
dropbox = document.getElementById("dropbox");
dropbox.addEventListener("dragenter", dragenter);
dropbox.addEventListener("dragover", dragover);
dropbox.addEventListener("drop", drop);
在此示例中,我們將 ID 為 dropbox 的元素轉換為我們的拖放區。這是透過新增 dragenter、dragover 和 drop 事件的監聽器來完成的。
在我們的案例中,我們實際上不需要對 dragenter 和 dragover 事件做任何事情,因此這些函式都很簡單。它們只是停止事件傳播並阻止預設操作發生
function dragenter(e) {
e.stopPropagation();
e.preventDefault();
}
function dragover(e) {
e.stopPropagation();
e.preventDefault();
}
真正的魔法發生在 drop() 函式中
function drop(e) {
e.stopPropagation();
e.preventDefault();
const dt = e.dataTransfer;
const files = dt.files;
handleFiles(files);
}
在這裡,我們從事件中檢索 dataTransfer 欄位,從中拉出檔案列表,然後將其傳遞給 handleFiles()。從這一點開始,無論使用者是使用 input 元素還是拖放,檔案處理都是相同的。
示例:顯示使用者選擇影像的縮圖
假設你正在開發下一個偉大的照片共享網站,並希望在使用者實際上傳影像之前使用 HTML 顯示影像的縮圖預覽。你可以像前面討論的那樣建立輸入元素或拖放區,並讓它們呼叫一個函式,例如下面的 handleFiles() 函式。
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
const objectURL = window.URL.createObjectURL(fileObj);
物件 URL 是一個標識 File 物件的字串。每次你呼叫 URL.createObjectURL() 時,即使你已經為該檔案建立了一個物件 URL,也會建立一個唯一的物件 URL。這些都必須釋放。雖然它們在文件解除安裝時會自動釋放,但如果你的頁面動態使用它們,你應該透過呼叫 URL.revokeObjectURL() 顯式釋放它們
URL.revokeObjectURL(objectURL);
示例:使用物件 URL 顯示影像
此示例使用物件 URL 顯示影像縮圖。此外,它還顯示其他檔案資訊,包括它們的名稱和大小。
呈現介面的 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>
#fileElem {
display: none;
}
這建立了我們的檔案 <input> 元素以及一個呼叫檔案選擇器的連結(因為我們保持檔案輸入隱藏以防止顯示不那麼吸引人的使用者介面)。這在使用 click() 方法使用隱藏檔案輸入元素一節中解釋,呼叫檔案選擇器的方法也是如此。
handleFiles() 方法如下
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 設定為顯示“未選擇檔案!”。否則,我們開始構建檔案列表,如下所示
- 建立一個新的無序列表 (
<ul>) 元素。 - 透過呼叫其
Node.appendChild()方法,將新的列表元素插入到<div>塊中。 - 對於
files表示的FileList中的每個File- 建立一個新的列表項 (
<li>) 元素並將其插入到列表中。 - 建立一個新的影像 (
<img>) 元素。 - 使用
URL.createObjectURL()建立 blob URL,將影像的源設定為表示檔案的新物件 URL。 - 將影像的高度設定為 60 畫素。
- 將新的列表項附加到列表中。
- 建立一個新的列表項 (
這是上述程式碼的即時演示
請注意,我們不會在影像載入後立即撤銷物件 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() 選擇使用者選擇的所有要上傳的影像,如下所示
function sendFiles() {
const imgs = document.querySelectorAll(".obj");
for (const img of imgs) {
new FileUpload(img, img.file);
}
}
document.querySelectorAll 獲取文件中所有具有 CSS 類 obj 的元素的 NodeList。在我們的例子中,這些將是所有影像縮圖。一旦我們有了這個列表,遍歷它併為每個影像建立一個新的 FileUpload 例項就很容易了。每個例項都負責上傳相應的影像。
處理檔案的上傳過程
FileUpload 函式接受兩個輸入:一個影像元素和從中讀取影像資料的檔案。
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 來處理資料上傳。
在實際傳輸資料之前,需要執行幾個準備步驟
XMLHttpRequest的上傳progress監聽器設定為使用新的百分比資訊更新進度條,以便隨著上傳的進行,進度條將根據最新資訊進行更新。XMLHttpRequest的上傳load事件處理程式設定為將進度條進度資訊更新為 100%,以確保進度指示器實際達到 100%(以防在過程中出現粒度問題)。然後它會刪除進度條,因為它不再需要。這會導致進度條在上傳完成後消失。- 透過呼叫
XMLHttpRequest的open()方法開啟上傳影像檔案的請求,以開始生成 POST 請求。 - 上傳的 MIME 型別透過呼叫
XMLHttpRequest函式overrideMimeType()設定。在這種情況下,我們使用的是通用 MIME 型別;你可能需要或根本不需要設定 MIME 型別,具體取決於你的用例。 FileReader物件用於將檔案轉換為二進位制字串。- 最後,當內容載入後,呼叫
XMLHttpRequest函式send()來上傳檔案的內容。
非同步處理檔案上傳過程
此示例在伺服器端使用 PHP,在客戶端使用 JavaScript,演示了檔案的非同步上傳。
<?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。
<iframe id="viewer"></iframe>
這是 src 屬性的更改
const objURL = URL.createObjectURL(blob);
const iframe = document.getElementById("viewer");
iframe.setAttribute("src", objURL);
// Later:
URL.revokeObjectURL(objURL);
示例:將物件 URL 與其他檔案型別一起使用
你可以以相同的方式操作其他格式的檔案。以下是如何預覽上傳的影片
const video = document.getElementById("video");
const objURL = URL.createObjectURL(blob);
video.src = objURL;
video.play();
// Later:
URL.revokeObjectURL(objURL);