繁体   English   中英

Heroku上的Node.js + Socket.IO-文件下载

[英]Node.js + Socket.IO on Heroku - File Downloading

作为我的第一个Node.js项目,我一直在为自己的工作构建一个报告应用程序,人们可以在其中搜索然后将结果以CSV格式下载到他们的计算机上。

为此,我一直使用Socket.IO在按钮单击事件上将JSON数据传递回我的application.js文件。 从那里,我使用json2csv模块格式化数据。

这是我遇到问题的地方...

  1. 我知道Heroku使用临时文件存储(这应该很好,因为无论如何我只需要将该文件保存在会话中的服务器上,并且添加的清理工作就很好了),但是即使我看不到我的文件是否存在检查也返回正值我运行时的文件

     heroku run bash ls 
  2. 由于我正在使用Socket.IO(据我所知,无论如何),正常的请求和响应回调函数参数不可用。 我可以使用data.setHeader()设置CSV的标头data.setHeader() ,它是套接字函数回调而不是response.setHeader() 我是否需要从套接字中断开该事件侦听器并直接从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'); }); }); }); 

更新

这是我用来构建和发送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);
    });
});

正如您指出的那样,Heroku的文件系统可能有点棘手。 我可以为您的问题(1)提供帮助,即您没有连接到应用程序运行所在的同一虚拟机(dyno)。 当您运行heroku run bash您会进入一个干净的文件系统,其中包含应用程序运行所需的文件,并且run命令正在运行(与您在Procfile中指定的web进程相对)。

当您考虑到使用Heroku的优势之一是可以在需要时轻松地从1个节点扩展到几个节点时,这很有意义。 但是,当您有10个Web节点运行代码时,您仍然希望heroku run bash能够以相同的方式工作。 您应该连接到哪一个? :)

有关更多详细信息,请参见https://devcenter.heroku.com/articles/one-off-dynos#an-example-one-off-dyno

希望这会有所帮助。 祝你好运!

/威乐

因此,我们将不使用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
    }
});

我试图在关键部分添加评论。 因为我不知道您当前的知识水平,所以我没有在系统的每个部分添加评论,但是如果有任何不清楚的地方,请随时提出。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM