javascript
JavaScript如何进行文件上传
- 文件上傳原理
- 最原始的文件上傳
- 使用 koa2 作為服務端寫一個文件上傳接口
- 單文件上傳和上傳進度
- 多文件上傳和上傳進度
- 拖拽上傳
- 剪貼板上傳
- 大文件上傳之分片上傳
- 大文件上傳之斷點續傳
- node 端文件上傳
原理概述
原理很簡單,就是根據 http 協議的規范和定義,完成請求消息體的封裝和消息體的解析,然后將二進制內容保存到文件。
我們都知道如果要上傳一個文件,需要把 form 標簽的enctype設置為multipart/form-data,同時method必須為post方法。
那么multipart/form-data表示什么呢?
multipart互聯網上的混合資源,就是資源由多種元素組成,form-data表示可以使用HTML Forms 和 POST 方法上傳文件,具體的定義可以參考RFC 7578。
multipart/form-data 結構
看下 http 請求的消息體
[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="824" height="362"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="824" height="362"></svg>)
- 請求頭:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDCntfiXcSkPhS4PN 表示本次請求要上傳文件,其中boundary表示分隔符,如果要上傳多個表單項,就要使用boundary分割,每個表單項由———XXX開始,以———XXX結尾。
- 消息體- Form Data 部分
每一個表單項又由Content-Type和Content-Disposition組成。
Content-Disposition: form-data 為固定值,表示一個表單元素,name 表示表單元素的 名稱,回車換行后面就是name的值,如果是上傳文件就是文件的二進制內容。
Content-Type:表示當前的內容的 MIME 類型,是圖片還是文本還是二進制數據。
解析
客戶端發送請求到服務器后,服務器會收到請求的消息體,然后對消息體進行解析,解析出哪是普通表單哪些是附件。
可能大家馬上能想到通過正則或者字符串處理分割出內容,不過這樣是行不通的,二進制buffer轉化為string,對字符串進行截取后,其索引和字符串是不一致的,所以結果就不會正確,除非上傳的就是字符串。
不過一般情況下不需要自行解析,目前已經有很成熟的三方庫可以使用。
至于如何解析,這個也會占用很大篇幅,后面的文章在詳細說。
最原始的文件上傳
使用 form 表單上傳文件
在 ie時代,如果實現一個無刷新的文件上傳那可是費老勁了,大部分都是用 iframe 來實現局部刷新或者使用 flash 插件來搞定,在那個時代 ie 就是最好用的瀏覽器(別無選擇)。
DEMO
[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1024" height="366"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1024" height="366"></svg>)
這種方式上傳文件,不需要 js ,而且沒有兼容問題,所有瀏覽器都支持,就是體驗很差,導致頁面刷新,頁面其他數據丟失。
HTML
<form method="post" action="<http://localhost:8100>" enctype="multipart/form-data">選擇文件:<input type="file" name="f1"/> input 必須設置 name 屬性,否則數據無法發送<br/> <br/>標題:<input type="text" name="title"/><br/><br/><br/><button type="submit" id="btn-0">上 傳</button></form>復制代碼文件上傳接口
服務端文件的保存基于現有的庫koa-body結合 koa2實現服務端文件的保存和數據的返回。
在項目開發中,文件上傳本身和業務無關,代碼基本上都可通用。
在這里我們使用koa-body庫來實現解析和文件的保存。
koa-body 會自動保存文件到系統臨時目錄下,也可以指定保存的文件路徑。
[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1280" height="221"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1280" height="221"></svg>)
然后在后續中間件內得到已保存的文件的信息,再做二次處理。
- ctx.request.files.f1 得到文件信息,f1為input file 標簽的 name
- 獲得文件的擴展名,重命名文件
NODE
/*** 服務入口*/ var http = require('http'); var koaStatic = require('koa-static'); var path = require('path'); var koaBody = require('koa-body');//文件保存庫 var fs = require('fs'); var Koa = require('koa2');var app = new Koa(); var port = process.env.PORT || '8100';var uploadHost= `http://localhost:${port}/uploads/`;app.use(koaBody({formidable: {//設置文件的默認保存目錄,不設置則保存在系統臨時目錄下 osuploadDir: path.resolve(__dirname, '../static/uploads')},multipart: true // 開啟文件上傳,默認是關閉 }));//開啟靜態文件訪問 app.use(koaStatic(path.resolve(__dirname, '../static') ));//文件二次處理,修改名稱 app.use((ctx) => {var file = ctx.request.files.f1;//得道文件對象var path = file.path;var fname = file.name;//原文件名稱var nextPath = path+fname;if(file.size>0 && path){//得到擴展名var extArr = fname.split('.');var ext = extArr[extArr.length-1];var nextPath = path+'.'+ext;//重命名文件fs.renameSync(path, nextPath);}//以 json 形式輸出上傳文件地址ctx.body = `{"fileUrl":"${uploadHost}${nextPath.slice(nextPath.lastIndexOf('/')+1)}"}`; });/*** http server*/ var server = http.createServer(app.callback()); server.listen(port); console.log('demo1 server start ...... '); 復制代碼CODE
github.com/Bigerfe/fe-…
多文件上傳
在 ie 時代的多文件上傳是需要創建多個 input file 標簽,現在 html5只需要一個標簽加個屬性就搞定了,file 標簽開啟multiple。
DEMO
[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1012" height="312"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1012" height="312"></svg>)
HTML
//設置 multiple屬性 <input type="file" name="f1" multiple/> 復制代碼NODE
服務端也需要進行簡單的調整,由單文件對象變為多文件數組,然后進行遍歷處理。
//二次處理文件,修改名稱 app.use((ctx) => {var files = ctx.request.files.f1;// 多文件, 得到上傳文件的數組var result=[];//遍歷處理files && files.forEach(item=>{var path = item.path;var fname = item.name;//原文件名稱var nextPath = path + fname;if (item.size > 0 && path) {//得到擴展名var extArr = fname.split('.');var ext = extArr[extArr.length - 1];var nextPath = path + '.' + ext;//重命名文件fs.renameSync(path, nextPath);//文件可訪問路徑放入數組result.push(uploadHost+ nextPath.slice(nextPath.lastIndexOf('/') + 1));}});//輸出 json 結果ctx.body = `{"fileUrl":${JSON.stringify(result)}}`; }) 復制代碼CODE
github.com/Bigerfe/fe-…
局部刷新 - iframe
這里說的是在 ie 時代的上傳文件局部刷新,借助 iframe 實現。
DEMO
[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1156" height="338"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1156" height="338"></svg>)
- 局部刷新
頁面內放一個隱藏的 iframe,或者使用 js 動態創建,指定 form 表單的 target 屬性值為iframe標簽 的 name 屬性值,這樣 form 表單的 shubmit 行為的跳轉就會在 iframe 內完成,整體頁面不會刷新。
- 拿到接口數據
然后為 iframe 添加load事件,得到 iframe 的頁面內容,將結果轉換為 JSON 對象,這樣就拿到了接口的數據
HTML
<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/><br/> input 必須設置 name 屬性,否則數據無法發送<br/> <br/>標題:<input type="text" name="title"/><br/><br/><br/><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> 復制代碼NODE
服務端代碼不需要改動,略.
CODE
github.com/Bigerfe/fe-…
無刷新上傳
無刷新上傳文件肯定要用到XMLHttpRequest,在 ie 時代也有這個對象,單只 支持文本數據的傳輸,無法用來讀取和上傳二進制數據。
現在已然升級到了XMLHttpRequest2,較1版本有非常大的升級,首先就是可以讀取和上傳二進制數據,可以使用·FormData·對象管理表單數據。
當然也可使用 fetch 進行上傳。
DEMO
[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1252" height="322"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1252" height="322"></svg>)
HTML
<div>選擇文件(可多選):<input type="file" id="f1" multiple/><br/><br/><button type="button" id="btn-submit">上 傳</button> </div>復制代碼JS xhr
<script>function submitUpload() {//獲得文件列表,注意這里不是數組,而是對象var fileList = document.getElementById('f1').files;if(!fileList.length){alert('請選擇文件');return;}var fd = new FormData(); //構造FormData對象fd.append('title', document.getElementById('title').value);//多文件上傳需要遍歷添加到 fromdata 對象for(var i =0;i<fileList.length;i++){fd.append('f1', fileList[i]);//支持多文件上傳}var xhr = new XMLHttpRequest(); //創建對象xhr.open('POST', '<http://localhost:8100/>', true);xhr.send(fd);//發送時 Content-Type默認就是: multipart/form-data; xhr.onreadystatechange = function () {console.log('state change', xhr.readyState);if (this.readyState == 4 && this.status == 200) {var obj = JSON.parse(xhr.responseText); //返回值console.log(obj);if(obj.fileUrl.length){alert('上傳成功');}}}}//綁定提交事件document.getElementById('btn-submit').addEventListener('click',submitUpload); </script>復制代碼JS Fetch
fetch('<http://localhost:8100/>', {method: 'POST',body: fd}).then(response => response.json()).then(response =>{console.log(response);if (response.fileUrl.length) {alert('上傳成功');}} ).catch(error => console.error('Error:', error)); 復制代碼CODE
github.com/Bigerfe/fe-…
多文件,單進度
借助XMLHttpRequest2的能力,實現多個文件或者一個文件的上傳進度條的顯示。
DEMO
[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1044" height="319"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1044" height="319"></svg>)
說明
- 頁面內增加一個用于顯示進度的標簽 div.progress
- js 內處理增加進度處理的監聽函數xhr.upload.onprogress
- event.lengthComputable這是一個狀態,表示發送的長度有了變化,可計算
- event.loaded表示發送了多少字節
- event.total表示文件總大小
- 根據event.loaded和event.total計算進度,渲染div.progress
PS 特別提醒
xhr.upload.onprogress要寫在xhr.send方法前面,否則event.lengthComputable狀態不會改變,只有在最后一次才能獲得,也就是100%的時候.
HTML
<div>選擇文件(可多選):<input type="file" id="f1" multiple/><br/><br/><div id="progress"><span class="red"></span></div><button type="button" id="btn-submit">上 傳</button></div> 復制代碼JS
<script>function submitUpload() {var progressSpan = document.getElementById('progress').firstElementChild;var fileList = document.getElementById('f1').files;progressSpan.style.width='0';progressSpan.classList.remove('green');if(!fileList.length){alert('請選擇文件');return;}var fd = new FormData(); //構造FormData對象fd.append('title', document.getElementById('title').value);for(var i =0;i<fileList.length;i++){fd.append('f1', fileList[i]);//支持多文件上傳}var xhr = new XMLHttpRequest(); //創建對象xhr.open('POST', '<http://10.70.65.235:8100/>', true);xhr.onreadystatechange = function () {console.log('state change', xhr.readyState);if (xhr.readyState == 4) {var obj = JSON.parse(xhr.responseText); //返回值console.log(obj);if(obj.fileUrl.length){//alert('上傳成功');}}}xhr.onprogress=updateProgress;xhr.upload.onprogress = updateProgress;function updateProgress(event) {console.log(event);if (event.lengthComputable) {var completedPercent = (event.loaded / event.total * 100).toFixed(2);progressSpan.style.width= completedPercent+'%';progressSpan.innerHTML=completedPercent+'%';if(completedPercent>90){//進度條變色progressSpan.classList.add('green');}console.log('已上傳',completedPercent);}}//注意 send 一定要寫在最下面,否則 onprogress 只會執行最后一次 也就是100%的時候xhr.send(fd);//發送時 Content-Type默認就是: multipart/form-data; }//綁定提交事件document.getElementById('btn-submit').addEventListener('click',submitUpload);</script> 復制代碼CODE
github.com/Bigerfe/fe-…
多文件上傳+預覽+取消
上一個栗子的多文件上傳只有一個進度條,有些需求可能會不大一樣,需要觀察到每個文件的上傳進度,并且可以終止上傳。
DEMO
[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1280" height="446"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1280" height="446"></svg>)
說明
- 為了預覽的需要,我們這里選擇上傳圖片文件,其他類型的也一樣,只是預覽不方便
- 頁面內增加一個多圖預覽的容器div.img-box
- 根據選擇的文件信息動態創建所屬的預覽區域和進度條以及取消按鈕
- 為取消按鈕綁定事件,調用xhr.abort();終止上傳
- 使用window.URL.createObjectURL預覽圖片,在圖片加載成功后需要清除使用的內存window.URL.revokeObjectURL(this.src);
HTML
<div>選擇文件(可多選):<div class="addfile">添加文件<input type="file" id="f1" multiple /></div><div class="img-box"></div><button type="button" id="btn-submit">上 傳</button></div> 復制代碼JS
<script>//更改網絡 為慢3g,就可以比較明顯的看到進度條了var fileMaxCount=6;var imgBox =document.getElementsByClassName('img-box')[0];var willUploadFile=[];//保存待上傳的文件以及相關附屬信息document.getElementById('f1').addEventListener('change',function (e) {var fileList = document.getElementById('f1').files;if (willUploadFile.length > fileMaxCount || fileList.length>fileMaxCount || (willUploadFile.length+ fileList.length>fileMaxCount)) {alert('最多只能上傳' + fileMaxCount + '張圖');return;}for (var i = 0; i < fileList.length; i++) {var f = fileList[i];//先預覽圖片var img = document.createElement('img');var item = document.createElement('div');var progress = document.createElement('div');progress.className='progress';progress.innerHTML = '<span class="red"></span><button type="button">Abort</button>';item.className='item';img.src = window.URL.createObjectURL(f);img.onload = function () {//顯示要是否這塊兒內存window.URL.revokeObjectURL(this.src);}item.appendChild(img);item.appendChild(progress);imgBox.appendChild(item);willUploadFile.push({file:f,item,progress});}});function xhrSend({file, progress}) {var progressSpan = progress.firstElementChild;var btnCancel = progress.getElementsByTagName('button')[0];var abortFn=function(){if(xhr && xhr.readyState!==4){//取消上傳xhr.abort();} }btnCancel.removeEventListener('click',abortFn);btnCancel.addEventListener('click',abortFn);progressSpan.style.width='0';progressSpan.classList.remove('green');var fd = new FormData(); //構造FormData對象fd.append('f1',file);var xhr = new XMLHttpRequest(); //創建對象xhr.open('POST', '<http://localhost:8100/>', true);xhr.onreadystatechange = function () {console.log('state change', xhr.readyState);//調用 abort 后,state 立即變成了4,并不會變成0//增加自定義屬性 xhr.uploadedif (xhr.readyState == 4 && xhr.uploaded) {var obj = JSON.parse(xhr.responseText); //返回值console.log(obj);if(obj.fileUrl.length){//alert('上傳成功');}}}xhr.onprogress=updateProgress;xhr.upload.onprogress = updateProgress;function updateProgress(event) {if (event.lengthComputable) {var completedPercent = (event.loaded / event.total * 100).toFixed(2);progressSpan.style.width= completedPercent+'%';progressSpan.innerHTML=completedPercent+'%';if(completedPercent>90){//進度條變色progressSpan.classList.add('green');}if(completedPercent>=100){xhr.uploaded=true;}console.log('已上傳',completedPercent);}}//注意 send 一定要寫在最下面,否則 onprogress 只會執行最后一次 也就是100%的時候xhr.send(fd);//發送時 Content-Type默認就是: multipart/form-data; return xhr;}//文件上傳function submitUpload(willFiles) {if(!willFiles.length){return;}//遍歷文件信息進行上傳willFiles.forEach(function (item) {xhrSend({file:item.file,progress:item.progress});});}//綁定提交事件document.getElementById('btn-submit').addEventListener('click',function () {submitUpload(willUploadFile);});</script> 復制代碼問題1
這里沒有做上傳的并發控制,可以通過控制同時可上傳文件的個數(這里控制為最多6個)或者上傳的時候做好并發處理,也就是同時只能上傳 X 個文件。
問題2
在測試過程中,取消請求的方法xhr.abort()調用后,xhr.readyState會立即變為4,而不是0,所以這里需要做容錯處理。
MDN 上說是0.
[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="740" height="102"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="740" height="102"></svg>)
如果大家有不同的結果,歡迎留言。
CODE
github.com/Bigerfe/fe-…
拖拽上傳
html5的出現,讓拖拽上傳交互成為可能,現在這樣的體驗也屢見不鮮。
DEMO
[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1080" height="495"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1080" height="495"></svg>)
說明
- 定義一個允許拖放文件的區域div.drop-box
- 取消drop 事件的默認行為e.preventDefault();,不然瀏覽器會直接打開文件
- 為拖拽區域綁定事件,鼠標在拖拽區域上 dragover, 鼠標離開拖拽區域dragleave, 在拖拽區域上釋放文件drop
- drop事件內獲得文件信息e.dataTransfer.files
HTML
<div class="drop-box" id="drop-box">拖動文件到這里,開始上傳</div><button type="button" id="btn-submit">上 傳</button> 復制代碼JS
<script>var box = document.getElementById('drop-box');//禁用瀏覽器的拖放默認行為document.addEventListener('drop',function (e) {console.log('document drog');e.preventDefault();});//設置拖拽事件function openDropEvent() {box.addEventListener("dragover",function (e) {console.log('elemenet dragover');box.classList.add('over');e.preventDefault();});box.addEventListener("dragleave", function (e) {console.log('elemenet dragleave');box.classList.remove('over');e.preventDefault();});box.addEventListener("drop", function (e) {e.preventDefault(); //取消瀏覽器默認拖拽效果var fileList = e.dataTransfer.files; //獲取拖拽中的文件對象var len=fileList.length;//用來獲取文件的長度(其實是獲得文件數量)//檢測是否是拖拽文件到頁面的操作if (!len) {box.classList.remove('over');return;}box.classList.add('over');window.willUploadFileList=fileList;}, false);}openDropEvent();function submitUpload() {var fileList = window.willUploadFileList||[];if(!fileList.length){alert('請選擇文件');return;}var fd = new FormData(); //構造FormData對象for(var i =0;i<fileList.length;i++){fd.append('f1', fileList[i]);//支持多文件上傳}var xhr = new XMLHttpRequest(); //創建對象xhr.open('POST', '<http://localhost:8100/>', true);xhr.onreadystatechange = function () {if (xhr.readyState == 4) {var obj = JSON.parse(xhr.responseText); //返回值if(obj.fileUrl.length){alert('上傳成功');}}}xhr.send(fd);//發送}//綁定提交事件document.getElementById('btn-submit').addEventListener('click',submitUpload);</script> 復制代碼CODE
github.com/Bigerfe/fe-…
剪貼板上傳
掘金的寫文編輯器是支持粘貼上傳圖片的,比如我從磁盤粘貼或者從網頁上右鍵復制圖片。
DEMO
[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1280" height="327"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1280" height="327"></svg>)
說明
- 頁面內增加一個可編輯的編輯區域div.editor-box,開啟contenteditable
- 為div.editor-box綁定paste事件
- 處理paste 事件,從event.clipboardData || window.clipboardData獲得數據
- 將數據轉換為文件items[i].getAsFile()
- 實現在編輯區域的光標處插入內容 insertNodeToEditor 方法
問題1
測試中發現復制多個文件無效,只有最后一個文件上傳,在掘金的編輯器里也同樣存在,在坐有知道原因的可以留言說下。
問題2
mac系統可以支持從磁盤復制文件后上傳,windows 系統測試未通過,剪貼板的數據未拿到。
HTML
<div class="editor-box" id="editor-box" contenteditable="true" >可以直接粘貼圖片到這里直接上傳</div> 復制代碼JS
//光標處插入 dom 節點function insertNodeToEditor(editor,ele) {//插入dom 節點var range;//記錄光標位置對象var node = window.getSelection().anchorNode;// 這里判斷是做是否有光標判斷,因為彈出框默認是沒有的if (node != null) {range = window.getSelection().getRangeAt(0);// 獲取光標起始位置range.insertNode(ele);// 在光標位置插入該對象} else {editor.append(ele);}}var box = document.getElementById('editor-box');//綁定paste事件box.addEventListener('paste',function (event) {var data = (event.clipboardData || window.clipboardData);var items = data.items;var fileList = [];//存儲文件數據if (items && items.length) {// 檢索剪切板itemsfor (var i = 0; i < items.length; i++) {console.log(items[i].getAsFile());fileList.push(items[i].getAsFile());}}window.willUploadFileList = fileList;event.preventDefault();//阻止默認行為submitUpload();}); function submitUpload() {var fileList = window.willUploadFileList||[];var fd = new FormData(); //構造FormData對象for(var i =0;i<fileList.length;i++){fd.append('f1', fileList[i]);//支持多文件上傳}var xhr = new XMLHttpRequest(); //創建對象xhr.open('POST', '<http://localhost:8100/>', true);xhr.onreadystatechange = function () {if (xhr.readyState === 4) {var obj = JSON.parse(xhr.responseText); //返回值console.log(obj);if(obj.fileUrl.length){var img = document.createElement('img');img.src= obj.fileUrl[0];img.style.width='100px';insertNodeToEditor(box,img);// alert('上傳成功');}}}xhr.send(fd);//發送}復制代碼CODE
github.com/Bigerfe/fe-…
大文件上傳-分片
在 ie 時代由于無法使用xhr上傳二進制數據,上傳大文件需要借助瀏覽器插件來完成。 現在來看實現大文件上傳簡直soeasy。
如果太大的文件,比如一個視頻1g 2g那么大,直接采用上面的栗子中的方法上傳可能會出鏈接現超時的情況,而且也會超過服務端允許上傳文件的大小限制,所以解決這個問題我們可以將文件進行分片上傳,每次只上傳很小的一部分 比如2M。
DEMO
[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="425" height="676"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="425" height="676"></svg>)
[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1247" height="398"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1247" height="398"></svg>)
說明
相信大家都對Blob 對象有所了解,它表示原始數據,也就是二進制數據,同時提供了對數據截取的方法slice,而 File 繼承了Blob的功能,所以可以直接使用此方法對數據進行分段截圖。
- 把大文件進行分段 比如2M,發送到服務器攜帶一個標志,暫時用當前的時間戳,用于標識一個完整的文件
- 服務端保存各段文件
- 瀏覽器端所有分片上傳完成,發送給服務端一個合并文件的請求
- 服務端根據文件標識、類型、各分片順序進行文件合并
- 刪除分片文件
HTML
代碼略,只需要一個 input file 標簽。
JS
//分片邏輯 像操作字符串一樣var start=0,end=0;while (true) {end+=chunkSize;var blob = file.slice(start,end);start+=chunkSize;if(!blob.size){//截取的數據為空 則結束//拆分結束break;}chunks.push(blob);//保存分段數據}<script>function submitUpload() {var chunkSize=2*1024*1024;//分片大小 2Mvar file = document.getElementById('f1').files[0];var chunks=[], //保存分片數據token = (+ new Date()),//時間戳name =file.name,chunkCount=0,sendChunkCount=0;//拆分文件 像操作字符串一樣if(file.size>chunkSize){//拆分文件var start=0,end=0;while (true) {end+=chunkSize;var blob = file.slice(start,end);start+=chunkSize;if(!blob.size){//截取的數據為空 則結束//拆分結束break;}chunks.push(blob);//保存分段數據}}else{chunks.push(file.slice(0));}chunkCount=chunks.length;//分片的個數 //沒有做并發限制,較大文件導致并發過多,tcp 鏈接被占光 ,需要做下并發控制,比如只有4個在請求在發送for(var i=0;i< chunkCount;i++){var fd = new FormData(); //構造FormData對象fd.append('token', token);fd.append('f1', chunks[i]);fd.append('index', i);xhrSend(fd,function () {sendChunkCount+=1;if(sendChunkCount===chunkCount){//上傳完成,發送合并請求console.log('上傳完成,發送合并請求');var formD = new FormData();formD.append('type','merge');formD.append('token',token);formD.append('chunkCount',chunkCount);formD.append('filename',name);xhrSend(formD);}});}}function xhrSend(fd,cb) {var xhr = new XMLHttpRequest(); //創建對象xhr.open('POST', '<http://localhost:8100/>', true);xhr.onreadystatechange = function () {console.log('state change', xhr.readyState);if (xhr.readyState == 4) {console.log(xhr.responseText);cb && cb();}}xhr.send(fd);//發送}//綁定提交事件document.getElementById('btn-submit').addEventListener('click',submitUpload); </script>復制代碼NODE
服務端需要做一些改動,保存分片文件、合并分段文件、刪除分段文件。
PS
合并文件這里使用 stream pipe 實現,這樣更節省內存,邊讀邊寫入,占用內存更小,效率更高,代碼見fnMergeFile方法。
//二次處理文件,修改名稱 app.use((ctx) => {var body = ctx.request.body;var files = ctx.request.files ? ctx.request.files.f1:[];//得到上傳文件的數組var result=[];var fileToken = ctx.request.body.token;// 文件標識var fileIndex=ctx.request.body.index;//文件順序if(files && !Array.isArray(files)){//單文件上傳容錯files=[files];}files && files.forEach(item=>{var path = item.path;var fname = item.name;//原文件名稱var nextPath = path.slice(0, path.lastIndexOf('/') + 1) + fileIndex + '-' + fileToken;if (item.size > 0 && path) {//得到擴展名var extArr = fname.split('.');var ext = extArr[extArr.length - 1];//var nextPath = path + '.' + ext;//重命名文件fs.renameSync(path, nextPath);result.push(uploadHost+nextPath.slice(nextPath.lastIndexOf('/') + 1));}});if(body.type==='merge'){//合并分片文件var filename = body.filename,chunkCount = body.chunkCount,folder = path.resolve(__dirname, '../static/uploads')+'/';var writeStream = fs.createWriteStream(`${folder}${filename}`);var cindex=0;//合并文件function fnMergeFile(){var fname = `${folder}${cindex}-${fileToken}`;var readStream = fs.createReadStream(fname);readStream.pipe(writeStream, { end: false });readStream.on("end", function () {fs.unlink(fname, function (err) {if (err) {throw err;}});if (cindex+1 < chunkCount){cindex += 1;fnMergeFile();}});}fnMergeFile();ctx.body='merge ok 200';}}); 復制代碼CODE
github.com/Bigerfe/fe-…
大文件上傳-斷點續傳
在上面我們實現了大文件的分片上傳,解決了大文件上傳超時和服務器的限制。
但是仍然不夠完美,大文件上傳并不是短時間內就上傳完成,如果期間斷網,頁面刷新了仍然需要重頭上傳,這種時間的浪費怎么能忍?
所以我們實現斷點續傳,已上傳的部分跳過,只傳未上傳的部分。
方法1
在上面我們實現了文件分片上傳和最終的合并,現在要做的就是如何檢測這些分片,不再重新上傳即可。 這里我們可以在本地進行保存已上傳成功的分片,重新上傳的時候使用spark-md5來生成文件 hash,區分此文件是否已上傳。
- 為每個分段生成 hash 值,使用 spark-md5 庫
- 將上傳成功的分段信息保存到本地
- 重新上傳時,進行和本地分段 hash 值的對比,如果相同的話則跳過,繼續下一個分段的上傳
PS
生成 hash 過程肯定也會耗費資源,但是和重新上傳相比可以忽略不計了。
DEMO
[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1240" height="450"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1240" height="450"></svg>)
[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1095" height="404"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1095" height="404"></svg>)
HTML
代碼略 復制代碼JS
模擬分段保存,本地保存到localStorage
//獲得本地緩存的數據function getUploadedFromStorage(){return JSON.parse( localStorage.getItem(saveChunkKey) || "{}");}//寫入緩存function setUploadedToStorage(index) {var obj = getUploadedFromStorage();obj[index]=true; localStorage.setItem(saveChunkKey, JSON.stringify(obj) );}//分段對比var uploadedInfo = getUploadedFromStorage();//獲得已上傳的分段信息for(var i=0;i< chunkCount;i++){console.log('index',i, uploadedInfo[i]?'已上傳過':'未上傳');if(uploadedInfo[i]){//對比分段sendChunkCount=i+1;//記錄已上傳的索引continue;//如果已上傳則跳過}var fd = new FormData(); //構造FormData對象fd.append('token', token);fd.append('f1', chunks[i]);fd.append('index', i);(function (index) {xhrSend(fd, function () {sendChunkCount += 1;//將成功信息保存到本地setUploadedToStorage(index);if (sendChunkCount === chunkCount) {console.log('上傳完成,發送合并請求');var formD = new FormData();formD.append('type', 'merge');formD.append('token', token);formD.append('chunkCount', chunkCount);formD.append('filename', name);xhrSend(formD);}});})(i);}復制代碼方法2
為什么還有方法2呢,正常情況下方法1沒問題,但是需要將分片信息保存在客戶端,保存在客戶端是最不保險的,說不定出現各種神奇的幺蛾子。
所以這里有一個更完善的實現,只提供思路,代碼就不寫了,也是基于上面的實現,只是服務端需要增加一個接口。
基于上面一個栗子進行改進,服務端已保存了部分片段,客戶端上傳前需要從服務端獲取已上傳的分片信息(上面是保存在了本地瀏覽器),本地對比每個分片的 hash 值,跳過已上傳的部分,只傳未上傳的分片。
方法1是從本地獲取分片信息,這里只需要將此方法的能力改為從服務端獲取分片信息就行了。
-getUploadedFromStorage +getUploadedFromServer(fileHash) 復制代碼另外服務端增加一個獲取分片的接口供客戶端調用,思路最重要,代碼就不貼了。
node 端上傳圖片
不只會從客戶端上傳文件到服務器,服務器也會上傳文件到其他服務器。
- 讀取文件buffer fs
- 構建 form-data form-data
- 上傳文件 node-fetch
NODE
/*** filepath = 相對根目錄的路徑即可*/async function getFileBufer(filePath) => {return new Promise((resolve) => {fs.readFile(filePath, function (err, data) {var bufer = null;if (!err) {resolve({err: err,data: data});}});});}/*** 上傳文件*/let fetch = require('node-fetch');let formData = require('form-data');module.exports = async (options) => {let {imgPath} = options;let data = await getFileBufer(imgPath);if (data.err) {return null;}let form = new formData();form.append('xxx', xxx);form.append('pic', data.data);return fetch('<http://xx.com/upload>', {body: form,method: 'POST',headers: form.getHeaders()//要活的 form-data的頭,否則無法上傳}).then(res => {return res.json();}).then(data => {return data;})} 復制代碼其他
在瀏覽器端對文件的類型、大小、尺寸進行判斷
- file.type判斷類型
- file.size判斷大小
- 通過動態創建 img 標簽,圖片加載后獲得尺寸,naturalWidth naturalHeightor width height
JS
var file = document.getElementById('f1').files[0];//判斷類型if(f.type!=='image/jpeg' && f.type !== 'image/jpg' ){alert('只能上傳 jpg 圖片');flag=false;break;}//判斷大小if(file.size>100*1024){alert('不能大于100kb');}//判斷圖片尺寸var img =new Image();img.onload=function(){console.log('圖片原始大小 width*height', this.width, this.height);if(this.naturalWidth){console.log('圖片原始大小 naturalWidth*naturalHeight', this.naturalWidth, this.naturalHeight);}else{console.log('oImg.width*height', this.width, this.height);}} 復制代碼input file 外觀更改
由于input file 的外觀比較傳統,很多地方都需要進行美化。
PS
file 標簽隱藏后在 ie 下無法獲得文件內容,建議還是方法1 兼容性強。
源碼在這里
以上代碼均已上傳 github
github.com/Bigerfe/fe-…
參考資料
developer.mozilla.org/zh-CN/docs/…
developer.mozilla.org/zh-CN/docs/…
cloud.tencent.com/developer/n…
作者:zz_jesse
鏈接:https://juejin.cn/post/6844903968338870285
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
總結
以上是生活随笔為你收集整理的JavaScript如何进行文件上传的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PHP常用系统函数
- 下一篇: gradle idea java ssm