由于我的工作重心转向网盘的开发和维护,最近整了一个html版的文件浏览器demo,核心内容为上传和下载,积累了一点经验,这里把其中下载的内容拿出来谈一谈。(本文涉及的前端使用vue.js,后台使用node.js+express)

一个文件的下载

-下载链接

开始之前,回想起我们平时上网下载文件接触到的链接,都是譬如
http://xxxxxx/name=cccc.rar
http://xxxxxx/download/abcabc.exe
等这种类型的链接。

  观察后不难发现这些都是get形式的链接。事实上如果用post,后台会在xhr的responseText中返回文件内容,无法保存到本地,不过可以直接append到页面上,然后让用户自己复制。综合以上考虑,因此我们需要用get建立传输的链接。

-从打开开始

  首先,我们根据需要的参数,拼出需要的get链接。要下载一个文件,需要传入它的文件地址和文件名,get链接就可以这么设计:

http://localhost:2333/downloadSingle?dir=f%3A%5Cdemo&name=desktop.ini

其中有两个参数:dir,name

  后台中根据这两个参数拿到对应的文件,用node.js里fs流的方式传出来:

var currFilePath = path.join(dir,name);
var fReadStream = fs.createReadStream(currFilePath);
fReadStream.on("data",(chunk) => res.write(chunk,"binary"));

  这样下载下来的文件,文件名是混乱的,应该在报文里传入Content-Disposition告诉浏览器文件名(Content-type可以不设置):

res.set({
    "Content-type":"application/octet-stream",
    "Content-Disposition":"attachment;filename="+encodeURI(fileName)
});

  当然下载前应该判断下是否存在这个文件,最后后台的代码如下:

router.get('/downloadSingle',function(req, res, next){
    var currDir = path.normalize(req.query.dir),
        fileName = req.query.name,
        currFile = path.join(currDir,fileName),
        fReadStream;

    fs.exists(currFile,function(exist) {
        if(exist){
            res.set({
                "Content-type":"application/octet-stream",
                "Content-Disposition":"attachment;filename="+encodeURI(fileName)
            });
            fReadStream = fs.createReadStream(currFile);
            fReadStream.on("data",(chunk) => res.write(chunk,"binary"));
            fReadStream.on("end",function () {
                res.end();
            });
        }else{
            res.set("Content-type","text/html");
            res.send("file not exist!");
            res.end();
        }
    });
});

建立好后台之后,前端直接拿链接去测试。浏览器上打开(具体链接自己配置)
http://localhost:2333/downloadSingle?dir=f%3A%5Cdemo&name=desktop.ini
小文件在浏览上冒出来了,下载成功。

-无“闪现”下载

  你问怎么用代码下载下来?最简单的方法就是把这个链接放进window.open(url)里面,然后浏览器会新开个页面,建立链接后会关闭访问页,开始下载。然而,这种方法会闪一下,体验起来比较挫。接下来介绍一种没有”闪现”的方法。
  
  我在《ie8下用iframe解决表单submit以及二级域名跨域的方法》中介绍过如何使用iframe来避免submit提交后页面的自动跳转(使用iframe屏蔽submit提交表单后的自动跳转),基本原理就是隐藏一个iframe,随浏览器怎么折腾它的死活,反正我们看不到就行了。
  下面贴代码:

function downloadByIframe(url){
    var iframe = document.getElementById("myIframe");
    if(iframe){
        iframe.src = url;
    }else{
        iframe = document.createElement("iframe");
        iframe.style.display = "none";
        iframe.src = url;
        iframe.id = "myIframe";
        document.body.appendChild(iframe);
    }
}

  需要下载文件时,调用downloadByIframe(url)即可。
  不过这样的方法有个缺点,当后台找不到文件的时候,无法返回错误信息,不知道的还以为挂掉了。如果要即时相应的话,同样可以参考我这篇文章介绍的方法:使用onload获取后台返回在iframe中的数据

多个文件的下载

  以上介绍了下载一个文件时的处理方法。当然很可能会遇到多个文件要下载的情况。

  遇到多个文件要下载时,如果直接使用单个文件下载的方法,可以使用for循环,然后往里面扔downloadByIframe(url),简单暴力。
  不过文件越多越容易引起强迫症,而且对于文件夹而言,无法使用这种方式下载。

  现在很多网盘对待多文件下载的情况时,会先把它们全部压缩起来,打包成一个文件再下载。通过这种思路,我们可以先在服务器端压缩文件,存到本地,然后通过这个压缩文件的路径和名称,拼出get的链接,再使用上文介绍的下载单个文件方法,即可顺利下载。使用这种思路,文件夹同样也可以打包进来。

