最近要作個圖片上傳的需求,由於服務端春節請假回家還沒來,因此就我本身先折騰了一下,大概作出來個效果,後臺就用了nodejs,剛開始作的時候想網上找一下資料,發現大部分資料都是用node-formidable插件實現上傳的。可是本身又想手動實現一下,因此就開始折騰了。寫此博文也就是作個記錄。css
先大概整理一下整個思路,本身想要實現的效果是可以在頁面上無刷新上傳一個圖片而且顯示(後來作着作着就變成全部文件的上傳了,不過都一個樣)。html
在前端部分,想要無刷新首先想到的是ajax,可是ajax沒法上傳文件,因此仍是老老實實用form上傳,若是用form的話又要保證頁面無刷新,那就使用iframe來實現了。因此前端須要兩個頁面,一個用戶操做頁面index.html爲主頁面,還有一個是專門用來上傳的頁面upload.html,html以下:前端
index.html: <body> 您上傳的東西爲:<br><br> <div class="data"> (無) </div> <br> <button class="choose">上傳東西</button> <iframe src="upl" frameborder="0" id="upl"></iframe> </body> upload.html: <body> <form action="/upload" method=post enctype="multipart/form-data" accept-charset="utf-8"> <input type="file" id="data" name="data" /> <input type="submit" value="上傳" id="sub"/> </form> </body>
index.html頁面點擊上傳按鈕,js將會觸發iframe裏的upload頁面裏的input file的click事件,因此進行文件選擇,選擇好後再觸發upload頁面裏的submit的click事件,文件便開始上傳,文件上傳成功後,後臺將會返回一段html代碼,裏面就包含着文件連接。index.html頁面獲取到文件連接,若是是圖片則顯示圖片,若是是其餘則顯示下載連接。index.html的js代碼以下:node
window.onload = function(){ var frame = $("#upl")[0]; var cd; frameInit() frame.onload = function(){ frameInit() if($(cd).find("#path").length>0){ var path = $(cd).find("#path")[0].innerHTML; if(/png|gif|jpg/g.test(path)){ $(".data").html("<img src='"+path+"'><br>") }else { $(".data").html("<a href='"+path+"' target='_blank'>"+path+"</a><br>") } frame.src = "upl"; } } $(".choose").click(function(){ $(cd).find("#data").click(); }); function frameInit(){ cd = frame.contentDocument.body; var img = $(cd).find("#data")[0] if(img){ img.onchange = function(){ $(cd).find("#sub").click(); } } } }
經過iframe的onload事件來獲取後臺返回的連接。以上代碼比較簡單,就不具體解釋了。git
接下來是後臺的實現:github
首先先是要建個http server,而後,由於有兩個頁面,再加上還有文件下載之類的,因此先弄個最簡單的路由:ajax
var http = require('http'); var fs = require('fs'); http.createServer(function(req , res){ var imaps = req.url.split("/"); var maps = []; imaps.forEach(function(m){ if(m){maps.push(m)} }); switch (maps[0]||"index"){ case "index": var str = fs.readFileSync("./index.html"); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(str , "utf-8"); break; case "upl": var str = fs.readFileSync("./upload.html"); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(str , "utf-8"); break; case "upload": break; default : var path = maps.join("/"); var value = ""; var filename = maps[maps.length-1]; var checkReg = /^.+.(gif|png|jpg|css|js)+$/; if(maps[0]=="databox"){ checkReg = /.*/ } if(checkReg.test(filename)){ try{ value = fs.readFileSync(path) }catch(e){} } if(value){ res.end(value); }else { res.writeHead(404); res.end(''); } break; } }).listen(9010);
上面代碼也很簡單,路由index指向index.html,upl指向upload.html,而其餘若是是非指向databox裏的連接則只容許訪問圖片、css、js文件,若是是指向databox的連接則容許訪問一切,databox是用來存儲上傳文件的文件夾。上面代碼中upload路由就是文件上傳的提交地址,因此文件上傳後,對文件的處理就是這裏。post
對post過來的數據的處理,經常使用的辦法就是:ui
var chunks = []; var size = 0; req.on('data' , function(chunk){ chunks.push(chunk); size+=chunk.length; }); req.on("end",function(){ var buffer = Buffer.concat(chunks , size); });
那個buffer就是post過來的全部數據了,當咱們console.log(buffer.toString()),咱們就能夠看到post過來的數據的格式:編碼
其中,紅色方框裏的亂碼其實就是文件數據了,前面的是文件信息報頭。若是想得到裏面的數據,就得先把非文件數據過濾掉,根據控制檯輸出的信息可知過濾的方法很簡單,根據\r\n來分割就能夠了,數據開頭四個\r\n以後就是文件數據,而結尾的話則是去掉\r\n--WebKitFormblabla--\r\n,也是根據\r\n來過濾。因此把上面那段代碼補全後就是以下:
var chunks = []; var size = 0; req.on('data' , function(chunk){ chunks.push(chunk); size+=chunk.length; }); req.on("end",function(){ var buffer = Buffer.concat(chunks , size); if(!size){ res.writeHead(404); res.end(''); return; } var rems = []; //根據\r\n分離數據和報頭 for(var i=0;i<buffer.length;i++){ var v = buffer[i]; var v2 = buffer[i+1]; if(v==13 && v2==10){ rems.push(i); } } //圖片信息 var picmsg_1 = buffer.slice(rems[0]+2,rems[1]).toString(); var filename = picmsg_1.match(/filename=".*"/g)[0].split('"')[1]; //圖片數據 var nbuf = buffer.slice(rems[3]+2,rems[rems.length-2]); var path = './databox/'+filename; fs.writeFileSync(path , nbuf); console.log("保存"+filename+"成功"); res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8'}); res.end('<div id="path">'+path+'</div>'); });
對數據的過濾直接經過分析buffer,剛開始本身寫的時候是把buffer轉成string來分析,可是問題出現了,當過濾完後,把數據寫入文件前須要把string再轉成buffer寫進去,結果寫出來的文件都是錯誤的。改各類編碼轉buffer都不行,折騰了N久,最後的終於找到對應的方案,就是在buffer轉string的時候寫成buffer.toString("binary"),而後再過濾完後再處理成buffer的時候寫成new Buffer(str , 'binary')才行,可是查了一下文件,貌似buffer中binary的編碼被棄用了,或者說不建議使用。因此本身就想不轉string,直接分析buffer。經過查ascii表很容易經過一個for循環把\r\n找出來了。因而問題就解決了。
運行效果良好:
這看似把上傳文件的功能實現了,可是仔細一想,好像還有問題,由於本身此時是想實現個文件上傳了,而不是單單的圖片上傳,因此若是我上傳的數據幾百M,那麼一次性把buffer所有讀出來再處理,不要說處理速度慢,就單單這文件數據就能把內存耗的差很少了。因此這種把數據所有接收過來再處理的方法貌似不行,最好就是數據一邊接收一邊處理,不讓全部數據所有擠在內存上。因此,我就使用了stream。
整個處理代碼改爲了,原本是在數據接收完成上進行處理改爲在接收數據的時候進行處理:
var imgsays = []; var num = 0; var isStart = false; var ws; var filename; var path; req.on('data' , function(chunk){ var start = 0; var end = chunk.length; var rems = []; for(var i=0;i<chunk.length;i++){ if(chunk[i]==13 && chunk[i+1]==10){ num++; rems.push(i); if(num==4){ start = i+2; isStart = true; var str = (new Buffer(imgsays)).toString(); filename = str.match(/filename=".*"/g)[0].split('"')[1]; path = './databox/'+filename; ws = fs.createWriteStream(path); }else if(i==chunk.length-2){ //說明到了數據尾部的\r\n end = rems[rems.length-2]; break; } } if(num<4){ imgsays.push(chunk[i]) } } if(isStart){ ws.write(chunk.slice(start , end)); } }); req.on("end",function(){ ws.end(); console.log("保存"+filename+"成功"); res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8'}); res.end('<div id="path">'+path+'</div>'); });
原理差很少,對每次接收的buffer段進行判斷,當通過四個\r\n後分析文件報頭獲取文件類型,建立一個寫入流,而且開始寫入,同時加上對是否到了數據尾部判斷,數據尾部會跟着一個\r\n,若是到了尾部,則過濾掉尾部的信息。
如此一來,上傳的文件就不會由於太大而把內存撐爆了。
附上github地址:https://github.com/whxaxes/node-test/tree/master/server/upload 有興趣的能夠down下來
本人前端小菜,如有不當之處請指正。