普通文件上传
思路:
首先获取用户选择的文件对象,并将其添加到一个
FormData
对象中。然后,使用 axios 的post
方法将FormData
对象发送到服务器。在then
和catch
中,我们分别处理上传成功和失败的情况,并输出相应的信息。需要注意,在使用 axios 进行文件上传时,必须将数据格式设置为
multipart/form-data
,否则文件对象将无法正确传输。
传统方式:
function handleFileSelect(e) {
const formData = new FormData();
formData.append("file", file);
const header={"Content-Type": "multipart/form-data;charset=UTF-8"};
axios.post("http://xxx.xxx.xx.x:xxxx/upload",formData,{
headers: header,
}).then((res) => {
console.log(res);
});
}
封装方法:
在大型项目中,我一般会把get,post,put,delete以及upload方法进行封装。
//封装upload方法
import axios from "axios";
const requests = axios.create({
//配置对象
baseURL: myBaseURL,
timeout: 10000,
});
const header = {
"Content-Type": "multipart/form-data;charset=UTF-8",
};
const http = {
upload(url="",formData){
return new Promise((resolve, reject) => {
requests({
url,
data: formData,
headers: header,
method: "POST",
})
.then((res) => {
resolve(res.data);
return res;
})
.catch((err) => {
reject(err);
});
});
},
....
}
//封装请求方法
apiFun.upload = (formData) => {
return http.upload("/user/headshot", formData);
};
//上传文件
function handleFileSelect(e) {
const formData = new FormData();
formData.append("file", file);
ApiFun.upload(formData).then((res) => {
console.log(res);
ElMessage.success("上传成功");
});
}
}
分片上传
分片上传是将一个大文件切分成多个小块,然后将这些小块逐个上传到服务器的一种上传方式。
思路:
简单来说就是三个步骤:分片=》并行/串行发送分片=》合并请求
为什么要做分片上传?通常大家第一会想到的就是因为文件太大了。
分片上传解决以下几个问题:
1. 大文件上传容易导致网络传输中断:当我们尝试上传一个大文件时,如果网络连接不稳定或上传过程中出现异常,整个文件都需要重新上传。而通过分片上传,即使某个小块上传失败,只需要重新上传该小块,而不必重新上传整个文件。
2. 降低服务器压力:如果直接上传一个大文件,服务器需要同时处理大量的数据,并且需要分配较大的内存空间来存储这些数据。而通过分片上传,可以将服务器的负载分散到多个小块的处理上,减轻了服务器的压力。
3. 提高上传速度和稳定性:将大文件切分成小块后,可以并行上传这些小块,从而提高上传速度。同时,如果某个小块上传失败,可以重试该小块,而不会影响其他小块的上传,从而提高上传的稳定性。
4. 支持断点续传:通过分片上传,服务器可以保存每个小块的上传状态,包括已上传的字节数和已确认的小块。如果上传过程中断,下次可以从上次中断的位置继续上传,实现断点续传的功能。
具体实现思路:
- 将大文件转换成二进制流的格式
- 利用流可以切割的属性,将二进制流切割成多份
- 组装和分割块同等数量的请求块(或同等大小的请求块),并行或串行的形式发出请求
- 待我们监听到所有请求都成功发出去以后,再给服务端发出一个合并的信号
一般我会选择在文件小于5MB时普通文件上传,当文件大于5MB时进行分片上传。
整体逻辑就是:
将大文件进行分片:
我这里封装了md5计算方法
'use strict';
import '../plugins/js-spark-md5.js'
export default function (file, callback) {
var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
file = file,
chunkSize = 4194304, // Read in chunks of 4MB 即 4 * 1024 * 1024
chunks = Math.ceil(file.size / chunkSize),
currentChunk = 0,
spark = new SparkMD5.ArrayBuffer(), //向上取整,因为最后一块不一定满4MB
fileReader = new FileReader();
fileReader.onload = function (e) {
console.log('read chunk nr', currentChunk + 1, 'of', chunks);
spark.append(e.target.result); // Append array buffer
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
let data = {
"etag": spark.end(),
"chunks": chunks,
"size": file.size,
"blockToken": "",
}
callback(null, data);
console.log('finished loading');
}
};
fileReader.onerror = function () {
callback('oops, something went wrong.');
};
function loadNext() {
var start = currentChunk * chunkSize,
end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
loadNext();
};
因为我实现了多文件并行分片上传,所以这里在将文件加入列表时我就已经计算好了相关属性
当md5计算完毕,文件显示准备就绪。
创建切片请求:
返回结果的属性之一exist反应该文件是否上传过,如果存在,则可以实现秒传,无需再次上传。
如果不存在,则获取此次上传的目标ip和端口号
将每一个切片 并行/串行 的方式发出:
我这里使用promise实现了并行(如果是用循环遍历切片串行传输,可以通过循环下标i的值与切片总数相等来判断分片是否上传完毕)
且每个分片是大小一致的。
当所有分片上传完成后。再给服务器端发送合并请求。
文件合并请求:
断点续传
在分片上传的基础上,我们很容易考虑到断点续传的需求。
思路:
点击暂停按钮时停止上传。点击继续上传,继续上传剩下的分片或请求。
我们很容易想到给每个分片一个是否上传的状态标识来识别该分片是否上传完成。
法1:初始时所有分片的状态为未上传,当一个分片上传完成,将该标识设置为已上传。暂停上传时,将当前上传的分片也设置为未上传;重新进行上传时,就可以继续把其他未上传的分片上传了。
法2:将所有分片加入一个列表中,每当成功上传一个分片,就将该分片从列表中删除。而列表中剩下的分片就是还未上传的分片。
秒传
即前面分片上传中提到的。发送创建分片请求时,让服务器检查该文件是否上传过,如果是,则无需重复上传,直接显示上传成功实现秒传功能。
其他问题 之 上传过程中刷新页面怎么办
如果在上传过程中刷新了页面,通常会导致上传任务中断。
因为刷新页面会导致浏览器重新加载页面,之前的 JavaScript 代码和网络请求都会被取消。
当页面刷新后,可以尝试使用以下方法来处理上传任务的中断情况:
1. 利用浏览器的缓存机制:在上传之前,将文件对象保存到浏览器的本地存储或会话存储中。当页面刷新后,通过读取缓存中的文件对象信息,重新构建上传任务,并恢复之前的上传进度。
2. 检测页面刷新事件:可以通过监听 `beforeunload` 事件来捕获页面刷新的操作。在该事件触发时,可以弹出一个确认框,提示用户是否继续离开页面。如果用户选择离开页面,可以先中止当前的上传请求,然后再进行页面刷新。
该方法并不能完全保证上传任务的连续性和完整性。在实际应用中,为了确保上传任务的可靠性,通常建议在上传过程中避免刷新页面,或者提供其他手段来处理上传中断的情况,如支持断点续传功能。
使用 localStorage
实现断点续传的demo:
const input = document.getElementById('file-input');
const FILE_STORAGE_KEY = 'uploadedFile';
// 读取本地存储的文件对象信息
const storedFile = localStorage.getItem(FILE_STORAGE_KEY);
let file = null;
if (storedFile) {
// 如果存在已上传的文件信息,则恢复上传任务
file = JSON.parse(storedFile);
}
input.addEventListener('change', function() {
file = input.files[0];
// 将文件对象保存到本地存储
localStorage.setItem(FILE_STORAGE_KEY, JSON.stringify(file));
startUpload();
});
function startUpload() {
if (!file) {
console.log('请先选择文件');
return;
}
const formData = new FormData();
formData.append('file', file);
axios.post('/upload', formData, {
onUploadProgress: function(progressEvent) {
// 处理上传进度变化
if (progressEvent.lengthComputable) {
const percentComplete = progressEvent.loaded / progressEvent.total * 100;
console.log(percentComplete.toFixed(2) + '% 已上传');
}
}
}).then(function(response) {
// 处理上传完成事件
console.log('上传完成');
console.log(response.data);
// 上传完成后,清除本地存储的文件信息
localStorage.removeItem(FILE_STORAGE_KEY);
}).catch(function(error) {
// 处理上传错误事件
console.error('上传出错');
});
}
// 监听页面刷新事件
window.addEventListener('beforeunload', function(event) {
if (file) {
// 中止当前的上传请求
// ...
// 移除本地存储的文件信息
localStorage.removeItem(FILE_STORAGE_KEY);
}
});
使用 localStorage
存储已选择的文件对象信息,并在页面刷新时恢复该信息。当用户重新选择文件时,更新文件对象并保存到 localStorage
中。在上传过程中,如果用户刷新页面,会触发 beforeunload
事件,我们可以在该事件中中止上传请求并移除 localStorage
中的文件信息。
其他问题 之 某个切片上传失败怎么办
1. 重新上传该切片:如果上传失败的切片是由于网络等原因导致的,则可以尝试重新上传该切片。如果上传失败的切片数量较少,则可以通过手动重试的方式来完成。如果上传失败的切片数量较多,则可能需要设计一些自动化机制来处理重传逻辑,如使用队列等数据结构来记录上传失败的切片并进行重传。
2. 跳过该切片:如果上传失败的切片数量较多,或者由于某些原因无法进行重传,则可以考虑跳过该切片。具体实现方法可以根据上传任务的特点来确定,如将上传任务分为多个阶段,每个阶段上传一定数量的切片,如果某个阶段上传失败,则跳过该阶段并记录下失败的切片信息,待后续再进行重传。
3. 放弃上传任务:如果上传失败的切片数量较多,或者重传操作多次仍然无法恢复上传任务,则可以考虑放弃上传任务。在此情况下,可以将上传任务标记为“失败”状态,并记录下失败的切片信息。如果需要重新上传该文件,则可以在下一次上传任务中,首先检查之前已上传的切片信息,如果存在已上传的切片,则可以直接跳过这些切片并进行后续的上传操作。