Closed funfish closed 4 years ago
项目中遇到的文件下载,上传基本上最常见的事情了。大概半年前,需要实现某表单的查询下载功能,查询还好,只要后端返回数据,我负责展示就好了,但是下载要如何实现呢?用axios的GET请求返回的数据,不忍直视,根本就下载不了。一顿百度谷歌之后,哦,原来这么简单,只要一个window.location.href=url就搞定了,是不是很简单~
window.location.href=url
后来的文件下载我都统统用这种方式,只是下载提示不够明显,后来改为window.open打开个新的tag页,然后自动关闭,明显点下载。好像到这里就已经很完美,一切交给浏览器解决。
window.open
直到开始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就好了。代码如下:
fs.readFile
fs.createReadStream
Content-disposition
Content-Disposition
attachment(fileName)
ctx.curl
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需要一直修改。那有没有正常的用请求接口的方式来下载文件呢? 答案是有的,
window.open(url)
<a href="url" target="_blank">导出</a>
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控制很多东西,比如客户端权限认证,而不是一股脑丢给浏览器。
/download
文件上传也是软肋,毕竟多年来一直没有接触过。。。。以前知道的范围领域也就是input标签可以设置type属性为file,这样就能上传文件了。后来在项目中还真的遇到文件上传的,但是这个时候已经有各种组件了,上来直接是饿了吗element的组件库,又或则是Ant Design的组件,样式又漂亮,根本不需要自己去开发嘛。。。。但是这样真的好吗,之前忙一直没有时间去看,直到了用上了node.js中间层,需要自己来做中转维护。
想想只是用<input type="file" />要如何实现上传呢,明明这都没有和后端联系上。。。。于是乎只能从饿了吗的代码里面找起来,其实饿了吗和ant design的实现大同小异,只是语言不同罢了。代码如下:
<input type="file" />
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不能媲美的。
ev.target.files
FormData
multipart/form-data
formData.append(key, value)
formData.append('file', rawFile)
xhr.open
xhr.send
file
Response.body
node.js端的实现就曲折多了,为了获得上传的文件,用了官方的示例里面的方法ctx.getFileStream(),获得了文件流之后,再在官网介绍的httpclient里面有示例,用到了苏大神的formstream模块,生成可以被httpclient消费的stream对象,如下:
ctx.getFileStream()
httpclient
formstream
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用到的代码大致如下;
form.headers
boundary=
FormStream
leading
form-data
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模块还是自己多玩玩比较好。
ReadableStream.call(this, opts)
file.pipe(fs.createWriteStream(saveTo))
文件下载上传对于大多数JSer可能都不陌生,但是于我却是刚刚开始,犹如打开了新技能,同时也知道了postman里面的文件上传key值是file,所以想梳理api,总结一下文件相关部分。
前言
项目中遇到的文件下载,上传基本上最常见的事情了。大概半年前,需要实现某表单的查询下载功能,查询还好,只要后端返回数据,我负责展示就好了,但是下载要如何实现呢?用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就好了。代码如下:客户端的文件下载
之前在客户端表格导出不是用
window.location.href=url
就是用window.open(url)
,这个方法感觉很土,当然还有更土的就是用a标签,动态修改里面的href,<a href="url" target="_blank">导出</a>
一般这种才是最常见的吧,可惜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的实现大同小异,只是语言不同罢了。代码如下:上传的文件到哪里了?
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对象,如下:看着简单,但是刚开始弄的时候却一脸懵逼,不知道如何是好,尤其是添加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用到的代码大致如下;通过pipe,busboy会触发file事件,同时传入file参数,也就是一个可读流
ReadableStream.call(this, opts)
。对于这个可读流,可以直接file.pipe(fs.createWriteStream(saveTo))
将文件保存到本地磁盘,也可以再度转手如ctx.getFileStream()
。关于busboy模块还是自己多玩玩比较好。总结
文件下载上传对于大多数JSer可能都不陌生,但是于我却是刚刚开始,犹如打开了新技能,同时也知道了postman里面的文件上传key值是file,所以想梳理api,总结一下文件相关部分。