上传原理
简单点说就是根据 http 的 multipart/form-data 协议,浏览器解析文件并打包成二进制数据流,然后再通过 http 把数据流传到服务端multipart/form-data 结构如下:
请求头
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDCntfiXcSkPhS4PN
消息体 - Form Data
------WebKitFormBoundary4Ze9aQ5NKoUihnkZContent-Disposition: form-data; name="file"; filename="哟哟哟.txt"Content-Type: text/plain------WebKitFormBoundary4Ze9aQ5NKoUihnkZ
原始的文件上传
使用 form 表单上传
<form method="post" action="http://localhost:8100" enctype="multipart/form-data"><input type="file" name="f1" /><input type="file" name="f1" multiple /> // 多文件上传可以设置 multiple 属性<button type="submit" id="btn-0">上传</button></form>
局部刷新上传 - iframe
最原始的上传方式,会导致页面刷新,因此需要借用 iframe 实现表面上不刷新页面上传
局部刷新
页面内放置了一个隐藏的 iframe,指定 form 表单的 target 属性值为 iframe 标签的 name 属性值,这样 form 表单的 submit 行为的跳转就会在 iframe 内完成,整体页面不会刷新
拿接口数据
为 iframe 添加 load 事件,得到 iframe 的页面内容,将结果转换为 JSON 对象,这样就拿到了接口的数据
<iframe id="temp-iframe" name="temp-iframe" src="" style="display:none;"></iframe><form method="post" target="temp-iframe" action="http://localhost:8100" enctype="multipart/form-data"><input type="file" name="f1" id="f1" multiple /><button type="submit" id="btn-0">上传</button></form><script>var iframe = document.getElementById('temp-iframe');iframe.addEventListener('load', function () {var result = iframe.contentWindow.document.body.innerText;//接口数据转换为 JSON 对象var obj = JSON.parse(result);if (obj && obj.fileUrl.length) {alert('上传成功')}console.log(obj);});</script>
无刷新上传
可以自己构造一个 FormData 对象,然后使用 XMLHttpRequest、fetch 发送数据,这里用我们常用的 XMLHttpRequest和axios举个例子
XMLHttpRequest
const fileList = document.getElementById('file').files;const formData = new FormData();// 多文件上传需要遍历添加到 fromdata 对象for(let i =0;i<fileList.length;i++){fd.append('file', fileList[i]);}const xhr = new XMLHttpRequest(); // 创建 XMLHttpRequest 对象xhr.open('POST', 'http://localhost:8100/', true);xhr.send(fd); // 发送时 Content-Type 默认就是: multipart/form-data;xhr.onreadystatechange = function () {if (this.readyState == 4 && this.status == 200) {const obj = JSON.parse(xhr.responseText); // 返回值console.log(obj);}}
Axios
const handleChange = (e) => {// 获取文件const files = e.target.files;const formData = new FormData();formData.append('file', files[0]); // 这里就上传一个文件,多个文件的话可以 append 多个axios({method: 'post',url: '/file/upload',data: formData,header: {'content-type': 'multipart/form-data;charset-UTF-8',}});}const dom = (<inputref={uploadRef}style={{display: 'none'}}onChange={handleChange}type={'file'}accept={'.xlsx,.xls,.csv'}>)
上传进度
主要是通过绑定 onprogress 事件实现,主要是以下两种方式:
const setProgress = (event) => {const {lengthComputable, loaded, total} = event;}// 第一种xhr.upload.onprogress = setProgress// 第二种xhr.upload.addEventListener('progress', setProgress)
其中 progress 的回调函数中,有这几个常用的参数:
event.lengthComputable这是一个状态,表示发送的长度有了变化,可计event.loaded表示发送了多少字节event.total表示文件总大小
其中根据 loaded 和 total 就能计算上传的进度了,具体代码如下:
const fileList = document.getElementById('file').files;const formData = new FormData();// 多文件上传需要遍历添加到 fromdata 对象for(let i =0;i<fileList.length;i++){fd.append('file', fileList[i]);}const xhr = new XMLHttpRequest(); // 创建 XMLHttpRequest 对象xhr.open('POST', 'http://localhost:8100/', true);xhr.onreadystatechange = function () {if (this.readyState == 4 && this.status == 200) {const obj = JSON.parse(xhr.responseText); // 返回值console.log(obj);}}const setProgress = (event) => {if (event.lengthComputable) {const progress = (event.loaded / event.total * 100).toFixed(2);// 更新进度状态 && 进度条样式等等逻辑}}xhr.upload.onprogress = setProgress// 注意 send 一定要写在最下面,否则 onprogress 只会执行最后一次 也就是100%的时候xhr.send(fd);
PS:假如使用的是 axios,有对外暴露一个 onUploadProgress API,具体可自行查看文档
其他上传文件方式
拖拽上传
- 首先定义一个拖放的区域 div
- 再在 div 监听 drop 事件,获取文件信息
e.dataTransfer.files(记得取消默认行为和冒泡,否则浏览器会直接打开文件) -
剪切板上传
一般常在可编辑的 div 中使用,即:开启 contenteditable 的 div
首先需要为 div 绑定 paste 事件,并从 event.clipboardData 或 window.clipboardData 获取文件数据
- 这里拿到的数据,需要再使用 getAsFile 方法转换后,才是文件对象数据
- 最后再实现在光标位置插入文件 ```javascript const div = document.getElementById(‘div’);
// 绑定paste事件 div.addEventListener(‘paste’, function (event) { const data = (event.clipboardData || window.clipboardData);
const items = data.items; const fileList = [];//存储文件数据
if (items && items.length) { for (let i = 0; i < items.length; i++) { fileList.push(items[i].getAsFile()); } } uploadFile(fileList) event.preventDefault(); // 阻止默认行为 });
function uploadFile(fileList) { // 这里省略上传文件逻辑
// 光标位置插入文件,举一个图片的例子 const img = document.createElement(‘img’); img.src = ‘xxx’;
insertElement(div, img) }
// 光标位置插入文件 function insertElement(div, ele) { const node = window.getSelection().anchorNode; // 光标起点位置
// 判断是否有光标 if (node != null) { const range = window.getSelection().getRangeAt(0);// 获取光标起始位置 range.insertNode(ele);// 在光标位置插入该对象 } else { div.append(ele); } }
<a name="jUWDZ"></a>### 大文件-分片上传如果上传的文件很大,比如几百兆,甚至几 G 的话,用前面常规的方法可能就会导致请求超时,导致无法成功上传文件,因此我们可以采用将文件分片的方式,比如每次只上传很小的一部分,比如 2M,而且可以支持多个并发进行分片上传,提升上传的速度原理说明:<br />首先了解一下 `Blob` 对象,它表示了一个不可变的原始数据,也就是二进制数据,它提供了对数据进行截取的方法 `slice`,然而我们的 `File` 对象继承了 `Blob` 对象,因此也支持对文件进行截取具体流程:1. 前端将大文件进行分片,比如每部分 2M,发送到服务端携带一个标志(可以使用文件的 md5),用来标识一个完整的文件2. 服务端将各个分片文件保存3. 前端等所有分片上传完成后,发送给服务端一个合并文件的请求4. 服务端根据文件标识、类型、各分片顺序进行文件合并5. 删除分片文件```javascriptconst chunkSize = 2 * 1024 * 1024; // 分片大小 2Mconst file = document.getElementById('file').files[0]; // 文件对象const md5 = getMd5(file); // 可使用 SparkMD5 获取const chunks = [], // 保存分片数据const chunkCount = 0;const sendChunkCount = 0; // 发送完成的分片数// 文件分片if(file.size > chunkSize){let start=0,end=0;while (true) {end += chunkSize;const blob = file.slice(start, end);start+=chunkSize;if(!blob.size) {// 截取的数据为空 则结束break;}chunks.push(blob);//保存分段数据}} else {chunks.push(file.slice(0));}// 发送分片文件(数量大的话,需要做并发控制,设置最大同时发请求的数量)for(let i = 0;i < chunks.length; i++) {const fd = new FormData();fd.append('token', md5);fd.append('file', chunks[i]);fd.append('index', i);// 发送请求xhrSend(fd, () => {sendChunkCount+=1;if(sendChunkCount=== chunks.length){//上传完成,发送合并请求const fdMerge = new FormData();fdMerge.append('type','merge');fdMerge.append('token',token);fdMerge.append('chunkCount',chunks.length);fdMerge.append('filename',name);xhrSend(formD);}});}// 带回调函数的请求方法function xhrSend(fd, cb) {const xhr = new XMLHttpRequest();xhr.open('POST', 'http://localhost:8100/', true);xhr.onreadystatechange = function () {if (xhr.readyState == 4) {cb && cb();}}xhr.send(fd);}
大文件-断点续传
分片上传解决了大文件上传超时和文件上传速度的问题,但假如上传期间断网或者刷新页面了,就需要重新上传,难受,因此就有了断点续传的需求,可以跳过已经上传的部分,只传未上传的部分
实现方式可以有两种
- 浏览器端自行处理
- 服务端返回,告知从哪开始
浏览器端自行处理思路:文件分片上传中,已经实现在将文件进行分片,现在问题就是,断点续传时,如何检测每个分片是否已经上传完成呢?
- 首先,可以使用
spark-md5来针对每一个分片的文件,生成唯一的hash值 - 把
hash值作为key,将上传成功的分片信息保存在浏览器缓存中 - 重新上传时,和本地分片 hash 值进行对比,如果相同的话则跳过,继续下一个分片的上传 ```javascript // 上传逻辑,支持断点续传 const uploadedInfo = getUploadedFromStorage(); // 获得已上传的分片信息
for(let i = 0; i < chunkList.length; i++) { const hash = chunkList[i].hash; if (uploadedInfo[hash]) { // 判断该分片已完成上传 continue; }
// 。。。。省略上传的逻辑 var fd = new FormData(); //构造FormData对象 xhrSend(fd, (file) => { // 回调函数 // 上传完成一段分片后,缓存信息 setUploadedToStorage(file.hash); }) }
// 获得本地缓存的数据 function getUploadedFromStorage(){ return JSON.parse(localStorage.getItem(‘cacheChunk’) || “{}”); }
// 写入缓存 function setUploadedToStorage(hash) { const obj = getUploadedFromStorage();
obj[hash] = true;
localStorage.setItem(‘cacheChunk’, JSON.stringify(obj));
}
```
参考:
