Nodejs 获取文件上传进度

Nodejs 获取文件上传进度

国庆节对于一个大三党来说是一整块难得的时间,出去玩也好,做其他的事也好,都可以随心所欲地安排。想着利用这个时间开发 一个社交网站作为学习node的一个总结,不怎么精,也算凑合,无奈还有太多的事儿要做啊。废话不多说,转入正题:

今天遇到图片上传的问题,服务器能成功收到图片信息,但如何将进度返给客户端呢?用 <form method=‘post’ action="" target=“hidden- iframe” enctype=‘multipart/form-data’></form>发送文件是一个post请求,服务器只可能回复一次,而且这个回复主要用来回复前端上传 文件的获取地址,不适合再做他用。

网上查了查,说是可以用flash做,好吧,表示不会flash… 目前主要的问题是如何不断地从服务器获得上传进度,socket貌似能做到这一点,问题是不知道上传结束时客户端如何主动断开socket链接,有知道的朋友麻烦告诉一声^-^,于是决定采用轮询了。 先贴前端部分代码: html部分

      <iframe name="hidden_iframe" id='hidden_iframe' style="display:none" src=""> </iframe>
      <form method='post' action='/uploadimage' enctype='multipart/form-data' target="hidden_iframe">
               <input type="text" name="randomID" id="uploadTag" style="display:none" />
               选择图片:<input type='file' name='img' id='filearea' onchange="setTag()"/>
               <input type='submit' value='提交' onclick=" startRequest()" />
     </form>
	 <div id="showbox" ></div>

JS部分

       var uploadTag;
	   function startRequest()
	   {
             var json={"randomID":uploadTag};
			setTimeout(post_data(obj,'/getuploadprocess',dealResponse),1000);
	   }
	   function dealResponse(json)
	   {
	     var box=document.getElementById("showbox");
		 box.innerText=json.progress+'%';
		 if(json.progress!=100)
		 {
		   startRequest();
		 }
	   }   

//上面这俩个function 组成一个不达目的不罢休的轮询,post_data是个ajax,就不贴了

	   function setTag()
	   {
	       uploadTag=Math.ceil(Math.random()*1000000);
	       var tag=document.getElementById("uploadTag");
	       tag.value=uploadTag;
	   }

//设置随机id,避免搞不清是请求哪个文件的进度

	   function uploadErr(message)
	   {
	      alert(message);
	   }

//处理错误,当然这里只是简单alert,没有考虑用户体验,就写简单点吧

	   function uploadEnd(json)
	   {
	     var obj=JSON.parse(json)
	     alert(obj.message)
	   }

//上传结束的callback,返回图片的获取地址,前端应该将其设置成一个img元素的src属性,也从简了:)
** 后端:** 后端大部分人都是使用的express,然后就顺便设置了app.use(multer(dest:""))解析请求,那可就做不了进度条了,建议把这个设置删了,然后 var formidable=require(“formidable”);(写在路由模块里); 代码:

	   var uploadprogress={};//全局变量,存进度
	   module.exports=function(app)
	   {
	   ...........................................................................
	   ...........................................................................
	     app.post('/uploadimage',function(req,res){
	         var id=1;
	         var uploadfile;
	         var form=new formidable.IncomingForm();//formidable模块的用法自己去github看文档吧
	         form.uploadDir='';
	         from.keepExtensions=true;
	         form.on("field",function(name,value){//设置唯一可标识的id
	         id=req.session.user._id+req.body.randomID;//这个value就是表单中文本框的值
	   });
	         form.on("err",function(err){
	           var callback="<script>top.uploadErr('"+err+"');</script>";
	            res.end(callback);//这段文本发回前端就会被同名的函数执行
	   })
	          from.on("progress",function(bytesRecieved,bytesExpected){
	             uploadprogress.id=Math.ceil((bytesRecieved/bytesExpected)*100);//更新进度
	          //这里有个id的值是否已经设置的问题,不过没关系,就算仍是最初的1,也只耗一丁点内存
	   })
	          form.on("abort",function(){
	            ............
	   })
	          form.on("file",function(err,file){
	              if(err)
	             {
	                  console.log(err);
                       var callback="<script>top.uploadErr('"+err+"');</script>";
	                   res.end(callback);			
					}
	             uploadfile=file;//记录文件信息
	   })
	         form.on("end",function(){
	             var file=new File()
	             .......
	             ...... 
	            file.save();//将文件信息存入数据库,如果是文章里面的图片的话,就没有必要存了,在前端直接设置src就相当于存了
	            var fpath=uploadfile.path;
	            var callback="<script>top.uploadEnd('"+fpath+"');</script>";
	            res.end(callback);
	   })
	   		 form.parse(req);
	   })
   app.post('/getuploadprocess',function(req,res){
        var id=req.session.user._id+req.body.randomID;
        var pr;
        if(uploadprogress.id)
         {
           pr=uploadprogress.id;
         }
        else
       {
           pr=0;
       }
       if(pr==100)
       {
          delete uploadprogress.id;
       }
         res.status(200).json({"progress":pr});
   })
   }

