由于公司网络限制,客户端不能直接访问后端地址下载文件,只能通过node端来接收文件流,转发给客户端实现文件下载。
node调后台接口,返回的文件流。
后台传给node的文件流
NODE端代码
node端将后台的response信息全部转发给客户端,不做任何处理
router.post('/file/download', (req, res, next) => {
request.get({
url: `${CONFIG.api.speechMark}/file/download?filePath=${req.body.filePath}`,
json: req.body,
gzip:true,
headers:{
'Content-Type': 'application/octet-stream',
'usertoken': req.headers.usertoken,
},
}).on('response', function(response) {
console.log(response.statusCode) // 200
console.log(response.headers)
// console.log(response.headers['content-type']) // 'image/png'
// res.headers['content-type'] = response.headers['content-type']
this.pipe(res)
});
});
以下为踩坑代码,错误示范,可以省略
最开始的写法,想着和调普通接口一样,将后台传回的body传给客户端,但这样传给前端的文件流没有headers等信息,客户端无法正确解析文件流,尝试了各种方法都是一堆乱码
request.get({
url: `${CONFIG.api.speechMark}/file/download?filePath=${req.body.filePath}`,
json: req.body,
gzip:true,
headers:{
'Content-Type': 'application/octet-stream',
'usertoken': req.headers.usertoken,
},
}, (err, response, body) => {
console.log(response.headers)
if (err) {
LOG.error(err)
res.json(err);
} else {
LOG.info(body)
// 错误方法一: 客户端都无法拿到任何响应
res.writeHead(200, {'Content-Type': 'docx'})
res.write(body, "binary")
// 错误方法二: 强制改变了给客户端response的headers,但返回的方法还是json
//客户端当然接不到正确的文件流,下载的文件打开为二进制流
res.setHeader("Content-Type", "application/octet-stream")
res.json(body)
// 错误方法三: 客户端下载的文件直接打不开,报错文件损坏
res.send(body)
/** 错误方法四: 想着不得已的情况,将文件先下载,node给客户端返回下载路径
甚至还没来及实验,客户端下载后,node端怎么将服务器上的文件删掉,
node端下载的文件就打不开
*/
fs.writeFileSync('test.docx', body)
// res.download('test.docx', 'test1.docx', (err) => {
// console.log(err)
// })
// 错误方法五: 同上,下载的文件打不开
// 创建一个可以写入的流,写入到文件 output.txt 中
var writerStream = fs.createWriteStream('output.docx');
// for(var i=0;i<100;i++){
writerStream.write(body,'utf8');
// }
//标记写入完成
writerStream.end();
writerStream.on('finish',function(){
console.log('写入完成');
})
//失败
writerStream.on('error',function(){
console.log('写入失败');
})
// 错误方法六: 误以为,下载的文件流是不是压缩文件啊,又尝试解压~
var zip = new AdmZip('./test1.zip'); // 参数只能是zip文件路径,不能把body直接赋值,报错
// console.log(zip)
var zipEntries = zip.getEntries(); // an array of ZipEntry records
// console.log(zipEntries)
zipEntries.forEach(function(zipEntry) {
// console.log(zipEntry)
// if (zipEntry.entryName == "my_file.txt") {
// console.log(zipEntry.getData().toString('utf8'));
// }
});
// extracts everything
zip.extractAllTo(/*target path*/"./test/", /*overwrite*/true);
}
});
总之,尝试了各种方法返回给前端,都失败,此时已经心力交瘁了。o(╥﹏╥)o
客户端代码
客户端代码没有什么坑了,其中几个关键点
- 在headers下面设置responseType: 'blob'
- 为生成的blob 设置type:'application/octet-stream'
- 为生成的blob 设置filename为node端传过来的filename,带上扩展名
getFileStrem = (filepath) => {
const data = {
filePath: filepath
}
this.request.post('/proxy/file/download', data, {
headers: {
'usertoken': this.state.userToken,
},
// responseType: 'array',
// responseType: 'arraybuffer',
responseType: 'blob'
/**这里是response的content-type,开始架构师让改res的type,
我在上面的headers中加了content-type,改的是req的type*/)
}).then((res) => {
console.log(res);
console.log(res.headers)
/*此方法为展示图片 start*/
let imgsrc = window.URL.createObjectURL(res.data)
/*end*/
/*此方法为下载文件到本地 start*/
// let type = 'application/msword'
let type = 'application/octet-stream'
// let type = result.type
// const buf = Buffer.from(result, 'binary')
let blob = new Blob([res.data], {type: type})
let fileName = res.headers.filename || '未知文件'
if (typeof window.navigator.msSaveBlob !== 'undefined') {
/*
* IE workaround for "HTML7007: One or more blob URLs were revoked by closing
* the blob for which they were created. These URLs will no longer resolve as
* the data backing the URL has been freed."
*/
window.navigator.msSaveBlob(blob, fileName);
} else {
let URL = window.URL || window.webkitURL
let objectUrl = URL.createObjectURL(blob)
console.log(objectUrl)
if (fileName) {
var a = document.createElement('a')
// safari doesn't support this yet
if (typeof a.download === 'undefined') {
window.location = objectUrl
} else {
a.href = objectUrl
a.download = fileName
document.body.appendChild(a)
a.click()
a.remove();
message.success(`${fileName} 已下载`);
}
} else {
window.location = objectUrl
}
/*end*/
}
}).catch((err) => {
console.log(err)
message.error( '系统错误,请稍后重试');
});
}
注意:开始动态设置了blob的content-type,但发现textGrid文件不知道该设置成什么,下载的文件依旧是乱码,经后端小伙伴提示,content-type可以统一设置为application/octet-stream,用filename的扩展名就会生成对应的文件,果然成功。
封装请求NODE接口组件
小插曲,开始node返回给客户端的result只有文件,没有headers等信息,原因是封装的请求node接口的组件,没有返回
// 失败的filter
rejectFilter = (error) => {...};
// 成功的filter
resolveFilter = (result) => {
// return Promise.resolve(result.data); // 开始,这里直接返回了result.data
return Promise.resolve(result);
};
// 存放所有的请求句柄
request = {
get: (url, params, options) => {...},
post: (url, params, options) => {
return axios.post(url, params, {
cancelToken: new CancelToken((c) => {
cancel = c;
}),
...options, // responseType 就是加在这里
headers: (Object.assign({}, (options || {}).headers || {}, {'X-Requested-With': 'XMLHttpRequest'})),
}).then(this.resolveFilter, this.rejectFilter);
},
postTest: (url, params, options) => {...},
put: (url, params, options) => {...},
del: (url, params, options) => {... },
};
以上全部踩坑过程,解决问题过程中,咨询了公司架构师、高级前端开发、后端开发,收获颇多,非常感谢!
架构师总结了我的问题,集中在:
1.了解http协议 (request,response)
2.如何处理不同场景下的响应体 (response, body)
3.响应头中的 content-type 与响应数据对应的关系
顺便贴几个用到的文章
https://segmentfault.com/a/1190000013729797
https://www.npmjs.com/package/adm-zip
https://www.npmjs.com/package/request