前端-文件上傳幾種方式及其核心思想
一、文件上傳幾種方式
- form表單上傳
- iframe
- FormData異步上傳
1、from 表單上傳
首先要知道我們上傳文件時(shí)需要修改form表單的 enctype='multipart/form-data'
產(chǎn)生問(wèn)題:
form表單提交之后會(huì)刷新頁(yè)面
form表單上傳大文件時(shí),很容易遇見(jiàn)服務(wù)器超時(shí)
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上傳
通過(guò)canvas講圖片裝成base64,然后在服務(wù)端進(jìn)行解碼。
base64會(huì)將原本的體積轉(zhuǎn)成4/3的體積,so會(huì)增大請(qǐng)求體加,浪費(fèi)帶寬,上傳和解析的時(shí)間會(huì)明顯增加。
<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]) // 創(chuàng)建URL對(duì)象
let img = new Image()
img.crossOrigin = "anonymous" // 只有服務(wù)器模式打開(kāi), 才有效
img.src = dataURL
img.onload = function() {
URL.revokeObjectURL(this.src) // img加載完成后,主動(dòng)釋放URL對(duì)象
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:二進(jìn)制形式
除了進(jìn)行base64編碼,還可以在前端直接讀取文件內(nèi)容后以二進(jìn)制格式上傳
關(guān)鍵api:
參考
-
FileReader:對(duì)象允許Web應(yīng)用程序異步讀取存儲(chǔ)在用戶計(jì)算機(jī)上的文件(或原始數(shù)據(jù)緩沖區(qū))的內(nèi)容,使用 File 或 Blob 對(duì)象指定要讀取的文件或數(shù)據(jù)。
-
File:對(duì)象可以是來(lái)自用戶在一個(gè)
<input>元素上選擇文件后返回的files對(duì)象 -
readAsBinaryString: 方法會(huì)讀取指定的 Blob 或 File 對(duì)象,當(dāng)讀取完成的時(shí)候,readyState 會(huì)變成DONE(已完成),并觸發(fā) loadend (en-US) 事件,同時(shí)result 屬性將包含所讀取文件原始二進(jìn)制格式
-
-
Blob: 前端的一個(gè)專門用于支持文件操作的二進(jìn)制對(duì)象
-
ArrayBuffer:前端的一個(gè)通用的二進(jìn)制緩沖區(qū),類似數(shù)組,但在API和特性上卻有諸多不同
-
Buffer:Node.js提供的一個(gè)二進(jìn)制緩沖區(qū),常用來(lái)處理I/O操作
-
Uint8Array:類型數(shù)組表示的8位無(wú)符號(hào)整數(shù)數(shù)組
二進(jìn)制上傳
文件路徑格式轉(zhuǎn)二進(jìn)制
var reader = new FileReader();//①
reader.readAsBinaryString(file);// 把從input里讀取的文件內(nèi)容,放到fileReader的result字段里
reader.onload = function(){
readBinary(this.result) // 讀取result或直接上傳
}
// 讀取二進(jìn)制文件
function readBinary(text){
var data = new ArrayBuffer(text.length);//創(chuàng)建一個(gè)長(zhǎng)度為text.length的二進(jìn)制緩存區(qū)
var ui8a = new Uint8Array(data, 0);
for (var i = 0; i < text.length; i++){
ui8a[i] = (text.charCodeAt(i) & 0xff);
}
console.log(ui8a)
}
二進(jìn)制下載
在向后端發(fā)起請(qǐng)求時(shí),需要在請(qǐng)求頭中加上
responseType: 'blob'
這樣在返回data中可以得到一個(gè)瀏覽器可以解析的blob數(shù)據(jù)
const downURL = window.URL.createObjectURL(new Blob([data]));
// data 為獲取到的二進(jìn)制數(shù)據(jù)
const listNode = document.createElement("a");
// 這里注意 : 非同源a標(biāo)簽的download去命名沒(méi)有用
listNode.download = '合同公允價(jià)錯(cuò)誤文件下載.xlsx';
listNode.style.display = "none";
listNode.href = downURL;
2、frame上傳
低版本瀏覽器上,xhr請(qǐng)求不支持formdata上傳,只能form表單上傳。
form表單上傳,出現(xiàn)的問(wèn)題上文已經(jīng)提到,會(huì)本身進(jìn)行頁(yè)面跳轉(zhuǎn),產(chǎn)生原因?yàn)閠arget屬性導(dǎo)致
target我們或多或少有些了解,a標(biāo)簽也有改屬性:
_self:默認(rèn)值,在相同的窗口中打開(kāi)響應(yīng)頁(yè)面
_blank:在新窗口打開(kāi)
_parent:在父窗口打開(kāi)
_top:在最頂層的窗口打開(kāi)
實(shí)現(xiàn)方案
實(shí)現(xiàn)異步上傳的感覺(jué),自理我們就要用到framename去置頂名字的iframe中打開(kāi),也就是<iframe name='formtarget'></iframe>,<form target='formtarget'>,這樣一來(lái)返回的數(shù)據(jù)會(huì)被iframe接收,就不會(huì)出現(xiàn)刷新問(wèn)題,而返回的內(nèi)容可以通過(guò)iframe文本拿到。
問(wèn)題:預(yù)覽圖片只有先傳給后臺(tái),后臺(tái)再返回一個(gè)線上的地址
<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模擬表單數(shù)據(jù),通過(guò)ajax進(jìn)行提交,可以更加靈活地發(fā)送Ajax請(qǐng)求。可以使用FormData來(lái)模擬表單提交。
let files = e.target.files // 獲取input的file對(duì)象
let formData = new FormData();
formData.append('file', file);
axios.post(url, formData);
二、大文件上傳
在同一個(gè)請(qǐng)求中,要上傳大量的數(shù)據(jù),導(dǎo)致整個(gè)過(guò)程會(huì)比較漫長(zhǎng),且失敗后需要重頭開(kāi)始上傳。
大文件上傳我們需要考慮三個(gè)方面:
- 切片:拆分上傳請(qǐng)求
- 斷點(diǎn)續(xù)傳
- 顯示上傳進(jìn)度和暫停上傳
1、切片
識(shí)別切片來(lái)源
保證切片拼接順序
- 我們一般采用編碼的方式進(jìn)行上傳,獲取文件對(duì)應(yīng)的二進(jìn)制內(nèi)容。
- 計(jì)算出內(nèi)容的總大小,根據(jù)文件大小切成對(duì)應(yīng)的分片。
- 上傳時(shí)標(biāo)識(shí)出當(dāng)前文件,告訴后端上傳到了第幾個(gè)(可以用時(shí)間戳形式)。
- 不加表示的話后端在追加切片時(shí),無(wú)法識(shí)別切片順序
- 接口異常的情況下無(wú)法正確拼接
實(shí)現(xiàn)
根據(jù)文件名、文件長(zhǎng)度等基本信息進(jìn)行拼接,為了避免多個(gè)用戶上傳相同的文件,可以再額外拼接用戶信息如uid等保證唯一性
根據(jù)文件的二進(jìn)制內(nèi)容計(jì)算文件的hash,這樣只要文件內(nèi)容不一樣,則標(biāo)識(shí)也會(huì)不一樣,缺點(diǎn)在于計(jì)算量比較大.
將文件拆分成piece大小的分塊,然后每次請(qǐng)求只需要上傳這一個(gè)部分的分塊即可
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; // 每次上傳的開(kāi)始字節(jié)
let end = start + piece; // 每次上傳的結(jié)尾字節(jié)
let chunks = []
while (start < totalSize) {
// 根據(jù)長(zhǎng)度截取每次需要上傳的數(shù)據(jù)
// File對(duì)象繼承自Blob對(duì)象,因此包含slice方法
let blob = file.slice(start, end);
chunks.push(blob)
start = end;
end = start + piece;
}
return chunks
}
請(qǐng)求
/**
* 文件上傳
* @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、斷點(diǎn)續(xù)傳
我們?cè)谏蟼骰蛘呦螺d文件的時(shí)候,如果已經(jīng)進(jìn)行了一部分,這時(shí)候網(wǎng)絡(luò)故障、頁(yè)面關(guān)閉的情況下,不需要從頭開(kāi)始操作,而是從指定位置繼續(xù)進(jìn)行操作,這種處理方式就是所說(shuō)的“斷點(diǎn)續(xù)傳”
斷點(diǎn):的由來(lái)是在下載過(guò)程中,將一個(gè)下載文件分成了多個(gè)部分,同時(shí)進(jìn)行多個(gè)部分一起的下載,當(dāng)某個(gè)時(shí)間點(diǎn),任務(wù)被暫停了,此時(shí)下載暫停的位置就是斷點(diǎn)了。
續(xù)傳:一個(gè)任務(wù)從暫停到開(kāi)始時(shí),會(huì)從上一次任務(wù)暫停處開(kāi)始(可以每次傳輸成功后加一個(gè)表示為告訴前端傳輸進(jìn)度)。
實(shí)現(xiàn)思路:
- 保存已上傳的切片信息
- 選擇未上傳的切片進(jìn)行上傳
- 全部上傳成功后后端進(jìn)行文件合并
實(shí)現(xiàn)方案:
- 本地存儲(chǔ):我們可以利用localstorage,cookie等方式存儲(chǔ)在瀏覽器內(nèi),這種情況下我們不依賴于后端,直接本地讀取就行。清理了本地文件,會(huì)導(dǎo)致上傳記錄丟失。
- 其實(shí)服務(wù)器知道我們已經(jīng)傳輸?shù)搅四男┣衅切┻M(jìn)度,我們通過(guò)接口去傳輸為上傳的切片即可。
3、上傳進(jìn)度和暫停
進(jìn)度:我們可以利用xhr.upload.onprogress = Function方法做進(jìn)度的監(jiān)聽(tīng)
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
var percent = Math.floor( e.loaded / e.total * 100);//進(jìn)度計(jì)算
if(percent == 100){
}else{
}
}
};
暫停:如果該請(qǐng)求已被發(fā)出,XMLHttpRequest.abort() 方法將終止該請(qǐng)求,實(shí)現(xiàn)上傳暫停的效果。
繼續(xù):和斷點(diǎn)繼傳類似,先獲取傳輸?shù)牧斜恚缓笾匦掳l(fā)送未上傳的切片。

浙公網(wǎng)安備 33010602011771號(hào)