ok主要的代码已经贴完了,下面来说一下工作的流程,首先由表单提交文件,在确定文件后会自动生成一个随机ID用于标识,这个ID发给后台和user的id组合为文件的标识;后台接受文件上传请求,formidable开始解析文件,主要监听progress事件,由其返回的参数来更新上传进度。在表单点击提交的时候同时触发startRequest函数,向后台获取上传进度,这个请求由/getuploadprogress处理,里面的代码就是简单的响应和清理功能。自此全部结束,前端获得进度后想要以什么方式展现就随看官发挥吧,我就简单展现其百分比了

PS:如果有大神路过,小弟请教一个问题:这个代码 是实现了功能,但获得进度是个长轮询,不知道对服务器来讲压力大不大。。测试时我同时上传俩个400多M的文件,电脑风扇就转个不停。虽然文件上传成功了,但还是担心性能问题(我用本机做的服务器),求大神留下高见!


12 回复

Node.js 获取文件上传进度

国庆节对于一个大三党来说是一整块难得的时间,出去玩也好,做其他的事也好,都可以随心所欲地安排。想着利用这个时间开发一个社交网站作为学习Node.js的一个总结,不怎么精,也算凑合,无奈还有太多的事儿要做啊。废话不多说,转入正题:

今天遇到图片上传的问题,服务器能成功收到图片信息,但如何将进度反馈给客户端呢?使用 <form method='post' action="" target="hidden-iframe" enctype='multipart/form-data'> 发送文件是一个POST请求,服务器只可能回复一次,而且这个回复主要用来回复前端上传文件的获取地址,不适合再做他用。

在网上查找了一些资料,有人说可以使用Flash实现,但是本人不会Flash… 目前主要的问题是如何不断从服务器获得上传进度,Socket似乎可以做到这一点,但是不知道上传结束时客户端如何主动断开Socket连接。于是决定采用轮询的方式。

前端部分代码

HTML 部分
<iframe name="hidden_iframe" id='hidden_iframe' style="display:none" src=""></iframe>
<form method='post' action='/uploadimage' enctype='multipart/form-data' target="hidden_iframe">
    <input type="text" name="randomID" id="uploadTag" style="display:none" />
    选择图片:<input type='file' name='img' id='filearea' onchange="setTag()"/>
    <input type='submit' value='提交' onclick="startRequest()" />
</form>
<div id="showbox"></div>
JS 部分
var uploadTag;

function startRequest() {
    var json = {"randomID": uploadTag};
    setTimeout(post_data(json, '/getuploadprocess', dealResponse), 1000);
}

function dealResponse(json) {
    var box = document.getElementById("showbox");
    box.innerText = json.progress + '%';
    if (json.progress != 100) {
        startRequest();
    }
}

function setTag() {
    uploadTag = Math.ceil(Math.random() * 1000000);
    var tag = document.getElementById("uploadTag");
    tag.value = uploadTag;
}

function uploadErr(message) {
    alert(message);
}

function uploadEnd(json) {
    var obj = JSON.parse(json);
    alert(obj.message);
}

后端部分代码

后端大部分人都是使用的Express,然后就顺便设置了 app.use(multer(dest: "")) 解析请求,这样就不能实现进度条了。建议把这个设置删了,然后使用 var formidable = require("formidable"); (写在路由模块里)。

var uploadprogress = {}; // 全局变量,存储进度

module.exports = function (app) {
    app.post('/uploadimage', function (req, res) {
        var id = 1;
        var uploadfile;
        var form = new formidable.IncomingForm(); // formidable模块的用法请参考官方文档
        form.uploadDir = '';
        form.keepExtensions = true;
        
        form.on("field", function (name, value) { // 设置唯一的ID
            id = req.session.user._id + req.body.randomID; // 这个value就是表单中的文本框值
        });

        form.on("error", function (err) {
            var callback = "<script>top.uploadErr('" + err + "');</script>";
            res.end(callback); // 将这段文本发送回前端会执行同名的函数
        });

        form.on("progress", function (bytesReceived, bytesExpected) {
            uploadprogress[id] = Math.ceil((bytesReceived / bytesExpected) * 100); // 更新进度
        });

        form.on("abort", function () {
            // 处理中断逻辑
        });

        form.on("file", function (name, file) {
            if (err) {
                console.log(err);
                var callback = "<script>top.uploadErr('" + err + "');</script>";
                res.end(callback);
            }
            uploadfile = file; // 记录文件信息
        });

        form.on("end", function () {
            var file = new File();
            // 存储文件信息
            file.save();

            var fpath = uploadfile.path;
            var callback = "<script>top.uploadEnd('" + fpath + "');</script>";
            res.end(callback);
        });

        form.parse(req);
    });

    app.post('/getuploadprocess', function (req, res) {
        var id = req.session.user._id + req.body.randomID;
        var pr = uploadprogress[id] || 0;
        if (pr == 100) {
            delete uploadprogress[id];
        }
        res.status(200).json({ "progress": pr });
    });
}

