2014-05-10 34 views
0

作爲我的第一個Node.js項目,我一直在爲我的工作構建報告應用程序,用戶可以在其中搜索並將結果以CSV格式下載到計算機中。Heroku上的Node.js + Socket.IO - 文件正在下載

爲了實現這一點,我一直在使用Socket.IO將JSON數據傳遞迴我的application.js文件上的一個按鈕單擊事件。從那裏我使用json2csv模塊格式化數據。

這裏就是我遇到的問題...

  1. 我知道Heroku的使用短暫的文件存儲(這應該是罰款,因爲我只需要的文件是服務器上的會話反正和補充清理是好的),但我檢查文件是否存在回來陽性,即使我不能看到該文件,當我運行

    heroku run bash 
    ls 
    
  2. 由於我使用的Socket.IO(據我所知,無論如何)正常的請求和響應回調函數參數不可用。我可以使用data.setHeader()這是套接字函數回調而不是response.setHeader()來設置CSV的標題嗎?我是否需要從套接字中分離出事件監聽器,並直接從app.get運行它?

    下面的代碼我有一個從它根據我的搜索事件和格式採用JSON數據:

    socket.on('export', function (data) { 
        jsoncsv({data: data, fields: ['foo', 'bar'], fieldNames: ['Foo', 'Bar']}, function(err, csv) { 
         if (err) console.log(err); 
         fs.writeFile('file.csv', csv, function(err) { 
          if (err) console.log(err); 
          console.log('File Created'); 
         }); 
         fs.exists('file.csv', function (err) { 
          if (err) console.log(err); 
          console.log('File Exists, Starting Download...'); 
          var file = fs.createReadStream('file.csv'); 
          file.pipe(data); 
          console.log('File Downloaded'); 
         }); 
        }); 
    }); 
    

UPDATE

下面是實際的客戶端代碼我用於構建和發送JSON作爲事件。確切的事件是$('#export').on('click', function() {});

server.on('listTags', function (data) { 
    var from = new Date($('#from').val()), to = new Date($('#to').val()), csvData = []; 
    var table = $('<table></table>'); 
    $('#data').empty().append(table); 
    table.append('<tr>'+ 
        '<th>Id</th>' + 
        '<th>First Name</th>' + 
        '<th>Last Name</th>' + 
        '<th>Email</th>' + 
        '<th>Date Tag Applied</th>' + 
        '</tr>'); 
    $.each(data, function(i, val) { 
     var dateCreated = new Date(data[i]['DateCreated']); 
     if (dateCreated >= from && dateCreated <= to) { 
      data[i]['DateCreated'] = dateCreated.toLocaleString(); 
      var tableRow = 
      '<tr>' + 
       '<td>' + data[i]['ContactId'] + '</td>' + 
       '<td>' + data[i]['Contact.FirstName'] + '</td>' + 
       '<td>' + data[i]['Contact.LastName'] + '</td>' + 
       '<td>' + data[i]['Contact.Email'] + '</td>' + 
       '<td>' + data[i]['DateCreated'] + '</td>' + 
      '</tr>'; 
      table.append(tableRow); 
      csvData.push(data[i]); 
     } 
    }); 
    $('.controls').html('<p><button id="export">Export '+ csvData.length +' Records</button></p>'); 
    $('#export').on('click', function() { 
     server.emit('export', csvData); 
    }); 
}); 
+0

您可以使用http服務器(使用http也可以將搜索查詢發送到服務器)更容易地使用響應頭。作爲一個小貼士:不是創建一個文件,然後從它創建一個讀取流,爲什麼不直接創建一個流,將其傳遞給'data'並將它寫入csv變量? 'var stream = new stream.Writeable(); stream.pipe(數據); stream.write(csv,'utf8',function(){console.log('done')});'[reference](http://nodejs.org/api/stream.html#stream_new_stream_writable_options) – MarijnS95

+0

Thanks for the小費,我不知道這是可能的,所以我會試一試。我是否還需要設置文件的標題?否則,我在開發週期的這個階段使用Socket.IO,儘管我不確定這是否是我需要的。 – ventismith

+0

你如何處理客戶端的下載?如果您使用正確的MIME類型發送文件(在您的案例中爲'text/csv'),瀏覽器就會喜歡它。如果你只是運行一個簡單的網頁,並且如果你的服務器已經有一個http服務器來處理該網頁,我建議使用HTTP。將流傳輸到客戶端非常容易,您還可以對其進行編碼(以節省帶寬)。 – MarijnS95

回答

0

當您指出自己Heroku的文件系統可能有點棘手。 我可以幫你解決你的問題(1),那就是你沒有連接到相同的你的應用程序運行的虛擬機(dyno)。運行heroku run bash時,您會找到一個乾淨的文件系統,其中包含應用程序運行所需的文件,並且run命令正在運行(與您在Procfile中指定的web進程相反)。

當您考慮使用Heroku的優勢之一是您可以在需要時輕鬆地從1節點擴展到幾個節點時,這是有道理的。但是,如果有10個網絡節點與您的代碼一起運行,您仍然希望heroku run bash以相同的方式工作。你應該連接哪一個? :)

有關更多詳細信息,請參閱https://devcenter.heroku.com/articles/one-off-dynos#an-example-one-off-dyno

