在node.js下浅谈文件上传的方法

本文详细介绍了HTML5文件上传的方法,包括使用FormData实现文件上传、multer模块在Express中的配置及使用、上传并发数量的选择、低版本IE兼容方案、XMLHttpRequest的封装与回调处理以及文件重命名等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这是接着上篇文章的下部分,关于文件上传的方法。相对下载,前端部分复杂了不少,接下来就谈谈上传的问题。

Html5上传

-FormData

  FormData是html5新提供XMLHttpRequest Level 2 api,对post数据做了很方便的改进,最大的优点就是可以封入文件类型的对象,上传混合数据时不用依赖submit方法了。
  用法如下:

var formData = new FormData();
formData.append('username', '张三');
formData.append('id', 123456);
formData.append('file', file);  //file为通过file input选择的文件obj
var xhr = new XMLHttpRequest();
xhr.open("post", url);
xhr.send(formData);

  这样一套下来带走表单数据。这是我们前端上传的核心部分之一。
  

-multer模块

  在express中,上传multipart/form-data数据需要使用multer模块,一个express的中间件服务。
  该模块使用前需要初始化,设置一些数据参数。
  首先是上传文件保存的地址:

var multer = require('multer');
var upload = multer({ dest: './uploads/'}); //保存在uploads文件夹中

  这个文件地址用于存储文件上传后放置的文件夹位置,系统会自动给它命名。
  
  之后需要提前设置通过multipart/form-data上传过来的文件的name属性。比如我通过FormData封装了三个数据,分别是”username”,”id”,”file”。那么,我需要在multer的实例中设置这三个数据,代码如下:

var cpUpload = upload.fields([
    {username: 'file'},
    {id: 'file'},
    {file: 'src', maxCount: 10}  //maxCount为一次传输文件的最大数量
]);

  之后在router中传入这个中间件,后台就可以接收到从uploadFile接口传入的数据:

router.post("/uploadFile",cpUpload, function(req, res, next){
    var files = req.files.file,
        dir = req.body.dir;
    ...
});

  文件的数据保存在req.files.file中,其他类型数据保存在req.body.dir中。

-上传并发数量

  和下载类似的,上传存在上传单个文件和多个文件的区别。因此上传思路可以有两种:单文件多ajax,多文件少ajax。
  
  单文件多ajax由于一次只有一个文件,报文size相对比较小,相对稳定性比较好。但是由于对服务器重复请求增加了时间总长度,总上传时间没有只进行一次ajax快。
  大致代码如下:

for (var key in files) {
    if (parseInt(key,10) >= 0) { //排除length等不需要的属性
        var formData = new FormData();
        formData.append("file",files[key]);
        ... //xhr的初始化等代码
        xhr.send(formData);
    }
}

  多文件少ajax由于有更少的请求,总时间比单文件ajax流快。但由于报文由几个文件叠加,几个大文件叠加起来的总报文会非常大,网络不稳定时可能会导致上传了很久的文件最后功亏一篑。
  由于FormData可以装入多个文件名一致的文件,所以代码如下:

var formData = new FormData();
for (var key in files) {
    if (parseInt(key,10) >= 0) { //排除length等不需要的属性
        formData.append("file",files[key]);
    }
}
... //xhr的初始化等代码
xhr.send(formData);

  具体选择哪种传输见仁见智。在这里我用的是单文件多ajax的方式。

-低版本ie兼容

低版本ie下不能使用FormData的方式传输文件,需要用原来的submit方式。这里直接贴代码。

var form = document.getElementById("form");
var iframe = document.getElementById("iframe");
if(!iframe){
    iframe = document.createElement("iframe");
}

form.action = "/uploadFile";
form.target = "iframe";

iframe.name = "iframe";
iframe.src = "";
iframe.style.display = "none";

iframe.onload = function(){
    ...
};

document.getElementById("options").appendChild(iframe);
form.submit();

  值得一提的是,submit的提交方式,在多选文件的情况下是提交一组文件的,也就是多文件单ajax的方式。