-archiver压缩模块

  在开始之前,首先要介绍下在本文node.js中使用的压缩模块archiver,npm可以下载到。我是通过《 nodejs 中压缩/解压方案》(倪舒扬)这篇文章了解到archiver的,该篇文章比较详细的讨论了node.js其他几个压缩模块的利弊,有兴趣可以阅读一下。

  先贴代码:

var output = fs.createWriteStream(path.join("zip",zipName));
var archive = archiver.create('zip', {});
archive.pipe(output);   //和输出流相接
archive.append(fs.createReadStream(fileDir),{"name":fileName}); //塞文件进去
archive.finalize();

其中,archive.append只能放入单个文件,如果要放入文件夹或多个文件,需要使用archive.bulk()这个方法:

archive.bulk([ 
    {
        cwd:currDir,    //设置相对路径
        src: [folderName1, fileName],//文件夹格式:xxx/**
        expand: currDir
    }
]);

官方文档对这个方法有更多的介绍archive.bulk传送门,有兴趣的可以啃一下。

archiver还提供了几个监听事件,用于处理阶段性的事件:

archive.on('error', function(err){
    //报错
});
archive.on('end', function(a){
    //压缩完毕生成文件
});

-代码设计

前端代码比较简单,直接贴出来(为省事我用的vue.js,大家可以用jquery或者XMLHttpRequest自己写一个):

download:function(fileArray){
    var rootDir = this.$data.dir;
    this.$http.post("/download",{
        dir:rootDir,
        fileArray: fileArray
    }).then(function(result){
        //success
        var data = result.data;
        if(data.code == "s_ok"){
            downloadByIframe(data.url);
        }else{
            alert(data.summary);
        }
    });
}

后台综合上面的archiver,代码如下:

router.post('/download',function(req, res){
    var currDir = path.normalize(req.body.dir),
        fileArray = req.body.fileArray,
        fileNameArray = [];

    //将文件和文件夹分开命名
    fileArray.forEach(function(file) {
        if(file.type == 1){
            fileNameArray.push(file.name);
        }else{
            fileNameArray.push(path.join(file.name,"**"));  //文件夹格式:folderName/**
        }
    });

    if(fileArray.length == 0){
        res.send({"code":"fail", "summary":"no files"});
        return;
    }

    var output = fs.createWriteStream(path.join("zip",zipName));
    var archive = archiver.create('zip', {});
    archive.pipe(output);   //和输出流相接
    //打包文件
    archive.bulk([ 
        {
            cwd:currDir,    //设置相对路径
            src: fileNameArray,
            expand: currDir
        }
    ]);

    archive.on('error', function(err){
        res.send({"code":"failed", "summary":err});
        throw err;
    });
    archive.on('end', function(a){
        //输出下载链接
        var downloadUrl = "/downloadSingle?dir="+encodeURIComponent(zipDir)+"&name="+encodeURIComponent(zipName)+"&comefrom=archive";
        res.send({"code":"s_ok", "url":downloadUrl});
    });
    archive.finalize();
});

其中需要强调的一点是,archiver的bulk中src传入的文件夹和文件写法不同,可以参考如下格式:

//[文件夹, 文件夹, 文件, 文件]
["calendar\**, nginx-1.8.1\**, .DS_Store, desktop.ini"]

锦上添花

-统一下载接口

  后台download这个接口经过改造,可以一起适配单个文件和多个文件下载的情况。
  通过判断fileNameArray中是否为单个文件,可以建立一个分支:

if(filesCount == 1 && fileNameArray.length == 1){
    //只有一个文件的时候直接走get
    var downloadUrl = "/downloadSingle?dir="+encodeURIComponent(currDir)+"&name="+encodeURIComponent(fileNameArray[0]);
    res.send({"code":"s_ok", "url":downloadUrl});
}else{
    //多个文件就压缩后再走get
    //用archiver压缩
    ... 
}

  这样前端不管用户选择了几个文件,只需要post download这个接口,拿到返回的链接塞进downloadByIframe()这个方法即可。

  至此,本文文件下载的方法全部介绍完毕。有兴趣的同学可以下载我的demo察看源代码。传送门

Logo

前往低代码交流专区

更多推荐