Learning Objectives
- 用 input type="file" 讓使用者選擇本機檔案,並用 multiple、accept 設定數量與可接受的檔案類型
- 從 FileList 與 File 物件取得檔名、大小與類型,並說明表單上傳檔案需要 enctype="multipart/form-data"
- 用 FileReader 或 URL.createObjectURL 在上傳前預覽圖片
- 用 label 搭配隱藏的檔案欄位,做出自訂樣式的上傳按鈕
選擇本機檔案
交作業、換大頭貼、匯入成績表,都要讓使用者從自己的電腦選檔案。HTML 用 <input type="file">:
<input type="file" id="upload"> <!-- 一次選一個 -->
<input type="file" id="photos" multiple> <!-- 一次可以選好幾個 -->
<input type="file" accept="image/png,image/gif"> <!-- 只接受 png 與 gif -->
<input type="file" accept=".pdf"> <!-- 只接受 PDF -->
<input type="file" accept=".csv,.xls"> <!-- 多種副檔名用逗號分隔 -->
<input type="file" accept="image/*"> <!-- 所有圖片(jpg、png、gif…) -->
accept 可以寫副檔名(.pdf),也可以寫網際網路媒體類型(MIME type,例如 image/png、image/*)。
accept 只是提示:檔案選擇視窗會先篩出符合的檔案,但使用者仍然可以切換成「所有檔案」選別的。
所以 JavaScript 還是要檢查檔案類型;檔案真的送到伺服器時,伺服器也一定要再檢查一次。
這一課的程式只在你的瀏覽器裡讀取檔案,不會上傳到任何伺服器。 你在練習裡選的檔案不會離開你的電腦。
FileList 與 File 物件
使用者選好檔案時,檔案欄位會發生 change 事件。input.files 是一個 FileList(類陣列),裡面每一項是一個 File 物件:
input.addEventListener('change', () => {
console.log(input.files.length); // 選了幾個
const file = input.files[0]; // 第一個檔案;沒選時是 undefined
console.log(file.name, file.size, file.type);
});
| File 的屬性 | 意思 |
|---|---|
name |
檔名(不含資料夾路徑:瀏覽器不讓網頁知道檔案放在你電腦的哪裡) |
size |
檔案大小,單位是位元組(byte) |
type |
MIME 類型,例如 'image/png';瀏覽器判斷不出來時是空字串 |
lastModified |
最後修改時間(毫秒數);投影片的 lastModifiedDate 是舊的寫法 |
FileList 和上一課的 NodeList 一樣不是陣列,可以用 for...of 逐一處理,或用 Array.from(input.files) 轉成陣列。
用表單上傳檔案:傳統的表單送出要上傳檔案時,<form> 必須設定 method="post" 與 enctype="multipart/form-data";
預設的編碼方式(application/x-www-form-urlencoded)只會送出檔名,不會送出檔案內容。
讀出檔案的資訊
執行動畫裡沒有網頁可以選檔案,所以用 new File([內容], 檔名, { type }) 自己做三個 File 物件,它們和 input.files 裡的檔案是同一種物件。注意最後一行:size 和字串的 length 不一樣。
想一想:選擇檔案
<input type="file" accept="image/*"> 的意思是什麼?
使用者選了 D:\作業\report.pdf,input.files[0].name 是什麼?
用傳統的 <form> 送出檔案時,<form> 的 enctype 要設成什麼?
上傳前預覽圖片
使用者選好照片後,先在頁面上顯示出來讓他確認,是很常見的設計。流程是:
- 監聽檔案欄位的
change事件 - 從
input.files[0]取得 File 物件,檢查type是不是圖片 - 讀取檔案,得到一個
<img>可以使用的網址 - 把網址指定給
<img>的src
第 3 步有兩種做法:
// 做法一:FileReader 把檔案讀成 data: 開頭的網址(檔案內容直接寫在網址裡)
const reader = new FileReader();
reader.addEventListener('load', () => {
preview.src = reader.result; // 'data:image/png;base64,iVBORw0…'
});
reader.readAsDataURL(file); // 讀檔是非同步的:讀完才發生 load 事件
// 做法二:URL.createObjectURL 產生一個指向這個檔案的 blob: 網址,不必讀完整個檔案
preview.src = URL.createObjectURL(file);
// 不再需要時(例如換了一張圖),用 URL.revokeObjectURL(網址) 釋放
FileReader 還有 readAsText(file)(讀成文字,例如讀 CSV)。它是非同步的:呼叫 readAs… 之後,
要等 load 事件發生才能讀 reader.result,在那之前 result 是 null。
現在也可以用 await file.text() 直接讀出文字(後面學到 async/await 時會用到)。
自訂上傳按鈕
瀏覽器內建的檔案按鈕(「選擇檔案/未選擇任何檔案」)幾乎改不了樣式,連 cursor: pointer 都不一定有效。投影片介紹了兩種做法:
偽裝法(舊做法):把檔案欄位放進一個做成按鈕樣子的父元素裡,再把檔案欄位的透明度設成 0、蓋在父元素上。 缺點是有些瀏覽器不能調整檔案欄位的大小,父元素太大時會點不到透明的欄位。
label 法(建議):點 <label for="f"> 等於點 id="f" 的欄位。把檔案欄位藏起來,改把 <label> 做成你要的按鈕樣式,
裡面要放文字或圖片都可以:
<input type="file" id="f" class="visually-hidden" accept="image/*">
<label for="f" class="upload-btn">選擇照片</label>
藏起檔案欄位時,不要用 display: none 或 visibility: hidden:那樣鍵盤使用者就沒辦法用 Tab 移到這個欄位、按空白鍵開啟選擇視窗。
改用「視覺上隱藏」(visually hidden)的寫法,把元素縮成 1 像素並裁掉,畫面上看不到、但鍵盤與螢幕閱讀器仍然找得到:
.visually-hidden {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.visually-hidden:focus + .upload-btn { outline: 3px solid #00b297; } /* 鍵盤移到欄位時,在按鈕上顯示外框 */
另一種做法是放一個普通的 <button>,在它的 click 處理器裡呼叫 fileInput.click() 開啟選擇視窗。
想一想:預覽與自訂按鈕
下面的程式會印出什麼?
const r = new FileReader();
r.readAsText(new File(['hi'], 'a.txt'));
console.log(r.result);
關於 URL.createObjectURL(file),下列哪一個說法正確?
用 <label for="f"> 自訂上傳按鈕時,為什麼不建議用 display: none 隱藏檔案欄位?
在這一課的練習裡選了一張照片並顯示預覽,照片會被送到 Uedu 的伺服器嗎?
實作:檢查副檔名
accept 只是提示,程式還要自己檢查。完成 isAccepted(name, accept):accept 是用逗號分隔的副檔名(例如 .png,.jpg),檔名的副檔名(不分大小寫)在清單裡就回傳 true。
重點精華回顧
<input type="file">讓使用者選擇本機檔案;multiple可以一次選多個,accept用副檔名或 MIME 類型(image/*)限制選擇視窗顯示的檔案。accept只是提示,程式與伺服器都要再檢查檔案類型。input.files是 FileList,每一項是 File 物件:name(不含路徑)、size(位元組)、type(MIME 類型)。- 傳統表單上傳檔案要設定
method="post"與enctype="multipart/form-data"。 - 上傳前預覽:
FileReader的readAsDataURL讀成data:網址(非同步,讀完才發生load),或用URL.createObjectURL產生blob:網址,用完以revokeObjectURL釋放。 - 自訂上傳按鈕:用
<label for>做成按鈕,檔案欄位以 visually hidden 的方式隱藏,不要用display: none。 - 選擇與預覽檔案都只在瀏覽器裡進行,檔案不會因此被上傳。