在現代網頁應用程式中,檔案上傳功能幾乎是不可或缺的一部分。無論是上傳個人頭像、分享文件,或是提交表單附件,一個良好的上傳體驗能大幅提升用戶滿意度。本文將帶您一步步實作一個 React 檔案上傳元件,不僅能基本的選擇並上傳檔案,更能即時顯示上傳進度,提供使用者清晰的視覺回饋。
透過這個教學,您將學習如何:
- 在 React 中建立檔案上傳元件
- 使用 useState 管理檔案和上傳狀態
- 運用 Axios 處理檔案上傳和監控上傳進度
- 實作條件式渲染顯示不同上傳階段的 UI
無論您是 React 新手或有經驗的開發者,希望這個實作能夠幫助您理解如何打造具有良好使用者體驗的檔案上傳功能。
讓我們先看一眼完成過後的元件,您可以選擇上傳檔案,並點擊提交之後看到提交進度:
接著,就讓我們開始吧!
專案準備與元件建立
現在,假設在 VSCode 上已經建立好環境,目前只有一個什麼都沒有寫的 App.jsx
。
當我們在 React 要為 App.jsx
建立一個新功能的時候,請保持好習慣,先到 src 資料夾建立 components 資料夾,並在其中新增一個元件。這次,由於我們要實作的是一個簡單的檔案上傳功能,所以將這個檔案名稱命名為 FileUploader.jsx
。在這裡,我們會放入所有與檔案上傳相關的功能。
首先,在這個元件先制定一個空白元件,然後匯出,並記得在 App.jsx
上匯入,並放在其渲染的位置。
接著,我們在 FileUploader.jsx
上撰寫一個上傳檔案的輸入欄位,就可以在網頁上看見這個元件了。
// FileUploader.jsx
export default function FileUploader() {
return (
<div>
<input type="file" />
</div>
)
}
當點擊這個按鈕,可以在電腦上選擇一個檔案上傳。這是這個輸入框的預設行為,除此之外,它沒有其他用處,因為它還未配置邏輯,而這是我們要進行的下一個步驟。
設定檔案狀態管理
所以,我們想做的第一件事,是希望顯示這個檔案的一些細節,進一步說,我們是希望透過狀態 (state) 來取得這個檔案的資料,因此得以在 UI 顯示這個檔案的細節。
同時,在我們透過元件將這個檔案送進後端時,也能透過狀態,來給予使用者回饋。
為此,我們要從 react 中匯入 useState()
方法,並設定這個 file 的狀態:
// FileUploader.jsx
import { useState } from "react";
export default function FileUploader() {
const [file, setFile] = useState(null);
return (
<div>
<input type="file" />
</div>
)
}
接著,我們要從這個輸入框取得這個檔案,將其放到狀態之中。為了做到這一點,我們將創建一個函式 handleFileChange
,來處理這個輸入框的 change 事件。
// FileUploader.jsx
import { useState } from "react";
export default function FileUploader() {
const [file, setFile] = useState(null);
function handleFileChange(e){}
return (
<div>
<input type="file" onChange={handleFileChange} />
</div>
)
}
在這篇文章先只假想簡單的情境,如果 e.target.files
為真值的話,我們將設定第一個檔案給狀態 file,由於 e.target.files
是陣列資料,因此會這樣寫:
function handleFileChange(e){
if (e.target.files){
setFile(e.target.files[0]);
}
}
既然 file 有東西了,我們便可以透過條件渲染,把更多它的內容渲染在畫面上:
// FileUploader.jsx
import { useState } from "react";
export default function FileUploader() {
const [file, setFile] = useState(null);
function handleFileChange(){
if (e.target.files){
setFile(e.target.files[0]);
}
}
return (
<div>
<input type="file" onChange={handleFileChange} />
{ file && (
<div>
<p>名稱:{file.name}</p>
<p>尺寸:{(file.size / 1024).toFixed(2)} KB</p>
<p>種類:{file.type}</p>
</div>
) }
</div>
)
}
讓我整理目前為止發生了什麼事,當從電腦選擇檔案上傳到這個頁面中,將會觸發 handleFileChange
函式,我們會透過 e
事件參數取得這個檔案,並存入狀態 file 之中,最後透過 JSX,將這個檔案的更多細節渲染到畫面上,比如說檔案名稱、大小或是類型。
實作檔案上傳狀態處理
接下來,我們要處理提交檔案的過程,如前所述,我們可以設定提交檔案的不同狀態, 然後同樣的,透過這些狀態,來作為不同畫面的呈現依據,我們先假想有四種狀態,分別處理「閒置」(預設)、「提交中」、「提交成功」以及「提交失敗」:idle
、submitting
、success
、error
——
const [status, setStatus] = useState("idle")
// "idle"、"submitting"、"success" 以及 "error"
有了這些狀態,可以根據不同的狀態,用 JSX 來渲染不同的介面,比如說,當有檔案、且不是 submitting
狀態時,我們希望將提交按鈕渲染出來:
{file && status !== "submitting" && <button>提交</button>}
現在只要上傳檔案到頁面上,便能夠看到提交按鈕被渲染出來。
接著,我們來為這個按鈕建立一個非同步函式 handleFileUpload
,讓它有把檔案提交給後端的功能。
這時,我們將借助於 axios 套件以及測試平台 httpbin.org,前者讓非同步處理變得非常簡單;而後者則讓我們能夠輕鬆模擬後端環境。
如果您還沒安裝 axios 套件,可以在終端機輸入:
npm install axios
然後在 FileUploader.jsx
上匯入:
import axios from 'axios';
安裝完成後,便可以著手撰寫這段非同步函式:
async function handleFileUpload(){
if (!file) return;
// 觸發函式時,設定狀態為 submitting
setStatus("submitting");
// 建立提交檔案
const formData = new FormData();
formData.append("file", file);
try {
await axios.post("https://httpbin.org/post", formData, {
"Content-Type": "multipart/form-data"
}); // 模擬真實後端處理
setStatus("success");
} catch {
setStatus("error");
}
}
然後透過點擊提交按鈕來觸發此函式:
{file && status !== "submitting" && (
<button onClick={handleFileUpload}>提交</button>)}
在這裡,我們僅分別為成功與失敗設定狀態,不做其他處理,然後同樣的,透過 JSX 呈現到畫面上告知使用者提交結果:
{status === "success" && (
<p>提交成功!</p>
)}
{status === "error" && (
<p>提交失敗!</p>
)}
實現上傳進度顯示功能
現在上傳檔案、點擊提交按鈕之後,這個按鈕會消失一段時間,然後呈現出結果,這是如預期的情況,因為我們是設定當有檔案,且狀態不屬於 submitting
時,才渲染這個按鈕,而接下來,為了給予使用者良好的視覺反饋,提交檔案時,我們希望在畫面呈現提交檔案的進度。
為了實現這個功能,我們將新增一個狀態,來追蹤提交檔案的上傳進度:
const [uploadProgress, setUploadProgress] = useState(0);
使用 axios 套件的另一個好處就是,添加監聽器會變得非常容易,讓我們回到這個新增資料的邏輯,可以在第三個參數,把原有的文件類型改寫成 headers 的值,並新增一個 key:onUploadProgress
。
onUploadProgress
是 Axios 預設的名稱,它的值是一個函式。這個函式會傳入一個進度參數,能夠用來追蹤進度。現在,我們就是要靠它來計算當前的檔案上傳進度:
await axios.post("<https://httpbin.org/post>", formData, {
headers: { "Content-Type": "multipart/form-data", },
onUploadProgress: (progressEvent) => {
const progress = progressEvent.total
? Math.round((progressEvent.loaded * 100) / progressEvent.total)
: 0;
setUploadProgress(progress);
},
});
讓我解釋這段程式碼發生了什麼事,這段程式碼發生在剛剛寫的 handleFileUpload
之中,當我們按下提交按鈕,它會將檔案放到 formData
中送到後端,而在過程中 axios 會調用這個函式,作為 onUploadProgress
的值傳給 Axios。
在這個函式中,我們使用三元運算子判斷 progressEvent.total
是否存在,倘若是 0 或不存在,將其設為 0;如果存在,則計算上傳進度的百分比,然後調用 setUploadProgress
來更新狀態,就能實現即時的進度更新。
接著,這個非同步函式完成之後,我們手動將進度條的值設為 100,同理,如果上傳失敗了,則設為 0。
try {
await axios.post(...);
setStatus("success");
setUploadProgress(100);
} catch {
setStatus("error");
setUploadProgress(0);
}
同時,由於我們希望每次調用這個函式時,也能夠重置進度條為 0,讓進度條從 0 開始:
async function handleFileUpload(){
if (!file) return;
setStatus("submitting");
setUploadProgress(0);
const formData = ...
}
最後,讓我們來到 JSX 將這個進度條渲染出來:
{status === "submitting" && (
<div>
<div style={{
width: "100%",
backgroundColor: "gray",
height: "20px"
}}>
<div style={{
width: `${uploadProgress}%`,
backgroundColor: "blue",
height: "20px"
}} />
</div>
<p>已上傳:{uploadProgress}%</p>
</div>
)
}
結語
在本文中,我們從零開始打造了一個具有實用性的 React 檔案上傳元件。透過逐步建構,我們實現了檔案選擇、資訊顯示、狀態管理以及即時進度回饋等功能,這些都是現代網頁應用中不可或缺的使用者體驗要素。
當然,這只是一個基礎實作。在實際應用中,您可能需要更多樣式來美化,以及加入更多功能,如檔案類型驗證、檔案大小限制、多檔案上傳,或是與後端進行更複雜的整合,也許來生有緣能與您分享。希望這篇文章的內容對您有所幫助。
附註:完整程式碼參考
// FileUploader.jsx
import axios from 'axios';
import { useState } from "react";
export default function FileUploader() {
const [file, setFile] = useState(null);
const [status, setStatus] = useState("idle");
const [uploadProgress, setUploadProgress] = useState(0);
function handleFileChange(e){
if (e.target.files){
setFile(e.target.files[0]);
}
}
async function handleFileUpload(){
if (!file) return;
setStatus("submitting");
setUploadProgress(0);
const formData = new FormData();
formData.append("file", file);
try {
await axios.post("https://httpbin.org/post", formData, {
headers: { "Content-Type": "multipart/form-data", },
onUploadProgress: (progressEvent) => { // 進度條邏輯
const progress = progressEvent.total
? Math.round((progressEvent.loaded * 100) / progressEvent.total)
: 0;
setUploadProgress(progress);
},
});
setStatus("success");
setUploadProgress(100);
} catch {
setStatus("error");
setUploadProgress(0);
}
}
return (
<div>
<input type="file" onChange={handleFileChange} />
{/* file 有值時,渲染檔案細節 */}
{file && (
<div>
<p>名稱:{file.name}</p>
<p>尺寸:{(file.size / 1024).toFixed(2)} KB</p>
<p>種類:{file.type}</p>
</div>
)}
{/* 當狀態是提交中,渲染進度條 */}
{status === "submitting" && (
<div>
<div style={{
width: "100%",
backgroundColor: "gray",
height: "20px"
}}>
<div style={{
width: `${uploadProgress}%`,
backgroundColor: "blue",
height: "20px",
}} />
</div>
<p>已上傳:{uploadProgress}%</p>
</div>
)}
{/* 當有檔案且狀態不為提交中時,渲染提交按鈕 */}
{file && status !== "submitting" && (
<button onClick={handleFileUpload}>提交</button>
)}
{/* 提交成功提示 */}
{status === "success" && (
<p>提交成功!</p>
)}
{/* 提交失敗提示 */}
{status === "error" && (
<p>提交失敗!</p>
)}
</div>
);
}