简体   繁体   English

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

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

As my first Node.js project, I've been building a reporting app for my work where people can search and then download the results to their computer as a CSV. 作为我的第一个Node.js项目,我一直在为自己的工作构建一个报告应用程序,人们可以在其中搜索然后将结果以CSV格式下载到他们的计算机上。

To accomplish this, I've been using Socket.IO to pass the JSON data back to my application.js file on a button click event. 为此,我一直使用Socket.IO在按钮单击事件上将JSON数据传递回我的application.js文件。 From there I use the json2csv module to format the data. 从那里,我使用json2csv模块格式化数据。

Here's where I run into issues... 这是我遇到问题的地方...

  1. I know Heroku uses Ephemeral File Storage (which should be fine since I only need the file to be on the server for the session anyway and the added cleanup is nice), however my file exists check comes back positive even though I can't see the file when I run 我知道Heroku使用临时文件存储(这应该很好,因为无论如何我只需要将该文件保存在会话中的服务器上,并且添加的清理工作就很好了),但是即使我看不到我的文件是否存在检查也返回正值我运行时的文件

     heroku run bash ls 
  2. Since I'm using Socket.IO (as far as I can tell, anyhow) the normal request and response callback function parameters aren't available. 由于我正在使用Socket.IO(据我所知,无论如何),正常的请求和响应回调函数参数不可用。 Can I set the headers for the CSV using data.setHeader() which is the socket function callback instead of response.setHeader() ? 我可以使用data.setHeader()设置CSV的标头data.setHeader() ,它是套接字函数回调而不是response.setHeader() Do I need to break out that event listener from the sockets and run it directly from the app.get ? 我是否需要从套接字中断开该事件侦听器并直接从app.get运行它?

    Here's the code I have that takes the JSON data from the event and formats it based on my searches: 这是我从事件中获取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 更新

Here's the actual client-side code I'm using to build and send the JSON as an event. 这是我用来构建和发送JSON作为事件的实际客户端代码。 The exact event is the $('#export').on('click', function () {}); 确切的事件是$('#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);
    });
});

As you point out yourself Heroku's file system can be a bit tricky. 正如您指出的那样,Heroku的文件系统可能有点棘手。 I can help with your question (1), and that is that you are not connected to the same virtual machine (dyno) that your application is running from. 我可以为您的问题(1)提供帮助,即您没有连接到应用程序运行所在的同一虚拟机(dyno)。 When you run heroku run bash you come to a clean filesystem with the files that are needed for your app to run, and the run command is running (as opposed to the web process you have specified in your Procfile). 当您运行heroku run bash您会进入一个干净的文件系统,其中包含应用程序运行所需的文件,并且run命令正在运行(与您在Procfile中指定的web进程相对)。

This makes sense when you consider that one of the advantages using Heroku is that you can easily scale up from 1 node to several when needed. 当您考虑到使用Heroku的优势之一是可以在需要时轻松地从1个节点扩展到几个节点时,这很有意义。 But you would still expect heroku run bash to work the same way when you have 10 web nodes running with your code. 但是,当您有10个Web节点运行代码时,您仍然希望heroku run bash能够以相同的方式工作。 Which one should you then be connected to? 您应该连接到哪一个? :) :)

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

Hope this is helpful. 希望这会有所帮助。 Best of luck! 祝你好运!

/Wille /威乐

So instead of using socket.io, we are just going to use an http server. 因此,我们将不使用socket.io,而是使用http服务器。 I have a lot of code for you, as it is partially stripped of my own http server, and should of course also serve files (eg. your html, css and js files). 我有很多代码可供您使用,因为它已部分剥离了我自己的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;
}

The top part is interesting for you, especially inside the first if. 顶部对您来说很有趣,尤其是在第一个if内部。 There it checks if the header request-action is present. 在那里检查标题request-action是否存在。 This header will contain your event name (like the name export ). 该标题将包含您的事件名称(例如名称export )。 The header request-data contains the data you would otherwise send over the socket. 标头request-data包含您将通过套接字发送的数据。 Now you might also want to know how to manage this client side: 现在,您可能还想知道如何管理此客户端:

$('#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
    }
});

I have tried to add comments on the crucial parts. 我试图在关键部分添加评论。 Because I do not know your current knowledge level, I have not added a comment on every part of the system, but if anything is unclear feel free to ask. 因为我不知道您当前的知识水平,所以我没有在系统的每个部分添加评论,但是如果有任何不清楚的地方,请随时提出。

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

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