以 React + Express 為範例探討 Web 應用中的檔案上傳機制
最近在工作遇到了需要處理上傳檔案的需求,借此機會完整地學習 Web 前後端處理檔案上傳的機制。本篇筆記以一個簡單範例輔助學習,該範例提供使用者在畫面中上傳任意圖片格式的單一圖片檔案,並儲存於伺服器端的特定資料夾中。使用者可以透過拖放的方式將檔案上傳至網站上,亦可以用點擊的方式打開資料總管,選擇要上傳的檔案。
備註
本篇筆記專注於探討 React 專案中檔案上傳的各種機制與資料結構。本文不會深入討論元件的樣式問題。本筆記提供的範例程式碼將專注於功能實現。
範例
Example Code: React
- 使用 Vite 在 frontend 資料夾建立一個新的 React 專案
yarn create vite frontend --template react-ts
cd frontend
yarn
- 安裝必要套件
yarn add @mui/material @emotion/react @emotion/styled
yarn add axios react-dropzone
- frontend/src/App.tsx
import React, { useState } from "react";
import { useDropzone } from "react-dropzone";
import axios from "axios";
import LinearProgress from "@mui/material/LinearProgress";
import { Box, Typography, Paper, Button, Snackbar } from "@mui/material";
const App: React.FC = () => {
const [file, setFile] = useState<File | null>(null);
const [fileInfo, setFileInfo] = useState<{
name: string;
size: number;
type: string;
preview: string;
} | null>(null);
const [uploadProgress, setUploadProgress] = useState<number>(0);
const [openSnackbar, setOpenSnackbar] = useState<boolean>(false);
const simulateUploadProgress = () => {
setUploadProgress(0);
const interval = setInterval(() => {
setUploadProgress((oldProgress) => {
if (oldProgress >= 100) {
clearInterval(interval);
return 100;
}
return oldProgress ? oldProgress * 2 : oldProgress + 10; // 每次增加 10%,可根據需求調整
});
}, 100); // 每 100 毫秒更新一次進度
};
const handleFileChange = (acceptedFiles: File[]) => {
console.log(acceptedFiles);
const selectedFile = acceptedFiles[0];
setFile(selectedFile);
setFileInfo({
name: selectedFile.name,
size: selectedFile.size,
type: selectedFile.type,
preview: URL.createObjectURL(selectedFile),
});
simulateUploadProgress(); // 模擬上傳進度
};
const uploadFile = () => {
if (!file) {
alert("請先選擇一個檔案。");
return;
}
const formData = new FormData();
formData.append("file", file);
console.log("file", file);
console.log("formData", formData);
axios
.post("http://localhost:5000/upload", formData)
.then((response) => {
console.log(response.data);
setOpenSnackbar(true); // 顯示上傳成功的通知
})
.catch((error) => {
console.error(error);
});
};
const handleCloseSnackbar = () => {
setOpenSnackbar(false);
};
const { getRootProps, getInputProps } = useDropzone({
// accept: { "image/jpeg": [], "image/png": [] },
accept: { "image/*": [] },
onDrop: handleFileChange,
onDragEnter: () => {
setUploadProgress(0);
},
onFileDialogOpen: () => {
setUploadProgress(0);
},
});
console.log("getRootProps", getRootProps());
console.log("getInputProps", getInputProps());
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
paddingTop: "20px",
}}
>
<Paper elevation={3} sx={{ width: "50%", padding: "20px" }}>
<Box
{...getRootProps()}
sx={{ border: "2px dashed #ccc", p: 3, mb: 3 }}
>
<input {...getInputProps()} />
<Typography sx={{ textAlign: "center" }}>
將檔案拖曳到這裡,或點擊選擇檔案
</Typography>
</Box>
<LinearProgress
variant='determinate'
value={uploadProgress}
sx={{ marginTop: "10px" }}
/>
{uploadProgress === 100 && fileInfo && (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
paddingTop: "20px",
}}
>
<img
src={fileInfo.preview}
alt='Preview'
style={{
maxWidth: "100px",
maxHeight: "100px",
marginRight: "10px",
}}
/>
<Typography sx={{ flexGrow: 1 }}>{fileInfo.name}</Typography>
<Typography sx={{ mx: 1 }}>{fileInfo.type}</Typography>
<Typography sx={{ mx: 1 }}>
{(fileInfo.size / 1024).toFixed(2)} KB
</Typography>
</Box>
)}
<Button
variant='contained'
onClick={uploadFile}
sx={{ width: "100%", marginTop: "10px" }}
>
上傳檔案
</Button>
</Paper>
<Snackbar
open={openSnackbar}
autoHideDuration={6000}
onClose={handleCloseSnackbar}
message='上傳成功!'
/>
</Box>
);
};
export default App;
Example Code: Express
- 在 backend 資料夾建立一個新的 Express 專案
mkdir backend
cd backend
yarn init -y
- 安裝必要套件
yarn add express multer
yarn add @types/node @types/express @types/multer ts-node-dev typescript -D
- 在 backend 目錄中,創建 tsconfig.json:
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"moduleResolution": "node",
"outDir": "./dist",
"esModuleInterop": true,
"strict": true
},
"include": ["src/**/*"]
}
- 打開 backend/package.json 文件,並在其中添加如下的 scripts 部分:
"scripts": {
"start": "ts-node-dev src/index.ts",
"build": "tsc",
"serve": "node dist/index.js"
}