希望這是有幫助的。 祝你好運!

/意志

0

因此,而不是使用socket.io的,我們只是將使用一個HTTP服務器。我有很多代碼給你,因爲它被部分剝離了我自己的http服務器,並且當然也應該提供文件(例如,你的html,css和js文件)。

var http = require('http'), 
    url = require('url'), 
    fs = require('fs'), 
    path = require('path'); 
var server = http.createServer(function (req, res) { 
    var location = path.join(__dirname, url.parse(req.url).pathname), 
     ext = location.split('.').slice(-1)[0]; 
    if (req.headers['request-action']&&req.headers['request-action'] == 'export'&&req.headers['request-data']) { //this is your export event 
     jsoncsv({data: req.headers['request-data'], fields: ['foo', 'bar'], fieldNames: ['Foo', 'Bar']}, function(err, csv) { 
      if (err){ 
       console.log(err); 
       res.writeHead(404, {'content-type':'text/plain'}); 
       res.end('Error at jsoncsv function: ', err); 
       return; 
      } 
      res.setHeader('content-type', 'text/csv'); 
      var stream = new stream.Writeable(); 
      compressSend(req, res, stream); //this is the equivalent of stream.pipe(res), but with an encoding system inbetween to compress the data 
      stream.write(csv, 'utf8', function(){ 
       console.log('done writing csv to stream'); 
      }); 
     }); 
    } else {//here we handle normal files 
     fs.lstat(location, function(err, info){ 
      if(err||info.isDirectory()){ 
       res.writeHead(404, {'content-type':'text/plain'}); 
       res.end('404 file not found'); 
       console.log('File '+location+' not found'); 
       return; 
      } 
      //yay, the file exists 
      var reader = fs.createReadStream(location); // this creates a read stream from a normal file 
      reader.on('error', function(err){ 
       console.log('Something strange happened while reading: ', err); 
       res.writeHead(404, {'content-type':'text/plain'}); 
       res.end('Something strange happened while reading'); 
      }); 
      reader.on('open', function(){ 
       res.setHeader('Content-Type', getHeader(ext)); //of course we should send the correct header for normal files too 
       res.setHeader('Content-Length', info.size); //this sends the size of the file in bytes 
       //the reader is now streaming the data 
       compressSend(req, res, reader); //this function checks whether the receiver (the browser) supports encoding and then en200s it to that. Saves bandwidth 
      }); 
      res.on('close', function(){ 
       if(reader.fd) //we shall end the reading of the file if the connection is interrupted before streaming the whole file 
        reader.close(); 
      }); 
     }); 
    } 
}).listen(80); 
function compressSend(req, res, input){ 
    var acceptEncoding = req.headers['Accept-Encoding']; 
    if (!acceptEncoding){ 
     res.writeHead(200, {}); 
     input.pipe(res); 
    } else if (acceptEncoding.match(/\bgzip\b/)) { 
     res.writeHead(200, { 'Content-Encoding': 'gzip' }); 
     input.pipe(zlib.createGzip()).pipe(res); 
    } else if (acceptEncoding.match(/\bdeflate\b/)) { 
     res.writeHead(200, { 'Content-Encoding': 'deflate' }); 
     input.pipe(zlib.createDeflate()).pipe(res); 
    } else { 
     res.writeHead(200, {}); 
     input.pipe(res); 
    } 
} 
function getHeader(ext){ 
    ext = ext.toLowerCase(); 
    switch(ext) { 
     case 'js': header = 'text/javascript'; break; 
     case 'html': header = 'text/html'; break; 
     case 'css': header = 'text/css'; break; 
     case 'xml': header = 'text/xml'; break; 
     default: header = 'text/plain'; break; 
    } 
    return header; 
} 

最上面的部分對你很有意思,特別是在第一個if裏面。在那裏它檢查標題request-action是否存在。此標題將包含您的活動名稱(如名稱export)。標頭request-data包含您將通過套接字發送的數據。現在你可能也想知道如何管理這個客戶端:

$('#export').on('click', function() { 
    var xhr = new XMLHttpRequest(); 
    xhr.open('GET', 'localhost'); 
    xhr.setRequestHeader('request-action', 'export'); //here we set that 'event' header, so the server knows what it should do 
    xhr.setRequestHeader('request-data', 'csvData); //this contains the data that has to be sent to the server 
    xhr.send(); 
    xhr.onloadend = function(){//we got all the data back from the server 
     var file = new Blob([xhr.response], {type: 'text/csv'}); //xhr.response contains the data. A blob wants the data in array format so that is why we put it in the brackets 
     //now the download part is tricky. We first create an object URL that refers to the blob: 
     var url = URL.createObjectURL(file); 
     //if we just set the window.location to this url, it downloads the file with the url as name. we do not want that, so we use a nice trick: 
     var a = document.createElement('a'); 
     a.href = url; 
     a.download = 'generatedCSVFile.csv' //this does the naming trick 
     a.click(); //simulate a click to download the file 
    } 
}); 

我試圖在關鍵部分添加註釋。因爲我不知道你目前的知識水平,所以我沒有在系統的每個部分都添加評論,但是如果有什麼不清楚的話,可以隨意問。