nodejs获取文件流转发给前端并下载

由于公司网络限制,客户端不能直接访问后端地址下载文件,只能通过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

客户端代码

客户端代码没有什么坑了,其中几个关键点

  1. 在headers下面设置responseType: 'blob'
  2. 为生成的blob 设置type:'application/octet-stream'
  3. 为生成的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

完结

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容