NodeJS - 使用核心NodeJS和原始Node解决方案实现文件上传进度条

22

Ryan Dahl曾表示他发明NodeJS是为了解决文件上传进度条问题 (https://youtu.be/SAc0vQCC6UQ)。在Node诞生于2009年时,使用当时可用的技术,比如Express和更先进的客户端JavaScript库不能自动提供进度更新。那么NodeJS究竟是如何解决这个问题的呢?

现在我试图仅使用核心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');
});

文件选择器.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++);
};

需要注意的一点是,尽管调用了response.write方法,但浏览器在收到足够的数据之前不会显示任何内容,正如这个问题所指出的:链接 - JohnnyFun
@JohnnyFun,那很有道理。我仍然很好奇Ryan是如何解决向浏览器发送进度更新的问题的,因为在链接中提到强制浏览器清除其缓冲区的解决方案并不是一个生产解决方案。 - HelpMeStackOverflowMyOnlyHope
这个问题引起了我的兴趣。我现在更加想深入了解网络是如何工作的。因此,我一直在学习C语言。 - Gilbert
2个回答

18
首先,你的代码确实可以工作;Node发送分块响应,但浏览器仅在等待更多内容之前才显示它。
更多信息请参见Node文档

第一次调用response.write()时,它将发送缓冲的头信息和第一个正文到客户端。第二次调用response.write()时,Node会假设您要流式传输数据,并单独发送数据。也就是说,响应会在第一个正文块之前缓冲。

如果您将content-type设置为html,如response.setHeader('Content-Type', 'text/html; charset=UTF-8');,这会使chrome呈现内容,但只有当我使用了一系列的set timeout调用并在其中使用了response.write调用时才起作用;当我尝试使用您的代码时,它仍然没有更新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请求。它是多任务的。在this video的14分钟处,他详细讲解了它的工作原理......甚至说:“这是它的工作方式......”。几分钟后,他提到了一种iframe技术,并区分了长轮询和简单ajax请求。他表示他想编写一个优化这些类型行为的服务器。
无论如何,在那些日子里,这是不常见的。大多数Web服务器软件只会处理一个请求。如果他们去数据库,调用Web服务,与文件系统交互或类似的操作,该进程将只是坐等完成,而不是在等待期间处理其他请求。
如果您想同时处理多个请求,则必须启动另一个线程或添加更多带有负载平衡器的服务器。
Node.js 利用非阻塞 IO 非常高效地使用主进程。尽管 Node 并非第一个这样做的,但它在非阻塞 IO 领域中的独特之处在于,所有默认方法都是异步的,而你必须调用 "sync" 方法才能做错误的事情。这有点强制用户做正确的事情。
此外,应该注意到之所以选择 JavaScript 是因为它已经是运行在事件循环中的一种语言;它被设计用来处理异步代码。你可以使用匿名函数和闭包,这使得异步操作更容易维护。
我还想提一下,使用 Promise 库也会使编写异步代码更加简洁。例如,看看 bluebirdjs--它有一个不错的 "promisify" 方法,可以将对象原型上具有回调签名 (function(error, params){}) 的函数转换为返回 Promise 的形式。

不确定你关于内容类型的问题(这取决于浏览器),但我编辑了我的答案,表明这可能不是在生产环境中应该完成的任务。相反,请使用WebSockets /长轮询。(有关更多信息,请参见我的编辑) - JohnnyFun
为了完全回答我的问题,我想知道如何编辑我的代码示例以使用长轮询,如果那是他当时用来创建浏览器进度条的技术。不过你的解释很有道理,并且在思考这个问题时非常有帮助。 - HelpMeStackOverflowMyOnlyHope
嗯,说实话我应该做更多的研究。我在stackoverflow上还很新,我应该删除我的答案,这样对其他人来说就更清楚这个问题还没有得到解答吗? - JohnnyFun
不太理解status是做什么的,但是剩下的对我帮助很大,谢谢! - Obzzen
不错的交易,伙计。status只是用来告诉前端代码服务器在处理过程中进行到哪个阶段的。 - JohnnyFun
显示剩余6条评论

1
Node因其单线程事件循环而更擅长解决此上传问题。http事件处理程序中的代码可以轻松访问其他事件处理程序使用的内存。在传统的Web服务器环境中,主守护进程会启动工作线程来处理请求。我想,在传统的多线程模型中,难以检查文件上传状态,因为客户端需要向服务器发出新的调用,询问“文件进度是什么?”然后由完全独立的线程处理。现在,这个新线程需要与当前正在运行的上传线程通信。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接