nodejs學習之文件上傳

  最近要作個圖片上傳的需求,由於服務端春節請假回家還沒來,因此就我本身先折騰了一下,大概作出來個效果,後臺就用了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下來

  本人前端小菜,如有不當之處請指正。

相關文章
相關標籤/搜索