说起文件上传,在我们的开发中是绕不过去的话题。但要是碰到几十 MB 甚至 GB 级别的大文件,传统上传方式就有点 “拉胯” 了。今天就给大伙唠唠前端大文件上传,讲讲它的原理、能解决啥问题、有哪些关键功能,再给大家推荐一个超实用的大文件上传库,让大文件上传不再是难题。
一、大文件上传是啥?
(一)大文件上传的定义
简单来说,大文件上传就是把个头很大的文件,从咱们的客户端,像浏览器啥的,传到服务器上。这些大文件要是用传统上传方法,很容易出现上传失败、网络超时,还可能把服务器资源占得死死的。所以,大文件上传一般得靠分片上传、断点续传、并行上传这些技术,才能顺顺利利完成。
(二)大文件上传解决的问题
- 网络不稳定:网络这东西,时好时坏。大文件上传的时候,网络要是波动或者断了,很容易就上传失败。大文件上传技术就能应对这种情况,保证上传能完成。
- 服务器资源限制:大文件上传要是处理不好,服务器的内存和 CPU 就遭老罪了,搞不好服务器都得崩溃。用对技术,就能减少对服务器资源的占用,让服务器稳稳地运行。
- 用户体验:有了上传进度展示、断点续传这些功能,用户能随时知道上传到啥程度了,就算上传中断,也不用从头再来,体验感直接拉满。
- 文件完整性验证:文件传输的时候,数据有可能损坏或者丢失。大文件上传技术有文件完整性验证,能保证上传的文件和原来的一模一样。
(三)大文件上传对比普通文件上传的优势
- 能处理大文件:普通文件上传碰到 GB 级别的大文件,基本就歇菜了。大文件上传用分片技术,再大的文件都能搞定。
- 断点续传:普通文件上传中断了就得重新开始,大文件上传能接着没传完的地方继续,省时间又省带宽。
- 并行上传:大文件上传可以把文件分成好多片同时上传,速度 “蹭蹭” 往上涨。
- 优化资源利用:分片上传后,服务器每次处理的文件片段小,内存占用就少,服务器性能也能提升。
- 精准进度监控:大文件上传能更准确地显示上传进度,用户心里有数。
二、前端实现大文件上传的关键要点
从前端角度看,要实现大文件上传,得搞定下面这些关键功能:
(一)文件分片
把大文件按照固定大小,比如 1MB 或者 5MB,切成一个个小文件,也就是分片。这可以用 File API 里的 File.slice () 方法来实现。
(二)分片上传
用 XMLHttpRequest 或者 Fetch API,把每个分片一个一个传到服务器。为了更快,还能同时上传好几个分片,也就是并行上传。
(三)断点续传
上传要是中断了,前端得记住哪些分片已经传了。下次接着传没传完的。这得前端和服务器一起配合,服务器一般会记录已经上传的分片信息,像文件哈希值或者分片索引。
(四)上传进度监控
通过 XMLHttpRequest 的 progress 事件,或者 Fetch API 的 ReadableStream,实时监控上传进度。然后算出已经上传的百分比,展示给用户。
(五)文件完整性校验
文件上传完了,前端算一下文件的哈希值,比如 MD5 或者 SHA-256,再和服务器那边校验一下,保证文件没出问题。这得用 FileReader 读取文件内容来生成哈希值。
(六)错误处理与重试机制
上传的时候难免会遇到网络错误、服务器错误啥的。前端得有一套完善的错误处理机制,每个分片还要有重试机制,保证上传失败的分片能重新传。
(七)并发控制
要控制好同时上传的分片数量,不然太多请求会占满带宽,服务器也扛不住。可以用队列或者 Promise 池来管理并发上传任务。
(八)用户体验优化
得给用户一个清晰直观的界面反馈,像进度条、上传速度、剩余时间这些都得展示出来。另外,还要支持拖拽上传、文件选择、批量上传这些实用功能。
(九)安全性考虑
为了防止有人上传恶意文件,得校验文件类型,限制文件大小。要是有敏感数据,就用 HTTPS 这些加密方式传输。
三、宝藏大文件上传库 ——enlarge - file - upload
自己从头写一个大文件上传库,难度可不小,要实现这么多功能,短时间内根本搞不定。给大家推荐一个超好用的库,叫 enlarge - file - upload。上面说的这些功能,它都实现了,不管你用 vue2、vue3、react,还是 jquery 原生 js 开发项目,都能直接用,真正做到开箱即用。
下面讲讲它在不同环境里咋用:
(一)安装
npm install enlarge-file-upload
安装很简单,就按常规的 npm 安装方式就行。在项目目录下打开命令行,输入npm install enlarge-file-upload
,等安装完,就能在项目里用这个库了。具体安装命令也可以去 npm 官网瞅瞅:www.npmjs.com/package/enl…
(二)参数介绍
这个库的参数挺多,能满足各种项目需求。详细的参数说明可以看官方文档。这里给大家讲讲常用的几个参数:
file
:这个是要上传的文件对象,必须得有。你选好要上传的文件,获取到的文件对象就填这儿。serverUrl
:这是服务器接收文件上传的接口地址,得填对,不然文件传不到服务器上。onProgress
:这是个回调函数,用来监听上传进度。上传的时候,它会实时被触发,通过它能拿到当前的上传进度百分比,方便展示给用户看。onSuccess
:文件上传成功了,就会执行这个回调函数。你可以在里面给用户提示上传成功,或者更新一下页面状态啥的。onError
:要是上传出错了,这个回调函数就会被调用,它会带着错误信息,方便你排查问题。
(三)使用案例
- 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">继续上传</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
库。页面加载完后,给上传按钮绑定了点击事件。点按钮的时候,就去获取文件输入框里选的文件。要是有文件,就创建EnlargeFileUpload
实例,把文件对象、服务器上传接口地址,还有各种回调函数传进去,最后调用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
来存选的文件。文件选择框内容变了,handleFileChange
函数就把选的文件赋值给fileRef
。点上传按钮的时候,看看fileRef
有没有值,有值就创建EnlargeFileUpload
实例开始上传文件,还设置了上传进度、成功和失败的回调函数。
- 在 vue2 中使用
<template>
<div>
<input type="file" @change="handleFileChange" />
<button @click="handlePause">暂停上传</button>
<button @click="handleResume">继续上传</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
有值,就创建EnlargeFileUpload
实例上传文件,设置好回调函数处理上传过程里的各种情况。
- 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
创建了fileInputRef
来引用文件输入框。点上传按钮的时候,从fileInputRef
里拿选的文件。为了防止组件重新渲染的时候重复创建EnlargeFileUpload
实例,用useMemo
来创建上传实例,设置好回调函数。最后在页面里渲染文件输入框和上传按钮。
- 封装为 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}>继续上传</button>
<div>上传进度:{progress.toFixed(2)}%</div>
<div>上传速度:{speed}</div>
</div>
);
};
export default FileUpload;
这段代码把文件上传功能封装成了 React Hook。Hook 里用useRef
创建文件输入框的引用,uploadFile
函数处理文件上传逻辑,最后返回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
关联文件输入框,点按钮就调用uploadFile
函数上传文件,在 React 项目里用起来就更方便了。
大文件上传在前端开发里很重要,搞懂原理和实现方法,再用好工具,就能给用户更好的上传体验。希望这篇文章能帮大家深入了解前端大文件上传,在项目开发里用得得心应手。