工作流程

  1. 表单提交文件时,生成一个随机ID用于标识。
  2. 随机ID与用户的ID组合为文件的标识。
  3. 后台接收文件上传请求,使用 formidable 模块解析文件,监听 progress 事件,通过其返回的参数更新上传进度。
  4. 在表单点击提交按钮时,触发 startRequest 函数,向后台获取上传进度,这个请求由 /getuploadprocess 处理,返回当前进度。
  5. 前端根据返回的进度展示到页面上。

这种方式虽然实现了功能,但由于采用了轮询机制,可能会对服务器造成一定的压力。如果需要优化性能,可以考虑使用WebSockets或类似的技术来实现实时通信。


代码格式化做好点嘛

新手问一下,为什么不用WebSocket呢?

虽然不懂,但是之前有看过 HTML5的进度事件 , 上传处理不是那么麻烦的事情吧…

不明白为什么这样复杂处理。xhr本身就支持监听上传的progress事件。

看下jquery-file-upload

iframe …

看到iframe就浑身难受 iframe真不是个好东西啊

用 XMLHttpRequest 2.0 - ProgressEvent

不好,轮询太low了。应该用html5。 html5 有现成的file api. 细心的会发现chrome在上传文件的时候,右下角会有个进度条的。

留着学习

为了实现文件上传进度的获取,可以使用WebSocket来实现实时通信,这样可以在不增加服务器负担的情况下获取上传进度。下面提供一个简单的示例代码,展示如何使用WebSocket来实现这一功能。

前端代码

首先,我们需要修改前端代码以支持WebSocket:

<iframe name="hidden_iframe" id='hidden_iframe' style="display:none" src=""></iframe>
<form method='post' action='/uploadimage' enctype='multipart/form-data' target="hidden_iframe">
    <input type="text" name="randomID" id="uploadTag" style="display:none" />
    选择图片:<input type='file' name='img' id='filearea' onchange="setTag()"/>
    <input type='button' value='提交' onclick="startUpload()" />
</form>
<div id="showbox"></div>

<script>
var ws;

function startUpload() {
    var socket = new WebSocket('ws://yourserver.com/socket');
    socket.onopen = function () {
        var formData = new FormData(document.querySelector('form'));
        fetch('/uploadimage', {
            method: 'POST',
            body: formData
        }).then(response => response.json()).then(data => {
            ws = socket;
            ws.send(JSON.stringify({ randomID: data.randomID }));
        });
    };

    socket.onmessage = function (event) {
        var progress = JSON.parse(event.data).progress;
        document.getElementById("showbox").innerText = progress + '%';
    };

    socket.onclose = function () {
        console.log('Socket closed.');
    };
}

function setTag() {
    document.getElementById("uploadTag").value = Math.ceil(Math.random() * 1000000);
}
</script>

后端代码

接下来,我们修改后端代码以支持WebSocket,并且在上传过程中实时发送进度信息:

const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const path = require('path');
const fs = require('fs');

const app = express();
const server = http.createServer(app);

const wss = new WebSocket.Server({ server });

wss.on('connection', function connection(ws) {
    ws.on('message', function incoming(message) {
        console.log('received: %s', message);
    });
});

app.post('/uploadimage', (req, res) => {
    const form = new formidable.IncomingForm();
    let bytesReceived = 0;
    let bytesExpected;

    form.on('progress', (bytesRead, totalBytes) => {
        bytesReceived += bytesRead;
        bytesExpected = totalBytes;
        wss.clients.forEach(client => {
            if (client.readyState === WebSocket.OPEN) {
                client.send(JSON.stringify({
                    progress: Math.round((bytesReceived / bytesExpected) * 100),
                    randomID: req.body.randomID
                }));
            }
        });
    });

    form.on('error', err => {
        res.status(500).send({ error: err.toString() });
    });

    form.on('end', () => {
        res.json({ randomID: req.body.randomID });
    });

    form.parse(req);
});

server.listen(3000, () => {
    console.log('Server is listening on port 3000');
});

通过上述代码,我们可以实现文件上传进度的实时反馈。WebSocket相比轮询方式,对服务器的压力更小,因为每次数据变化时只需发送一次消息,而不是每隔一段时间请求一次。

回到顶部