說起文件上傳,在我們的開發(fā)中是繞不過去的話題。但要是碰到幾十 MB 甚至 GB 級別的大文件,傳統(tǒng)上傳方式就有點 “拉胯” 了。今天就給大伙嘮嘮前端大文件上傳,講講它的原理、能解決啥問題、有哪些關鍵功能,再給大家推薦一個超實用的大文件上傳庫,讓大文件上傳不再是難題。
一、大文件上傳是啥?
(一)大文件上傳的定義
簡單來說,大文件上傳就是把個頭很大的文件,從咱們的客戶端,像瀏覽器啥的,傳到服務器上。這些大文件要是用傳統(tǒng)上傳方法,很容易出現(xiàn)上傳失敗、網(wǎng)絡超時,還可能把服務器資源占得死死的。所以,大文件上傳一般得靠分片上傳、斷點續(xù)傳、并行上傳這些技術,才能順順利利完成。
(二)大文件上傳解決的問題
- 網(wǎng)絡不穩(wěn)定:網(wǎng)絡這東西,時好時壞。大文件上傳的時候,網(wǎng)絡要是波動或者斷了,很容易就上傳失敗。大文件上傳技術就能應對這種情況,保證上傳能完成。
- 服務器資源限制:大文件上傳要是處理不好,服務器的內(nèi)存和 CPU 就遭老罪了,搞不好服務器都得崩潰。用對技術,就能減少對服務器資源的占用,讓服務器穩(wěn)穩(wěn)地運行。
- 用戶體驗:有了上傳進度展示、斷點續(xù)傳這些功能,用戶能隨時知道上傳到啥程度了,就算上傳中斷,也不用從頭再來,體驗感直接拉滿。
- 文件完整性驗證:文件傳輸?shù)臅r候,數(shù)據(jù)有可能損壞或者丟失。大文件上傳技術有文件完整性驗證,能保證上傳的文件和原來的一模一樣。
(三)大文件上傳對比普通文件上傳的優(yōu)勢
- 能處理大文件:普通文件上傳碰到 GB 級別的大文件,基本就歇菜了。大文件上傳用分片技術,再大的文件都能搞定。
- 斷點續(xù)傳:普通文件上傳中斷了就得重新開始,大文件上傳能接著沒傳完的地方繼續(xù),省時間又省帶寬。
- 并行上傳:大文件上傳可以把文件分成好多片同時上傳,速度 “蹭蹭” 往上漲。
- 優(yōu)化資源利用:分片上傳后,服務器每次處理的文件片段小,內(nèi)存占用就少,服務器性能也能提升。
- 精準進度監(jiān)控:大文件上傳能更準確地顯示上傳進度,用戶心里有數(shù)。
二、前端實現(xiàn)大文件上傳的關鍵要點
從前端角度看,要實現(xiàn)大文件上傳,得搞定下面這些關鍵功能:
(一)文件分片
把大文件按照固定大小,比如 1MB 或者 5MB,切成一個個小文件,也就是分片。這可以用 File API 里的 File.slice () 方法來實現(xiàn)。
(二)分片上傳
用 XMLHttpRequest 或者 Fetch API,把每個分片一個一個傳到服務器。為了更快,還能同時上傳好幾個分片,也就是并行上傳。
(三)斷點續(xù)傳
上傳要是中斷了,前端得記住哪些分片已經(jīng)傳了。下次接著傳沒傳完的。這得前端和服務器一起配合,服務器一般會記錄已經(jīng)上傳的分片信息,像文件哈希值或者分片索引。
(四)上傳進度監(jiān)控
通過 XMLHttpRequest 的 progress 事件,或者 Fetch API 的 ReadableStream,實時監(jiān)控上傳進度。然后算出已經(jīng)上傳的百分比,展示給用戶。
(五)文件完整性校驗
文件上傳完了,前端算一下文件的哈希值,比如 MD5 或者 SHA-256,再和服務器那邊校驗一下,保證文件沒出問題。這得用 FileReader 讀取文件內(nèi)容來生成哈希值。
(六)錯誤處理與重試機制
上傳的時候難免會遇到網(wǎng)絡錯誤、服務器錯誤啥的。前端得有一套完善的錯誤處理機制,每個分片還要有重試機制,保證上傳失敗的分片能重新傳。
(七)并發(fā)控制
要控制好同時上傳的分片數(shù)量,不然太多請求會占滿帶寬,服務器也扛不住??梢杂藐犃谢蛘?Promise 池來管理并發(fā)上傳任務。
(八)用戶體驗優(yōu)化
得給用戶一個清晰直觀的界面反饋,像進度條、上傳速度、剩余時間這些都得展示出來。另外,還要支持拖拽上傳、文件選擇、批量上傳這些實用功能。
(九)安全性考慮
為了防止有人上傳惡意文件,得校驗文件類型,限制文件大小。要是有敏感數(shù)據(jù),就用 HTTPS 這些加密方式傳輸。
三、寶藏大文件上傳庫 ——enlarge - file - upload
自己從頭寫一個大文件上傳庫,難度可不小,要實現(xiàn)這么多功能,短時間內(nèi)根本搞不定。給大家推薦一個超好用的庫,叫 enlarge - file - upload。上面說的這些功能,它都實現(xiàn)了,不管你用 vue2、vue3、react,還是 jquery 原生 js 開發(fā)項目,都能直接用,真正做到開箱即用。
下面講講它在不同環(huán)境里咋用:
(一)安裝
npm install enlarge-file-upload
安裝很簡單,就按常規(guī)的 npm 安裝方式就行。在項目目錄下打開命令行,輸入npm install enlarge-file-upload
,等安裝完,就能在項目里用這個庫了。具體安裝命令也可以去 npm 官網(wǎng)瞅瞅:www.npmjs.com/package/enl…
(二)參數(shù)介紹
這個庫的參數(shù)挺多,能滿足各種項目需求。詳細的參數(shù)說明可以看官方文檔。這里給大家講講常用的幾個參數(shù):
file
:這個是要上傳的文件對象,必須得有。你選好要上傳的文件,獲取到的文件對象就填這兒。serverUrl
:這是服務器接收文件上傳的接口地址,得填對,不然文件傳不到服務器上。onProgress
:這是個回調(diào)函數(shù),用來監(jiān)聽上傳進度。上傳的時候,它會實時被觸發(fā),通過它能拿到當前的上傳進度百分比,方便展示給用戶看。onSuccess
:文件上傳成功了,就會執(zhí)行這個回調(diào)函數(shù)。你可以在里面給用戶提示上傳成功,或者更新一下頁面狀態(tài)啥的。onError
:要是上傳出錯了,這個回調(diào)函數(shù)就會被調(diào)用,它會帶著錯誤信息,方便你排查問題。
(三)使用案例
- jquery 原生 js 中使用示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>文件上傳</title>
</head>
<script src="https://cdn.jsdelivr.net/npm/enlarge-file-upload/dist/upload.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios"></script>
<body>
<input type="file" id="fileInput" />
<button id="pauseButton">暫停上傳</button>
<button id="resumeButton">繼續(xù)上傳</button>
<div id="progress">上傳進度:0%</div>
<div id="speed">上傳速度:0 MB/s</div>
<script>
async function uploadFunction({ chunk, index, hash, cancelToken }) {
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("hash", hash);
formData.append("index", index);
await axios.post("http://localhost:3000/api/users", formData, {
cancelToken,
});
}
const config = {
chunkSize: 5 * 1024 * 1024,
concurrency: 5,
maxRetries: 3,
uploadFunction,
onProgress: (progress) => {
document.getElementById(
"progress"
).innerText = `上傳進度:${state.progress.toFixed(2)}%`;
},
onSuccess: () => {
console.log("上傳完畢");
},
onSpeed: (speed) => {
document.getElementById("speed").innerText = `上傳速度:${speed}`;
},
};
const { upload, pause, resume, state } = createUploader(config);
const fileInput = document.getElementById("fileInput");
fileInput.addEventListener("change", () => {
const file = fileInput.files[0];
upload(file);
});
document.getElementById("pauseButton").addEventListener("click", () => {
pause();
});
document.getElementById("resumeButton").addEventListener("click", () => {
resume();
});
</script>
</body>
</html>
</html>
在這個例子里,先引入了jquery
庫和enlarge-file-upload
庫。頁面加載完后,給上傳按鈕綁定了點擊事件。點按鈕的時候,就去獲取文件輸入框里選的文件。要是有文件,就創(chuàng)建EnlargeFileUpload
實例,把文件對象、服務器上傳接口地址,還有各種回調(diào)函數(shù)傳進去,最后調(diào)用start
方法開始上傳文件。
- 在 vue3 中使用
<template>
<div>
<input type="file" @change="handleFileChange">
<button @click="uploadFile">上傳文件</button>
</div>
</template>
<script setup>
import EnlargeFileUpload from 'enlarge-file-upload';
const fileRef = ref(null);
const handleFileChange = (e) => {
fileRef.value = e.target.files[0];
};
const uploadFile = () => {
if (fileRef.value) {
const uploader = new EnlargeFileUpload({
file: fileRef.value,
serverUrl: 'http://your-server-url.com/upload',
onProgress: (progress) => {
console.log(`上傳進度: ${progress}%`);
},
onSuccess: () => {
console.log('文件上傳成功');
},
onError: (error) => {
console.error('文件上傳失敗', error);
}
});
uploader.start();
}
};
</script>
在 vue3 項目里,用ref
定義了一個fileRef
來存選的文件。文件選擇框內(nèi)容變了,handleFileChange
函數(shù)就把選的文件賦值給fileRef
。點上傳按鈕的時候,看看fileRef
有沒有值,有值就創(chuàng)建EnlargeFileUpload
實例開始上傳文件,還設置了上傳進度、成功和失敗的回調(diào)函數(shù)。
- 在 vue2 中使用?
<template>
<div>
<input type="file" @change="handleFileChange" />
<button @click="handlePause">暫停上傳</button>
<button @click="handleResume">繼續(xù)上傳</button>
<div>上傳進度:{{ progress.toFixed(2) }}%</div>
<div>上傳速度:{{ speed }}</div>
</div>
</template>
<script>
import Vue from "vue";
import createUploader from "enlarge-file-upload";
import axios from "axios";
export default Vue.extend({
data() {
return {
progress: 0,
speed: "0 MB/s",
uploader: null,
};
},
methods: {
async uploadFunction({ chunk, index, hash, cancelToken }) {
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("hash", hash);
formData.append("index", index.toString());
await axios.post("http://localhost:3000/api/users", formData, {
cancelToken,
});
},
handleFileChange(event) {
const file = event.target.files?.[0];
if (file && this.uploader) {
this.uploader.upload(file);
}
},
handlePause() {
if (this.uploader) {
this.uploader.pause();
}
},
handleResume() {
if (this.uploader) {
this.uploader.resume();
}
},
},
created() {
const uploaderConfig = {
chunkSize: 5 * 1024 * 1024,
concurrency: 5,
maxRetries: 3,
uploadFunction: this.uploadFunction,
onProgress: (progressValue) => {
this.progress = progressValue;
},
onSuccess: () => {
console.log("上傳完畢");
},
onSpeed: (speedValue) => {
this.speed = speedValue;
},
};
this.uploader = createUploader(uploaderConfig);
},
});
</script>
<style scoped></style>
vue2 項目里,在data
里定義了file
變量存文件。handleFileChange
方法獲取選的文件,點上傳按鈕的時候,要是file
有值,就創(chuàng)建EnlargeFileUpload
實例上傳文件,設置好回調(diào)函數(shù)處理上傳過程里的各種情況。
- React 中使用示例
import React, { useRef, useMemo } from'react';
import EnlargeFileUpload from 'enlarge-file-upload';
const FileUploadComponent = () => {
const fileInputRef = useRef(null);
const uploadFile = () => {
const file = fileInputRef.current.files[0];
if (file) {
const uploader = useMemo(() => new EnlargeFileUpload({
file: file,
serverUrl: 'http://your-server-url.com/upload',
onProgress: (progress) => {
console.log(`上傳進度: ${progress}%`);
},
onSuccess: () => {
console.log('文件上傳成功');
},
onError: (error) => {
console.error('文件上傳失敗', error);
}
}), [file]);
uploader.start();
}
};
return (
<div>
<input type="file" ref={fileInputRef} />
<button onClick={uploadFile}>上傳文件</button>
</div>
);
};
export default FileUploadComponent;
React 項目里,用useRef
創(chuàng)建了fileInputRef
來引用文件輸入框。點上傳按鈕的時候,從fileInputRef
里拿選的文件。為了防止組件重新渲染的時候重復創(chuàng)建EnlargeFileUpload
實例,用useMemo
來創(chuàng)建上傳實例,設置好回調(diào)函數(shù)。最后在頁面里渲染文件輸入框和上傳按鈕。
- 封裝為 react Hooks
import React, { useState, useMemo } from "react";
import createUploader from "enlarge-file-upload";
import type { Config, UploadOptions } from "enlarge-file-upload";
import axios from "axios";
const FileUpload = () => {
const [progress, setProgress] = useState(0);
const [speed, setSpeed] = useState("0 MB/s");
async function uploadFunction({
chunk,
index,
hash,
cancelToken,
}: UploadOptions) {
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("hash", hash);
formData.append("index", index);
await axios.post("http://localhost:3000/api/users", formData, {
cancelToken,
});
}
const uploaderConfig: Config = useMemo(
() => ({
chunkSize: 5 * 1024 * 1024,
concurrency: 5,
maxRetries: 3,
uploadFunction,
onProgress: (progress) => {
setProgress(progress);
},
onSuccess: () => {
console.log("上傳完畢");
},
onSpeed: (speed) => {
setSpeed(speed);
},
}),
[]
);
const uploader = useMemo(
() => createUploader(uploaderConfig),
[uploaderConfig]
);
const handleFileChange = (event) => {
const file = event.target.files[0];
uploader?.upload(file);
};
const handlePause = () => {
uploader?.pause();
};
const handleResume = () => {
uploader?.resume();
};
return (
<div>
<input type="file" onChange={handleFileChange} />
<button onClick={handlePause}>暫停上傳</button>
<button onClick={handleResume}>繼續(xù)上傳</button>
<div>上傳進度:{progress.toFixed(2)}%</div>
<div>上傳速度:{speed}</div>
</div>
);
};
export default FileUpload;
這段代碼把文件上傳功能封裝成了 React Hook。Hook 里用useRef
創(chuàng)建文件輸入框的引用,uploadFile
函數(shù)處理文件上傳邏輯,最后返回fileInputRef
和uploadFile
,方便在其他組件里復用文件上傳功能。
- 使用上面封裝好的 Hooks 示例
import { useState, useMemo, useCallback } from "react";
import createUploader from "enlarge-file-upload";
import type { Config, UploadOptions } from "enlarge-file-upload";
import axios from "axios";
const useFileUploader = () => {
const [progress, setProgress] = useState(0);
const [speed, setSpeed] = useState("0 MB/s");
const uploadFunction = useCallback(
async ({ chunk, index, hash, cancelToken }: UploadOptions) => {
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("hash", hash);
formData.append("index", index);
await axios.post("http://localhost:3000/api/users", formData, {
cancelToken,
});
},
[]
);
const uploaderConfig: Config = useMemo(
() => ({
chunkSize: 5 * 1024 * 1024,
concurrency: 5,
maxRetries: 3,
uploadFunction,
onProgress: (progress) => {
setProgress(progress);
},
onSuccess: () => {
console.log("Upload complete");
},
onSpeed: (speed) => {
setSpeed(speed);
},
}),
[uploadFunction]
);
const uploader = useMemo(
() => createUploader(uploaderConfig),
[uploaderConfig]
);
const uploadFile = useCallback(
(file) => {
uploader?.upload(file);
},
[uploader]
);
const pauseUpload = useCallback(() => {
uploader?.pause();
}, [uploader]);
const resumeUpload = useCallback(() => {
uploader?.resume();
}, [uploader]);
return {
progress,
speed,
uploadFile,
pauseUpload,
resumeUpload,
};
};
export default useFileUploader;
這個示例里,引入封裝好的useFileUpload
Hook,通過解構拿到fileInputRef
和uploadFile
。在組件里用fileInputRef
關聯(lián)文件輸入框,點按鈕就調(diào)用uploadFile
函數(shù)上傳文件,在 React 項目里用起來就更方便了。
大文件上傳在前端開發(fā)里很重要,搞懂原理和實現(xiàn)方法,再用好工具,就能給用戶更好的上傳體驗。希望這篇文章能幫大家深入了解前端大文件上傳,在項目開發(fā)里用得得心應手。