由于我的工作重心转向网盘的开发和维护,最近整了一个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的链接,再使用上文介绍的下载单个文件方法,即可顺利下载。使用这种思路,文件夹同样也可以打包进来。
在开始之前,首先要介绍下在本文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察看源代码。传送门