前端-文件上传几种方式及其核心思想
一、文件上传几种方式
- form表单上传
- iframe
- FormData异步上传
1、from 表单上传
首先要知道我们上传文件时需要修改form表单的 enctype='multipart/form-data'
产生问题:
form表单提交之后会刷新页面
form表单上传大文件时,很容易遇见服务器超时
1.1 普通上传
<form action="http:localhost:8080/uploadFile" method="POST" enctype="multipart/form-data">
<input type="file" name="myfile">
<input type="submit">
</form>
1.2异步上传
方案1:base64上传
通过canvas讲图片装成base64,然后在服务端进行解码。
base64会将原本的体积转成4/3的体积,so会增大请求体加,浪费带宽,上传和解析的时间会明显增加。
<input type="file" id='file'>
<canvas id='canvas'></canvas>
<img src="" id='target-img'>
<script>
let canvas = document.getElementById("canvas"),
targetImg = document.getElementById('target-img'),
file = document.getElementById('file'),
context = canvas.getContext('2d')
file.onchange = function() {
let URL = window.URL || window.webkitURL
let dataURL = URL.createObjectURL(this.files[0]) // 创建URL对象
let img = new Image()
img.crossOrigin = "anonymous" // 只有服务器模式打开, 才有效
img.src = dataURL
img.onload = function() {
URL.revokeObjectURL(this.src) // img加载完成后,主动释放URL对象
canvas.width = img.width
canvas.height = img.height
context.drawImage(img, 0, 0, img.width, img.height)
let dataBase64Url = canvas.toDataURL('img/png')
targetImg.src = dataBase64Url
}
}
</script>
方案2:二进制形式
除了进行base64编码,还可以在前端直接读取文件内容后以二进制格式上传
关键api:
参考
-
FileReader:对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。
-
File:对象可以是来自用户在一个
<input>
元素上选择文件后返回的files对象 -
readAsBinaryString: 方法会读取指定的 Blob 或 File 对象,当读取完成的时候,readyState 会变成DONE(已完成),并触发 loadend (en-US) 事件,同时result 属性将包含所读取文件原始二进制格式
-
-
Blob: 前端的一个专门用于支持文件操作的二进制对象
-
ArrayBuffer:前端的一个通用的二进制缓冲区,类似数组,但在API和特性上却有诸多不同
-
Buffer:Node.js提供的一个二进制缓冲区,常用来处理I/O操作
-
Uint8Array:类型数组表示的8位无符号整数数组
二进制上传
文件路径格式转二进制
var reader = new FileReader();//①
reader.readAsBinaryString(file);// 把从input里读取的文件内容,放到fileReader的result字段里
reader.onload = function(){
readBinary(this.result) // 读取result或直接上传
}
// 读取二进制文件
function readBinary(text){
var data = new ArrayBuffer(text.length);//创建一个长度为text.length的二进制缓存区
var ui8a = new Uint8Array(data, 0);
for (var i = 0; i < text.length; i++){
ui8a[i] = (text.charCodeAt(i) & 0xff);
}
console.log(ui8a)
}
二进制下载
在向后端发起请求时,需要在请求头中加上
responseType: 'blob'
这样在返回data中可以得到一个浏览器可以解析的blob数据
const downURL = window.URL.createObjectURL(new Blob([data]));
// data 为获取到的二进制数据
const listNode = document.createElement("a");
// 这里注意 : 非同源a标签的download去命名没有用
listNode.download = '合同公允价错误文件下载.xlsx';
listNode.style.display = "none";
listNode.href = downURL;
2、frame上传
低版本浏览器上,xhr请求不支持formdata上传,只能form表单上传。
form表单上传,出现的问题上文已经提到,会本身进行页面跳转,产生原因为target属性导致
target我们或多或少有些了解,a标签也有改属性:
_self:默认值,在相同的窗口中打开响应页面
_blank:在新窗口打开
_parent:在父窗口打开
_top:在最顶层的窗口打开
实现方案
实现异步上传的感觉,自理我们就要用到framename去置顶名字的iframe中打开,也就是<iframe name='formtarget'></iframe>
,<form target='formtarget'>
,这样一来返回的数据会被iframe接收,就不会出现刷新问题,而返回的内容可以通过iframe文本拿到。
问题:预览图片只有先传给后台,后台再返回一个线上的地址
<iframe id="iframe1" name="formtarget" style="display: none"></iframe>
<form id="fm1" action="/app04/ajax1/" method="POST" target="formtarget" enctype="multipart/form-data">
<input type="file" name="k3"/>
<input type="submit">
</form>
<script>
file.onchange = function() {
let iframe = document.getElementById('iframe1')
iframe.addEventListener("load", function() {
var content = this.contents().
var data = JSON.parse(content)
})
}
</script>
3、FormData异步上传
利用FormData模拟表单数据,通过ajax进行提交,可以更加灵活地发送Ajax请求。可以使用FormData来模拟表单提交。
let files = e.target.files // 获取input的file对象
let formData = new FormData();
formData.append('file', file);
axios.post(url, formData);
二、大文件上传
在同一个请求中,要上传大量的数据,导致整个过程会比较漫长,且失败后需要重头开始上传。
大文件上传我们需要考虑三个方面:
- 切片:拆分上传请求
- 断点续传
- 显示上传进度和暂停上传
1、切片
识别切片来源
保证切片拼接顺序
- 我们一般采用编码的方式进行上传,获取文件对应的二进制内容。
- 计算出内容的总大小,根据文件大小切成对应的分片。
- 上传时标识出当前文件,告诉后端上传到了第几个(可以用时间戳形式)。
- 不加表示的话后端在追加切片时,无法识别切片顺序
- 接口异常的情况下无法正确拼接
实现
根据文件名、文件长度等基本信息进行拼接,为了避免多个用户上传相同的文件,可以再额外拼接用户信息如uid等保证唯一性
根据文件的二进制内容计算文件的hash,这样只要文件内容不一样,则标识也会不一样,缺点在于计算量比较大.
将文件拆分成piece大小的分块,然后每次请求只需要上传这一个部分的分块即可
let file = document.querySelector("[name=file]").files[0];
const LENGTH = 1024 * 1024 * 0.1;
let chunks = sliceFile(file, LENGTH); // 首先拆分切片
chunks.forEach((chunk,index) => {
let fd = new FormData();
fd.append("file", chunk);
// 传递context
fd.append("context", file.name + file.length);
// 传递切片索引值
fd.append("chunk", index + 1);
upload(fd)
})
function sliceFile(file, piece = 1024 * 1024 * 5) {
let totalSize = file.size; // 文件总大小
let start = 0; // 每次上传的开始字节
let end = start + piece; // 每次上传的结尾字节
let chunks = []
while (start < totalSize) {
// 根据长度截取每次需要上传的数据
// File对象继承自Blob对象,因此包含slice方法
let blob = file.slice(start, end);
chunks.push(blob)
start = end;
end = start + piece;
}
return chunks
}
请求
/**
* 文件上传
* @param {} params
*/
export function upload (params) {
const data = new FormData();
data.append('file', params.file);
data.append('type', params.type);
return $axios({
method: 'post',
url: "/api/Files/upload",
data: data,
headers: {
'Content-Type': 'multipart/form-data',
}
})
}
2、断点续传
我们在上传或者下载文件的时候,如果已经进行了一部分,这时候网络故障、页面关闭的情况下,不需要从头开始操作,而是从指定位置继续进行操作,这种处理方式就是所说的“断点续传”
断点:的由来是在下载过程中,将一个下载文件分成了多个部分,同时进行多个部分一起的下载,当某个时间点,任务被暂停了,此时下载暂停的位置就是断点了。
续传:一个任务从暂停到开始时,会从上一次任务暂停处开始(可以每次传输成功后加一个表示为告诉前端传输进度)。
实现思路:
- 保存已上传的切片信息
- 选择未上传的切片进行上传
- 全部上传成功后后端进行文件合并
实现方案:
- 本地存储:我们可以利用localstorage,cookie等方式存储在浏览器内,这种情况下我们不依赖于后端,直接本地读取就行。清理了本地文件,会导致上传记录丢失。
- 其实服务器知道我们已经传输到了哪些切片,那些进度,我们通过接口去传输为上传的切片即可。
3、上传进度和暂停
进度:我们可以利用xhr.upload.onprogress = Function方法做进度的监听
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
var percent = Math.floor( e.loaded / e.total * 100);//进度计算
if(percent == 100){
}else{
}
}
};
暂停:如果该请求已被发出,XMLHttpRequest.abort() 方法将终止该请求,实现上传暂停的效果。
继续:和断点继传类似,先获取传输的列表,然后重新发送未上传的切片。