• [Js进阶]axios + blob文件下载完整开发流程


    [Js进阶]axios + blob文件下载完整开发流程

    前端经常碰到有需要下载文件或图片的业务情况,即后端返回来的是文件流,那么该如何处理之呢?现总结开发流程如下。

    步骤一:请求数据

    const resp = await axios.get('/xxx', {
        params: {
            url
        }
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5

    后端直接返回来的就是文件流数据,即axios response对象打印如下。

    img

    axios response对象的data属性打印如下。

    img

    步骤二:把文件流包装成Blob对象

    在请求的config中添加 responseType: 'blob'

    const resp = await axios.get('/xxx', {
        params: {
            url
        },
        responseType: 'blob'
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    img

    {
        "data": {},
        "status": 200,
        "statusText": "OK",
        "headers": {
            "content-type": "text/plain;charset=UTF-8, application/vnd.openxmlformats-officeddocument.spreadsheetml.sheet;charset=UTF-8"
        },
        "config": {
            "url": "xxx",
            "method": "get",
            "headers": {},
            "params": {
                "url": "xxx.xlsx"
            },
            "baseURL": "xxxx",
            "transformRequest": [
                null
            ],
            "transformResponse": [
                null
            ],
            "timeout": 0,
            "withCredentials": true,
            "responseType": "blob",
            "xsrfCookieName": "XSRF-TOKEN",
            "xsrfHeaderName": "X-XSRF-TOKEN",
            "maxContentLength": -1,
            "maxBodyLength": -1,
            "transitional": {
                "silentJSONParsing": true,
                "forcedJSONParsing": true,
                "clarifyTimeoutError": false
            }
        },
        "request": {}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    步骤三:Axios全局封装

    针对Blob类型修改一番响应拦截。

    $http.interceptors.response.use(
        response => {
            console.log('响应拦截');
            console.log(response);
    
            // 如果是没有状态码的响应
            if (!response.data.code) {
                const resType = Object.prototype.toString.call(response.data);
                const isBlob = resType === '[object Blob]';
                if (isBlob || resType === '[object String]') return response;
            }
    
            // 如果有响应的状态码
            switch (response.data.code) {
                case 2000:
                    break;
                case 1001:
                    if (router.currentRoute.value.path !== '/Login') {
                        router
                            .replace({
                                path: '/Login',
                                query: { redirect: router.currentRoute.value.path }
                            })
                            .catch(err => {});
                    }
                    localStorage.removeItem('SonicToken');
                    break;
                case 1003:
                    ElMessage.error({
                        message: $tc('dialog.permissionDenied')
                    });
                    break;
                default:
                    if (response.data.message) {
                        ElMessage.error({
                            message: response.data.message
                        });
                    }
            }
            return response.data;
        },
        err => {
            ElMessage.error({
                message: '系统出错了!'
            });
            return Promise.reject(err);
        }
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    步骤四、封装下载文件方法

    /**
     * desc: 文件下载 导出结果处理  type:文件类型 zip .xls .xlsx ...
     */
    export function handleDownLoadFile(response, type, fileName) {
        let blob = new Blob([response], {
            type: type + ';charset=utf-8'
        });
        let src = window.URL.createObjectURL(blob);
        if (src) {
            let link = document.createElement('a');
            link.style.display = 'none';
            link.href = src;
            link.setAttribute('download', fileName);
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link); //下载完成移除元素
            window.URL.revokeObjectURL(src); //释放掉blob对象
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    步骤五:后端添加暴露响应头支持

    根据MDN文档:Access-Control-Expose-Headers

    默认情况下,header只有六种 simple response headers (简单响应首部)可以暴露给外部:

    • Cache-Control
    • Content-Language
    • Content-Type
    • Expires
    • Last-Modified
    • Pragma

    这里的暴露给外部,意思是让客户端可以访问得到,既可以在Network里看到,也可以在代码里获取到他们的值。

    上面问题提到的content-disposition不在其中,所以即使服务器在协议回包里加了该字段,但因没“暴露”给外部,客户端就“看得到,吃不到”。

    而响应首部 Access-Control-Expose-Headers 就是控制“暴露”的开关,它列出了哪些首部可以作为响应的一部分暴露给外部。

    所以如果想要让客户端可以访问到其他的首部信息,服务器不仅要在header里加入该首部,还要将它们在 Access-Control-Expose-Headers 里面列出来。

    后端设置

    response.setHeader("Access-Control-Expose-Headers", "Content-Disposition")
    response.setHeader("Content-Disposition", ...)
    
    • 1
    • 2

    成功设置后,服务台Network可以看到:

    img

    最终,js就能获取到响应header的Content-Disposition字段的值了。

    步骤六:正式处理业务逻辑

    const netDownLoadFile = async url => {
        ElMessage.success({
            message: '操作成功'
        });
        try {
            const resp = await axios.get('/xxx', {
                params: {
                    url
                },
                timeout: 60 * 1000
            });
    
    
            // 处理下载文件逻辑
            if (resp.data && resp.data.type === 'application/json') {
                ElMessage.error({
                    message: '操作失败'
                });
            } else {
                // 传回来的是文件
                handleDownLoadFileFromHeader(resp);
                ElMessage.success({
                    message: '操作成功'
                });
            }
        } catch (e) {
            console.log(e);
            ElMessage.error({
                message: '操作失败'
            });
        } finally {
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    补充:针对图片的处理

    有时候需要拿到图片,然后从文件流中生成图片url,填入img的src中。

    例如,需要上传图片,如果已上传,就展示,未上传,就显示上传按钮。

    ant design vue组件调用

    <a-upload
              name="avatar"
              list-type="picture-card"
              class="avatar-uploader"
              :show-upload-list="false"
              accept="image/jpeg,image/jpg,image/png"
              :beforeUpload="beforeUpload"
              :customRequest="uploadImage"
              >
        <div v-if="imageUrl">
            <img style="width: 200px" :src="imageUrl" alt="" />
            <a-button style="margin-top: 15px" size="small" @click="e => handlePreview(e, imageUrl)"
                      >查看大图
            a-button>
        div>
        <div v-else>
            <a-icon :type="loading ? 'loading' : 'plus'" />
            <div class="ant-upload-text">Uploaddiv>
        div>
    
        <a-modal :visible="previewVisible" :footer="null" @cancel="handleCancelPreview">
            <img alt="example" style="width: 100%" :src="previewImage" />
        a-modal>
    a-upload>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    自定义上传方法

    const methods = {
        // 上传图片相关
        beforeUpload(file) {
            const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/jpg' || file.type === 'image/png';
            if (!isJpgOrPng) {
                this.$message.error('只能上传jpg/png格式的图片');
            }
            const isLt2M = file.size / 1024 / 1024 < 10;
            if (!isLt2M) {
                this.$message.error('图?不得?于10MB');
            }
            return isJpgOrPng && isLt2M;
        }
    
        // 上传图片的正式请求
        uploadImage(file) {
            this.netSavPic(params);
            setTimeout(() => {
                this.init();
            }, 500);
        },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    图片业务逻辑

    async netGetPic(params) {
        this.spinning = true;
        let resp,
            err = {};
        try {
            resp = await getPicture(params);
            err = resp;
        } catch (error) {    
            err = error;
        }
        if (resp.data && resp.data.type == 'application/json') {
            this.imageUrl = '';
        } else {
            // 传回来的是图片
            const blob = new Blob([resp.data], { type: 'image/png' });
            this.imageUrl = window.URL.createObjectURL(blob);
        }
    
        this.spinning = false;
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    请求方法

    export function getReferencePicture(data, params) {
        const form = new FormData();
        Object.keys(data).forEach(key => {
            form.append(key, data[key]);
        });
        return axios({
            url: `${baseUrl}/getPicture`,
            responseType: 'blob', //或者是blob
            headers: { 'Content-Type': 'multipart/form-data' },
            method: 'post',
            data: form,
            params
        });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
  • 相关阅读:
    设计原则 | 开放封闭原则
    5G——物理层仿真
    【opencv】形态学重建案例-数糖果(细胞)个数
    Algorithm Design and Analysis
    Lambda 表达式原理分析学习(2022.06.23)
    LBA转换成CHS公式
    PIC单片机与PIC单片机C语言编程简介
    无重复字符的最长子串
    windows 部署多个tomcat详细步骤
    解决 edge 浏览器开发者工具出不来的问题
  • 原文地址:https://blog.csdn.net/qq_41996454/article/details/126019470