今年國慶假期終於能夠憋在家裏了不用出門了,不用出去看後腦了,真的是一種享受。這麼好的光陰怎麼浪費,睡覺、吃飯、打豆豆這怎麼可能(耍多了也煩),徹底不符合咱們程序員的做風,趕忙起來把文章寫完。html
這篇文章比較基礎,在國慶期間的業餘時間寫的,這幾天又完善了下,力求把更多的前端所涉及到的關於文件上傳的各類場景和應用都涵蓋了,如有疏漏和問題還請留言斧正和補充。前端
如下是本文所涉及到的知識點,break or continue ?html5
原理很簡單,就是根據 http 協議的規範和定義,完成請求消息體的封裝和消息體的解析,而後將二進制內容保存到文件。node
咱們都知道若是要上傳一個文件,須要把 form 標籤的enctype
設置爲multipart/form-data
,同時method
必須爲post
方法。react
那麼multipart/form-data
表示什麼呢?git
multipart互聯網上的混合資源,就是資源由多種元素組成,form-data表示可使用HTML Forms 和 POST 方法上傳文件,具體的定義能夠參考RFC 7578。程序員
multipart/form-data
結構github
看下 http 請求的消息體json
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDCntfiXcSkPhS4PN
表示本次請求要上傳文件,其中boundary表示分隔符,若是要上傳多個表單項,就要使用boundary分割,每一個表單項由———XXX開始,以———XXX結尾。windows
每個表單項又由Content-Type
和Content-Disposition
組成。
Content-Disposition: form-data
爲固定值,表示一個表單元素,name
表示表單元素的 名稱,回車換行後面就是name
的值,若是是上傳文件就是文件的二進制內容。
Content-Type
:表示當前的內容的 MIME 類型,是圖片仍是文本仍是二進制數據。
解析
客戶端發送請求到服務器後,服務器會收到請求的消息體,而後對消息體進行解析,解析出哪是普通表單哪些是附件。
可能你們立刻能想到經過正則或者字符串處理分割出內容,不過這樣是行不通的,二進制buffer
轉化爲string
,對字符串進行截取後,其索引和字符串是不一致的,因此結果就不會正確,除非上傳的就是字符串。
不過通常狀況下不須要自行解析,目前已經有很成熟的三方庫可使用。
至於如何解析,這個也會佔用很大篇幅,後面的文章在詳細說。
使用 form 表單上傳文件
在 ie
時代,若是實現一個無刷新的文件上傳那但是費老勁了,大部分都是用 iframe 來實現局部刷新或者使用 flash 插件來搞定,在那個時代 ie 就是最好用的瀏覽器(別無選擇)。
DEMO
這種方式上傳文件,不須要 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
會自動保存文件到系統臨時目錄下,也能夠指定保存的文件路徑。
而後在後續中間件內獲得已保存的文件的信息,再作二次處理。
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: {
//設置文件的默認保存目錄,不設置則保存在系統臨時目錄下 os
uploadDir: 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
在 ie
時代的多文件上傳是須要建立多個 input file
標籤,如今 html5
只須要一個標籤加個屬性就搞定了,file
標籤開啓multiple
。
DEMO
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
這裏說的是在 ie
時代的上傳文件局部刷新,藉助 iframe 實現。
DEMO
頁面內放一個隱藏的 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
無刷新上傳文件確定要用到XMLHttpRequest
,在 ie
時代也有這個對象,單隻 支持文本數據的傳輸,沒法用來讀取和上傳二進制數據。
如今已然升級到了XMLHttpRequest2
,較1版本有很是大的升級,首先就是能夠讀取和上傳二進制數據,可使用·FormData·對象管理表單數據。
固然也可以使用 fetch
進行上傳。
DEMO
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
藉助XMLHttpRequest2
的能力,實現多個文件或者一個文件的上傳進度條的顯示。
DEMO
說明
div.progress
js
內處理增長進度處理的監聽函數xhr.upload.onprogress
event.lengthComputable
這是一個狀態,表示發送的長度有了變化,可計算event.loaded
表示發送了多少字節event.total
表示文件總大小event.loaded
和event.total
計算進度,渲染div.progress
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
上一個栗子的多文件上傳只有一個進度條,有些需求可能會不大同樣,須要觀察到每一個文件的上傳進度,而且能夠終止上傳。
DEMO
說明
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.uploaded
if (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>
複製代碼
這裏沒有作上傳的併發控制,能夠經過控制同時可上傳文件的個數(這裏控制爲最多6個)或者上傳的時候作好併發處理,也就是同時只能上傳 X 個文件。
在測試過程當中,取消請求的方法xhr.abort()
調用後,xhr.readyState
會當即變爲4
,而不是0
,因此這裏須要作容錯處理。
MDN 上說是0.
若是你們有不一樣的結果,歡迎留言。
CODE
html5
的出現,讓拖拽上傳交互成爲可能,如今這樣的體驗也家常便飯。
DEMO
說明
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
掘金的寫文編輯器是支持粘貼上傳圖片的,好比我從磁盤粘貼或者從網頁上右鍵複製圖片。
DEMO
說明
div.editor-box
,開啓contenteditable
div.editor-box
綁定paste
事件paste
事件,從event.clipboardData || window.clipboardData
得到數據items[i].getAsFile()
insertNodeToEditor
方法測試中發現複製多個文件無效,只有最後一個文件上傳,在掘金的編輯器裏也一樣存在,在坐有知道緣由的能夠留言說下。
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) {
// 檢索剪切板items
for (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
在 ie
時代因爲沒法使用xhr
上傳二進制數據,上傳大文件須要藉助瀏覽器插件來完成。 如今來看實現大文件上傳簡直soeasy。
若是太大的文件,好比一個視頻1g 2g那麼大,直接採用上面的栗子中的方法上傳可能會出連接現超時的狀況,並且也會超過服務端容許上傳文件的大小限制,因此解決這個問題咱們能夠將文件進行分片上傳,每次只上傳很小的一部分 好比2M。
DEMO
說明
相信你們都對Blob
對象有所瞭解,它表示原始數據,也就是二進制數據,同時提供了對數據截取的方法slice
,而 File
繼承了Blob
的功能,因此能夠直接使用此方法對數據進行分段截圖。
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;//分片大小 2M
var 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
在上面咱們實現了大文件的分片上傳,解決了大文件上傳超時和服務器的限制。
可是仍然不夠完美,大文件上傳並非短期內就上傳完成,若是期間斷網,頁面刷新了仍然須要重頭上傳,這種時間的浪費怎麼能忍?
因此咱們實現斷點續傳,已上傳的部分跳過,只傳未上傳的部分。
在上面咱們實現了文件分片上傳和最終的合併,如今要作的就是如何檢測這些分片,再也不從新上傳便可。 這裏咱們能夠在本地進行保存已上傳成功的分片,從新上傳的時候使用spark-md5
來生成文件 hash,區分此文件是否已上傳。
spark-md5
庫PS
生成 hash 過程確定也會耗費資源,可是和從新上傳相比能夠忽略不計了。
DEMO
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呢,正常狀況下方法1沒問題,可是須要將分片信息保存在客戶端,保存在客戶端是最不保險的,說不定出現各類神奇的幺蛾子。
因此這裏有一個更完善的實現,只提供思路,代碼就不寫了,也是基於上面的實現,只是服務端須要增長一個接口。
基於上面一個栗子進行改進,服務端已保存了部分片斷,客戶端上傳前須要從服務端獲取已上傳的分片信息(上面是保存在了本地瀏覽器),本地對比每一個分片的 hash 值,跳過已上傳的部分,只傳未上傳的分片。
方法1是從本地獲取分片信息,這裏只須要將此方法的能力改成從服務端獲取分片信息就好了。
-getUploadedFromStorage
+getUploadedFromServer(fileHash)
複製代碼
另外服務端增長一個獲取分片的接口供客戶端調用,思路最重要,代碼就不貼了。
不僅會從客戶端上傳文件到服務器,服務器也會上傳文件到其餘服務器。
fs
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
判斷大小naturalWidth naturalHeight
or 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 的外觀比較傳統,不少地方都須要進行美化。
<label for="file">Choose file to upload</label>
<input type="file" id="file" name="file" multiple>
複製代碼
PS
file 標籤隱藏後在 ie 下沒法得到文件內容,建議仍是方法1
兼容性強。
以上代碼均已上傳 github
developer.mozilla.org/zh-CN/docs/…
developer.mozilla.org/zh-CN/docs/…
cloud.tencent.com/developer/n…
但願本文能夠給你帶了一些幫助,文中若有錯誤,歡迎在評論區指。
若是這篇文章幫助到了你,歡迎點贊和關注。
推薦關注個人微信公衆號【前端張大胖】,天天推送高質量文章和自學經驗和心得,咱們一塊兒交流成長。
你對 SSR 有興趣嗎?
若是你對 服務端渲染 ssr
技術有興趣,能夠關注個人開源項目:Zz.js
github:github.com/Bigerfe/koa…
SSR 技術原理