使用 XMLHttpRequest 追蹤檔案上傳進度

實作後端

建立一個 FastAPI 專案,用來接收檔案。

1
2
mkdir xhr-upload-example-api
cd xhr-upload-example-api

建立虛擬環境。

1
2
python -m venv .venv
source .venv/bin/activate

新增 requirements.txt 檔。

1
2
fastapi[standard]
ruff

新增 ruff.toml 檔。

1
2
3
4
5
line-length = 120
indent-width = 4

[format]
quote-style = "double"

新增 .vscode/settings.json 檔。

1
2
3
4
5
6
7
8
9
10
{
"[python]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
},
"editor.defaultFormatter": "charliermarsh.ruff"
}
}

新增 .gitignore 檔。

1
2
3
__pycache__/
.venv/
files/

新增 main.py 檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import os

from fastapi import FastAPI, File, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse

FILE_DIR = "files"

app = FastAPI()

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

if not os.path.exists(FILE_DIR):
os.makedirs(FILE_DIR)


@app.post("/upload")
async def upload(file: UploadFile = File(...)):
path = f"{FILE_DIR}/{file.filename}"

with open(path, "wb") as f:
f.write(file.file.read())

return JSONResponse(content={"path": path})

啟動伺服器。

1
fastapi dev main.py

實作前端

1
2
mkdir xhr-upload-example-ui
cd xhr-upload-example-ui

新增 index.html 檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="file" />
<button type="button">Upload</button>
<progress value="0" max="100" id="progress-bar"></progress>
<script>
const url = 'http://localhost:8000/upload';

const inputElement = document.querySelector('input');
const buttonElement = document.querySelector('button');
const progressBarElement = document.getElementById('progress-bar');

buttonElement.addEventListener('click', function() {
const [file] = inputElement.files;
const formData = new FormData();
formData.append('file', file);

const xhr = new XMLHttpRequest();
xhr.open('POST', url, true);

xhr.upload.onprogress = function(event) {
if (event.lengthComputable) {
progressBarElement.value = (event.loaded / event.total) * 100;
}
};

xhr.send(formData);
});
</script>
</body>
</html>

啟動伺服器。

1
live-server

程式碼