ajax回调

-ajax回调

  在单文件多ajax的时候,可能会遇到多个ajax回调的问题。在一般情况下,如果直接按照正常的方式写ajax回调会出现回调被覆盖的问题。

for (var key in files) {
    if (parseInt(key,10) >= 0) {     //排除length等不需要的属性
        var formData = new FormData();
        formData.append("file",files[key]);
        formData.append("dir",rootDir);
        var xhr = new XMLHttpRequest();
        xhr.open("POST","/uploadFile");
        xhr.send(formData);
        //并不是每个ajax的onreadystatechange都会触发
        xhr.onreadystatechange = function(){
            if(xhr.readyState == 4){
                xhr.onreadystatechange = function(){};
                if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
                    console.log("succ");
                }else{
                    console.log("error");
                }
            }
        }
    }
}

  在多个ajax成功上传后,控制台只会输出并不对应数量的callback甚至只有一个callback,前几个都没能顺利的进去,我不能肯定准确的原因。在研究jquery源码后发现,这里需要简单的将XMLHttpRequest一系列步奏封装成函数,就可以顺利走进每个回调了。

for (var key in files) {
    if (parseInt(key,10) >= 0) {     //排除length等不需要的属性
        var formData = new FormData();
        formData.append("file",files[key]);
        formData.append("dir",rootDir);
        xhrTest();
        function xhrTest(){
            var xhr = new XMLHttpRequest();
            xhr.open("POST","/uploadFile");
            xhr.send(formData);
            //正常回调
            xhr.onreadystatechange = function(){
                if(xhr.readyState == 4){
                    xhr.onreadystatechange = function(){};
                    if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
                        console.log("succ");
                    }else{
                        console.log("error");
                    }
                }
            };
        }
    }
}

  不知什么原因让浏览器遗忘了这些callback,用数组保存了这些xhr和callback,response该返回的也都返回了,或许需要闭包才能好好的保存它们。有知道原因的可以告诉我一下。
  
  上面写的有点不够好,参考jquery,xhr再封装一下。

var xhrId = 0;
var xhrCallbacks = {};
var xhrCallbackCount = 0;
/**
 * XMLHttpRequest的封装
 * @params options 参考jquery.ajax
 */
function xhrSend(options){
    var callback;
    return (function(){
        var xhr = new XMLHttpRequest();
        var id = ++xhrId;
        xhr.open(options.method,options.url);
        xhr.send(options.data);
        callback = function(){
            if(xhr.readyState == 4){
                delete xhrCallbacks[id];
                xhrCallbackCount--;
                xhr.onreadystatechange = function(){};
                if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
                    options.success && options.success(xhr.responseText);
                }else{
                    options.error && options.error(xhr.responseText);
                }
                if(xhrCallbackCount == 0){
                    options.done && options.done();
                }
            }
        }
        xhrCallbackCount++;
        xhr.onreadystatechange = xhrCallbacks[id] = callback;
    })();
}

  之后只需调用xhrSend(options)即可。

上传到当前目录

  事实上由于在multer模块的设置,上传的文件只在当前的uploads目录下。但是由于我们能获取到文件名,因此只要使用fs模块的rename移动文件并重命名就可以了。

fs.rename(currdir,realdir,function(err){
    if(err){}
});

-用promise处理异步回调

  由于fs.rename是异步,在多个fs.rename异步下不知道它何时完成所有的回调任务。这里使用es6的promise控制异步回调。

var fsPromise = function(file){
    return new Promise(function(resolved,rejected){
        fs.rename(currdir,realdir,function(err){
            if(err){
                rejected(err);
            }else{
                resolved();
            }
        });
    });
}

  接下来使用Promise.all控制所有rename:

Promise.all(files.map(fsPromise))
.then(function(){
    res.set({
        'Content-Type':'text/html'
    });
    res.send({"code":"s_ok");
})
.catch(function(err) {
    res.send({"code":"failed", "summary":err});
});

  至此,所有步骤完成。完整demo在这里

项目demo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值