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



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

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

这就是我遇到问题的地方…

  1. 我知道Heroku使用临时文件存储(这应该很好,因为我只需要文件在服务器上进行会话,并且添加的清理很好),但是我的文件存在检查返回阳性,即使我在运行

    时看不到文件
    heroku run bash
    ls
    
  2. 因为我使用Socket。IO(无论如何,据我所知)正常的请求和响应回调函数参数不可用。我可以使用data.setHeader()设置CSV的头,这是套接字函数回调而不是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的优点之一是您可以在需要时轻松地从一个节点扩展到多个节点时,这是有意义的。但是当你有10个web节点运行你的代码时,你仍然希望heroku run bash以同样的方式工作。你应该连接到哪一个?:)

详情见https://devcenter.heroku.com/articles/one-off-dynos#an-example-one-off-dyno

希望这是有帮助的。祝你好运!

/威利

所以不用socket。因此,我们将使用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(/bgzipb/)) {
        res.writeHead(200, { 'Content-Encoding': 'gzip' });
        input.pipe(zlib.createGzip()).pipe(res);
    } else if (acceptEncoding.match(/bdeflateb/)) {
        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
    }
});

我试图在关键部分添加注释。由于我不了解您目前的知识水平,所以我没有对系统的每个部分都添加评论,但如果有不清楚的地方请随时提问。