NodeJS - 使用 Core NodeJS 和原始 Node 解决方案的带有进度条的文件上传
NodeJS - file upload with progress bar using Core NodeJS and the original Node solution
Ryan Dahl 曾表示他发明了 NodeJS 来解决文件上传进度条问题 (https://youtu.be/SAc0vQCC6UQ)。使用 2009 年引入 Node 时可用的技术,所以在 Express 和更高级的 client-side javascript 自动告诉您更新进度的库之前,NodeJS 究竟是如何解决这个问题的?
现在尝试只使用 Core NodeJS,我了解请求流,我可以查看 header,获取总文件大小,然后获取每个数据块的大小,告诉我完成的百分比。但是后来我不明白如何将这些进度更新流式传输回浏览器,因为浏览器似乎直到 request.end().
才更新
我想再一次总结一下 NodeJS 最初是如何解决这个进度更新问题的。 WebSockets 还没有出现,所以你不能只打开到客户端的 WebSocket 连接并将进度更新流式传输回浏览器。是否使用了另一种 client-side javascript 技术?
这是我到目前为止的尝试。进度更新会流式传输到 server-side 控制台,但浏览器只会在响应流收到 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 Documentation:
The first time response.write() is called, it will send the buffered
header information and the first body to the client. The second time
response.write() is called, Node assumes you're going to be streaming
data, and sends that separately. That is, the response is buffered up
to the first chunk of body.
如果像 response.setHeader('Content-Type', 'text/html; charset=UTF-8');
一样将内容类型设置为 html,它会使 chrome 呈现内容,但这只有在我使用一系列设置超时调用时才起作用内部调用 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 分钟后谈论它——甚至说,"So here's how it works..."。几分钟后,他提到了一种 iframe 技术,并将长轮询与简单的 ajax 请求区分开来。他说他想编写一个针对这些类型的行为进行优化的服务器。
不管怎么说,这在当时是不常见的。大多数网络服务器软件一次只能处理一个请求。如果他们访问数据库、调用 Web 服务、与文件系统交互或类似的事情,进程将只是等待它完成,而不是在等待时处理其他请求。
如果您想同时处理多个请求,则必须启动另一个线程或使用负载平衡器添加更多服务器。
另一方面,Nodejs 通过执行非阻塞 IO 来非常有效地利用主进程。 Node 不是第一个这样做的,但它在非阻塞 IO 领域的与众不同之处在于它的所有默认方法都是异步的,您必须调用 "sync" 方法来执行 错误 事情。它有点强迫用户做正确的事情。
此外,需要注意的是,选择javascript的原因是因为它已经是一种在事件循环中运行的语言;它是 制作的 来处理异步代码。您可以拥有匿名函数和闭包,这使得异步操作更易于维护。
我还想提一下,使用 promise 库还可以使编写异步代码更加简洁。例如,查看 bluebirdjs——它有一个很好的 "promisify" 方法,可以将对象原型上具有回调签名 (function(error, params){}) 的函数转换为 return一个承诺。
Node 由于其 单线程 事件循环而更擅长解决此上传问题。 http 事件处理程序中的代码可以轻松访问其他事件处理程序使用的内存。在传统的 Web 服务器环境中,主守护进程启动工作线程来处理请求。我想,在传统的线程模型中,很难检查文件上传状态,因为客户端需要重新调用服务器询问“文件进度是多少?”然后将由一个完全独立的线程处理。该新线程现在需要与当前 运行 上传线程通信。
Ryan Dahl 曾表示他发明了 NodeJS 来解决文件上传进度条问题 (https://youtu.be/SAc0vQCC6UQ)。使用 2009 年引入 Node 时可用的技术,所以在 Express 和更高级的 client-side javascript 自动告诉您更新进度的库之前,NodeJS 究竟是如何解决这个问题的?
现在尝试只使用 Core NodeJS,我了解请求流,我可以查看 header,获取总文件大小,然后获取每个数据块的大小,告诉我完成的百分比。但是后来我不明白如何将这些进度更新流式传输回浏览器,因为浏览器似乎直到 request.end().
才更新我想再一次总结一下 NodeJS 最初是如何解决这个进度更新问题的。 WebSockets 还没有出现,所以你不能只打开到客户端的 WebSocket 连接并将进度更新流式传输回浏览器。是否使用了另一种 client-side javascript 技术?
这是我到目前为止的尝试。进度更新会流式传输到 server-side 控制台,但浏览器只会在响应流收到 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 Documentation:
The first time response.write() is called, it will send the buffered header information and the first body to the client. The second time response.write() is called, Node assumes you're going to be streaming data, and sends that separately. That is, the response is buffered up to the first chunk of body.
如果像 response.setHeader('Content-Type', 'text/html; charset=UTF-8');
一样将内容类型设置为 html,它会使 chrome 呈现内容,但这只有在我使用一系列设置超时调用时才起作用内部调用 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 分钟后谈论它——甚至说,"So here's how it works..."。几分钟后,他提到了一种 iframe 技术,并将长轮询与简单的 ajax 请求区分开来。他说他想编写一个针对这些类型的行为进行优化的服务器。
不管怎么说,这在当时是不常见的。大多数网络服务器软件一次只能处理一个请求。如果他们访问数据库、调用 Web 服务、与文件系统交互或类似的事情,进程将只是等待它完成,而不是在等待时处理其他请求。
如果您想同时处理多个请求,则必须启动另一个线程或使用负载平衡器添加更多服务器。
另一方面,Nodejs 通过执行非阻塞 IO 来非常有效地利用主进程。 Node 不是第一个这样做的,但它在非阻塞 IO 领域的与众不同之处在于它的所有默认方法都是异步的,您必须调用 "sync" 方法来执行 错误 事情。它有点强迫用户做正确的事情。
此外,需要注意的是,选择javascript的原因是因为它已经是一种在事件循环中运行的语言;它是 制作的 来处理异步代码。您可以拥有匿名函数和闭包,这使得异步操作更易于维护。
我还想提一下,使用 promise 库还可以使编写异步代码更加简洁。例如,查看 bluebirdjs——它有一个很好的 "promisify" 方法,可以将对象原型上具有回调签名 (function(error, params){}) 的函数转换为 return一个承诺。
Node 由于其 单线程 事件循环而更擅长解决此上传问题。 http 事件处理程序中的代码可以轻松访问其他事件处理程序使用的内存。在传统的 Web 服务器环境中,主守护进程启动工作线程来处理请求。我想,在传统的线程模型中,很难检查文件上传状态,因为客户端需要重新调用服务器询问“文件进度是多少?”然后将由一个完全独立的线程处理。该新线程现在需要与当前 运行 上传线程通信。