nodejs學習之文件上傳


  最近要做個圖片上傳的需求,因為服務端春節請假回家還沒來,所以就我自己先折騰了一下,大概做出來個效果,后台就用了nodejs,剛開始做的時候想網上找一下資料,發現大部分資料都是用node-formidable插件實現上傳的。但是自己又想手動實現一下,所以就開始折騰了。寫此博文也就是做個記錄。

  先大概整理一下整個思路,自己想要實現的效果是能夠在頁面上無刷新上傳一個圖片並且顯示(后來做着做着就變成所有文件的上傳了,不過都一個樣)。

  在前端部分,想要無刷新首先想到的是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代碼如下:

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事件來獲取后台返回的鏈接。以上代碼比較簡單,就不具體解釋了。

  接下來是后台的實現:

  首先先是要建個http server,然后,因為有兩個頁面,再加上還有文件下載之類的,所以先弄個最簡單的路由:

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過來的數據的處理,常用的辦法就是:

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下來

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM