NodeJS -使用Core NodeJS和原始Node解决方案上传带有进度条的文件

NodeJS - file upload with progress bar using Core NodeJS and the original Node solution

本文关键字:NodeJS 文件 使用 Core Node 原始 解决方案      更新时间:2023-09-26

Ryan Dahl说他发明NodeJS是为了解决文件上传进度条的问题(https://youtu.be/SAc0vQCC6UQ)。使用2009年引入Node时可用的技术,所以在Express和更高级的客户端javascript库自动告诉您进度更新之前,NodeJS究竟是如何解决这个问题的?

现在尝试使用Core NodeJS,我理解请求流,我可以查看头,获得总文件大小,然后获得每个数据块的大小,因为它通过,告诉我完成的百分比。但是我不明白如何将这些进度更新流回浏览器,因为浏览器似乎直到request.end()才更新。

我想再一次围绕NodeJS最初是如何解决这个进度更新问题的。WebSocket还没有出现,所以你不能只是打开一个WebSocket连接到客户端,然后将进度更新流回浏览器。是否使用了其他客户端javascript技术?

这是我到目前为止的尝试。进度更新流传输到服务器端控制台,但是浏览器只有在响应流接收到response.end()后才会更新。

var http = require('http');
var fs = require('fs');
var server = http.createServer(function(request, response){
    response.writeHead(200);
    if(request.method === 'GET'){
        fs.createReadStream('filechooser.html').pipe(response);     
    }
    else if(request.method === 'POST'){
        var outputFile = fs.createWriteStream('output');
        var total = request.headers['content-length'];
        var progress = 0;
        request.on('data', function(chunk){
            progress += chunk.length;
            var perc = parseInt((progress/total)*100);
            console.log('percent complete: '+perc+'%'n');
            response.write('percent complete: '+perc+'%'n');
        });
        request.pipe(outputFile);
        request.on('end', function(){
            response.end(''nArchived File'n'n');
        });
    }
});
server.listen(8080, function(){
    console.log('Server is listening on 8080');
});

filechooser.html:

<!DOCTYPE html>
<html>
<body>
<form id="uploadForm" enctype="multipart/form-data" action="/" method="post">
    <input type="file" id="upload" name="upload" />
    <input type="submit" value="Submit">
</form>
</body>
</html>

这是一个更新的尝试。浏览器现在显示进度更新,但我很确定这不是Ryan Dahl最初为生产场景提出的实际解决方案。他使用长时间投票了吗?解是什么样的呢?

var http = require('http');
var fs = require('fs');
var server = http.createServer(function(request, response){
    response.setHeader('Content-Type', 'text/html; charset=UTF-8');
    response.writeHead(200);
    if(request.method === 'GET'){
        fs.createReadStream('filechooser.html').pipe(response);     
    }
    else if(request.method === 'POST'){
        var outputFile = fs.createWriteStream('UPLOADED_FILE');
        var total = request.headers['content-length'];
        var progress = 0;
        response.write('STARTING UPLOAD');
        console.log(''nSTARTING UPLOAD'n');
        request.on('data', function(chunk){
            fakeNetworkLatency(function() {
                outputFile.write(chunk);
                progress += chunk.length;
                var perc = parseInt((progress/total)*100);
                console.log('percent complete: '+perc+'%'n');
                response.write('<p>percent complete: '+perc+'%');
            });
        });
        request.on('end', function(){
            fakeNetworkLatency(function() {
                outputFile.end();
                response.end('<p>FILE UPLOADED!');
                console.log('FILE UPLOADED'n');
            });
        });
    }
});
server.listen(8080, function(){
    console.log('Server is listening on 8080');
});
var delay = 100; //delay of 100 ms per chunk
var count =0;
var fakeNetworkLatency = function(callback){
    setTimeout(function() {
        callback();
    }, delay*count++);
};

首先,你的代码确实工作;Node发送分块响应,但浏览器只是在等待更多的响应,然后才懒得显示它。

更多信息见Node Documentation:

第一次调用response.write()时,它将发送缓冲的数据头信息和第一个主体发送给客户端。第二次response.write()被调用时,Node假定您将进行流处理数据,并分别发送。也就是说,响应被缓冲起来了到body的第一个块

如果您将content-type设置为html,如response.setHeader('Content-Type', 'text/html; charset=UTF-8');,它会使chrome渲染内容,但这只在我使用一系列设置超时调用响应时才有效。在内部写入调用;当我尝试使用你的代码时,它仍然没有更新dom,所以我深入挖掘…

问题在于,它实际上是由浏览器在它认为合适的时候呈现内容,所以我设置了代码来发送ajax请求来检查状态:

首先,我更新了服务器,简单地将其状态存储在一个全局变量中,并打开"checkstatus"端点来读取它:

var http = require('http');
var fs = require('fs');
var status = 0;
var server = http.createServer(function (request, response) {
    response.writeHead(200);
    if (request.method === 'GET') {
        if (request.url === '/checkstatus') {
            response.end(status.toString());
            return;
        }
        fs.createReadStream('filechooser.html').pipe(response);
    }
    else if (request.method === 'POST') {
        status = 0;
        var outputFile = fs.createWriteStream('output');
        var total = request.headers['content-length'];
        var progress = 0;
        request.on('data', function (chunk) {
            progress += chunk.length;
            var perc = parseInt((progress / total) * 100);
            console.log('percent complete: ' + perc + '%'n');
            status = perc;
        });
        request.pipe(outputFile);
        request.on('end', function () {
            response.end(''nArchived File'n'n');
        });
    }
});
server.listen(8080, function () {
    console.log('Server is listening on 8080');
});

然后,我更新了filechooser.html以使用ajax请求检查状态:

<!DOCTYPE html>
<html>
<body>
<form id="uploadForm" enctype="multipart/form-data" action="/" method="post">
    <input type="file" id="upload" name="upload"/>
    <input type="submit" value="Submit">
</form>
Percent Complete: <span id="status">0</span>%
</body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script>
    var $status = $('#status');
    /**
     * When the form is submitted, begin checking status periodically.
     * Note that this is NOT long-polling--that's when the server waits to respond until something changed. 
     * In a prod env, I recommend using a websockets library with a long-polling fall-back for older broswers--socket.io is a gentleman's choice)
     */
    $('form').on('submit', function() {
        var longPoll = setInterval(function () {
            $.get('/checkstatus').then(function (status) {
                $status.text(status);
                //when it's done, stop annoying the server
                if (parseInt(status) === 100) {
                    clearInterval(longPoll);
                }
            });
        }, 500);
    });
</script>
</html>

请注意,尽管我没有结束响应,服务器仍然能够处理传入的状态请求。

所以回答你的问题,Dahl被他看到的一个flickr应用程序所吸引,该应用程序上传了一个文件并长时间投票检查它的状态。他感兴趣的原因是,服务器能够在继续上传的同时处理这些ajax请求。这是多重任务。看他在这个视频的14分钟后谈论它——甚至说,"这是它的工作原理……"几分钟后,他提到了iframe技术,并将长轮询与简单的ajax请求区分开来。他说他想编写一个针对这些类型的行为进行优化的服务器。

无论如何,这在当时是不常见的。大多数web服务器软件一次只能处理一个请求。如果它们访问数据库,调用web服务,与文件系统交互,或者类似的事情,进程将只是坐在那里等待它完成,而不是在等待期间处理其他请求。

如果你想同时处理多个请求,你必须启动另一个线程或添加更多的服务器与负载均衡器。

另一方面,Nodejs通过进行非阻塞IO来非常有效地利用主进程。Node并不是第一个这样做的,但它在非阻塞IO领域的不同之处在于,它的所有默认方法都是异步的,你必须调用"sync"方法来做错误的事情。它迫使用户做正确的的事情。

另外,应该指出的是,选择javascript的原因是因为它已经是一种运行在事件循环中的语言;是用来处理异步代码的。你可以使用匿名函数和闭包,这使得异步操作更容易维护。

我还想提一下,使用promise库还可以使编写异步代码更加简洁。例如,看看bluebirdjs——它有一个很好的"promisify"方法,可以将对象原型上具有回调签名(function(error, params){})的函数转换为返回一个承诺。

Node更擅长解决这个上传问题,因为它的单线程事件循环。http事件处理程序中的代码可以很容易地访问其他事件处理程序使用的内存。在传统的web服务器环境中,主守护进程启动工作线程来处理请求。我可以想象,在传统的线程模型中,检查文件上传状态是很困难的,因为客户端需要重新调用服务器,询问"文件进度如何?"然后由一个完全独立的线程处理。这个新线程现在需要与当前正在运行的上传线程通信。