前言
项目中遇到的文件下载,上传基本上最常见的事情了。大概半年前,需要实现某表单的查询下载功能,查询还好,只要后端返回数据,我负责展示就好了,但是下载要如何实现呢?用axios的GET请求返回的数据,不忍直视,根本就下载不了。一顿百度谷歌之后,哦,原来这么简单,只要一个window.location.href=url
就搞定了,是不是很简单~
文件下载
后来的文件下载我都统统用这种方式,只是下载提示不够明显,后来改为window.open
打开个新的tag页,然后自动关闭,明显点下载。好像到这里就已经很完美,一切交给浏览器解决。
直到开始node.js中间层搭建。中间层的功能负责连接前后端,接口还是由后端提供的,具体可以参考淘宝前后端分离实践,以及天猪大神的egg - JSConf China 2016。node.js端用的是egg.js,小公司用它还是很顺心的。平常的API接口还好,只用转发到对应的后端接口就好了,但是文件下载怎么办?难不成我也要在node.js里面写个window.open
?滑稽可笑。
这个时候又回到了请求上面,客户端自然还是用window.open
,而nodejs端获得文件流再返回给客户端,这样window.open
才能用。于是在本地测试了一下,用fs.readFile
以及fs.createReadStream
的方式都可以返回给客户端,但是下载文件类型却是没有的,几经波折后才发现没有设置Content-disposition
,Content-Disposition
消息头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地,用上koa2的attachment(fileName)
方法更是简单,那这个不就迎刃而解了?
这里测试的只是本地的文件流,那后端接口上的文件流呢?这里还是用ctx.curl
方法来请求获取数据,只是需要注意的是curl的设置响应数据格式,不是之前的RESTful的API接口-json交流方式了,而是文件流,所以默认不设置dataType就好了。代码如下:
async download() {
const { ctx, app } = this;
let data = {};
try {
data = await ctx.curl(url, {
method: 'GET',
headers: {},
timeout: 8000,
});
} catch(e) {
data = {
status: 404,
data: {msg:'服务访问出错'}
}
}
ctx.status = data.status || 404;
ctx.set(data.headers || {});
ctx.body = data.data;
}
客户端的文件下载
之前在客户端表格导出不是用window.location.href=url
就是用window.open(url)
,这个方法感觉很土,当然还有更土的就是用a标签,动态修改里面的href,<a href="url" target="_blank">导出</a>
一般这种才是最常见的吧,可惜url需要一直修改。那有没有正常的用请求接口的方式来下载文件呢?
答案是有的,
fetch('/download').then((response) => {
return response.blob().then((blob) => {
var a = document.createElement('a');
var url = window.URL.createObjectURL(blob);
var filename = response.headers.get('Content-Disposition');
a.href = url;
a.download = filename;
a.click();
window.URL.revokeObjectURL(url);
})
});
这个好像那里看到过,不就是创建了个a标签,再点击下载嘛。。。。还不如直接用a标签方便的多!!而且这里还用到了HTML5的download属性,还有blob对象,实在是麻烦。另外如果接口返回的不是文本流,而是json的话,就不用blob,直接用返回的url作为href,来click就好了。
这么看来用接口的形式来下载文件似乎很笨吧,当然从另外一个角度讲,请求/download
接口后可以用js控制很多东西,比如客户端权限认证,而不是一股脑丢给浏览器。
文件上传
文件上传也是软肋,毕竟多年来一直没有接触过。。。。以前知道的范围领域也就是input标签可以设置type属性为file,这样就能上传文件了。后来在项目中还真的遇到文件上传的,但是这个时候已经有各种组件了,上来直接是饿了吗element的组件库,又或则是Ant Design的组件,样式又漂亮,根本不需要自己去开发嘛。。。。但是这样真的好吗,之前忙一直没有时间去看,直到了用上了node.js中间层,需要自己来做中转维护。
客户端实现
想想只是用<input type="file" />
要如何实现上传呢,明明这都没有和后端联系上。。。。于是乎只能从饿了吗的代码里面找起来,其实饿了吗和ant design的实现大同小异,只是语言不同罢了。代码如下:
let upLoad = (ev) => {
let files = Array.prototype.slice.call(ev.target.files);
let rawFile = files[0];
const formData = new FormData();
const xhr = new XMLHttpRequest();
formData.append('file', rawFile);
if (xhr.upload) {
xhr.upload.onprogress = function progress(e) {
if (e.total > 0) {
e.percent = e.loaded / e.total * 100;
}
};
}
xhr.onload = function onload() {
if (xhr.status < 200 || xhr.status >= 300) {
return console.log('wrong');
}
// onSuccess(getBody(xhr));
}
xhr.open('post', '/action?_csrf=VNPzCPKhQRs4eYhoCjFQgwQh', true);
xhr.send(formData);
}
上传的文件到哪里了?ev.target.files
里面就是上传的文件数组,获取到上传文件的对象,再添加到FormData
里面。FormData
又是何物?带着一脸懵逼又去一顿百度谷歌,FormData
是用XMLHttpRequest发送请求的键/值对,当然这也意味着是表单传输multipart/form-data
的形式。如果你想要传入参数,只需要formData.append(key, value)
就可以了。上面代码中自然是用formData.append('file', rawFile)
,紧接着用了xhr.open
和xhr.send
方法,开眼界了,原来xhr.send
里面可以带参数的。。。。文件的键名是file
。
可以看出上面的处理方式直接用的是XMLHttpRequest 2.0,那为什么不用fetch呢?fetch不应该是未来趋势吗?想来这里有兼容问题,另外一点fetch上传文件好像没有进度条一说,只是Response.body
有getReader方法用于读取原始字节流,如此来解决进度条问题Fetch进阶指南,以及XMLHttpRequestabort方法取消对象,也是fetch不能媲美的。
node.js端实现
node.js端的实现就曲折多了,为了获得上传的文件,用了官方的示例里面的方法ctx.getFileStream()
,获得了文件流之后,再在官网介绍的httpclient
里面有示例,用到了苏大神的formstream
模块,生成可以被httpclient
消费的stream对象,如下:
const fileStream = await ctx.getFileStream();
const form = new FormStream();
form.stream('file', fileStream, fileStream.filename);
data = await ctx.curl(url, {
method: 'POST',
headers: form.headers({
Cookie: 'cookieHere',
}),
// contentType: 'multipart/form-data',
stream: form,
dataType: 'json',
})
看着简单,但是刚开始弄的时候却一脸懵逼,不知道如何是好,尤其是添加cookie的时候,由于没有用到form.headers
,文件上传一直有问题,没有依据rfc1867, multipart/form-data
是必须的,同时最重要的是分隔符!!boundary=
这个在headers中是一定要加上的。
看了苏大神的FormStream
里面,才发现原来这是模拟浏览器文件上传的动作,添加leading
再添加stream/buffer,是个不错的npm模块,值得学习。
知道FormStream
了,那ctx.getFileStream()
又是如何获得stream对象呢,一开始以为是egg中ctx自带的方法,后来查了api指南才知道是egg-multipart模块引入的。但是egg-multipart核心部分其实是基于Busboy模块的。Busboy是个好东西,其安装量也是杠杠的。Busboy是用来解析node.js里接受到的form-data
请求,这里egg-multipart用到的代码大致如下;
busboy.on('file', onFile)
function onFile(fieldname, file, filename, encoding, mimetype) {
if (checkFile) {
var err = checkFile(fieldname, file, filename, encoding, mimetype)
if (err) {
// make sure request stream's data has been read
var blackHoleStream = new BlackHoleStream()
file.pipe(blackHoleStream)
return onError(err)
}
}
// opinionated, but 5 arguments is ridiculous
file.fieldname = fieldname
file.filename = filename
file.transferEncoding = file.encoding = encoding
file.mimeType = file.mime = mimetype
ch(file)
}
request.pipe(busboy)
通过pipe,busboy会触发file事件,同时传入file参数,也就是一个可读流ReadableStream.call(this, opts)
。对于这个可读流,可以直接file.pipe(fs.createWriteStream(saveTo))
将文件保存到本地磁盘,也可以再度转手如ctx.getFileStream()
。关于busboy模块还是自己多玩玩比较好。
总结
文件下载上传对于大多数JSer可能都不陌生,但是于我却是刚刚开始,犹如打开了新技能,同时也知道了postman里面的文件上传key值是file,所以想梳理api,总结一下文件相关部分。