『前端技巧』—— 导出功能的实现

7,471 阅读6分钟

本文是用来记录项目中遇到各种业务场景下的导出功能实现。欢迎点赞收藏

一、后端返回链接的导出

这种导出,我最喜欢了,后端比较有良心,前端很省事。当然在这种场景下,还分两种情况。

  • 链接带域名
if (res.code == 200) {
    window.location.href = res.data;
}
  • 链接不带域名

    域名是请求后端的域名,不是前端页面的域名。项目一般是给多个客户使用,故后端的域名一般都交给运维来配置。项目中这么实现。

    在静态资源public文件夹中建立config.js文件和config.js.example文件,其中config.js文件是忽悠上传到git上的,开发环境可以里面配置后端的域名,在生产环境是由运维在里面配置后端的域名,因为已经忽悠上传到git上的,更新代码时不会覆盖运维所配置的。config.js.example文件是一个示例作用,告诉运维怎么配置。

    config.js和config.js.example内容一样

    /*
    配置文件示列:
    配置文件路径 public/config.js
    */
    window.apiConfig = {
        baseUrl: '后端的域名',
    };
    

    然后再public/index.html引入config.js

    <script>
        var script = document.createElement('script');
        var num = Math.floor(Math.random() * 10000);
        script.src = 'config.js?a=' + num;
        document.getElementsByTagName('head')[0].appendChild(script);
        script = document.getElementById('scriptConfig');
        script.parentNode.removeChild(script);
        script = null;
    </script>
    

    最后这么使用即可

    if (res.code == 200) {
        window.location.href = window.apiConfig.baseUrl+res.data;
    }
    

二、后端返回二进制数据的导出

  • 首先我们要配置一下axios,因为默认服务器响应的数据类型是json,要改为blob。
    export function export(data){
        return service.get('接口地址',{
            params:data,
            responseType:'blob'
        })
    }
    
  • 然后利用new Blob()来处理二进制数据,生成一文件,再用createObjectURL()创建链接后,用a链接自动下载。下面把方法封装一下,挂在Vue原型链上。
const install = function(Vue,opts){
     * 处理二进制数据导出
     * @param blob 二进制流
     * @param name 文件名
     */
    Vue.prototype.exportExcels = function(blob,name){
        // type 为需要导出的文件类型,此处为xls表格类型
        const file = new Blob([blob], { type: 'application/vnd.ms-excel' });
        // 兼容不同浏览器的URL对象
        const url = window.URL || window.webkitURL || window.moxURL;
        // 创建下载链接
        const downloadHref = url.createObjectURL(file);
        // 创建a标签并为其添加属性
        let downloadLink = document.createElement('a');
        downloadLink.setAttribute('href', downloadHref);
        downloadLink.setAttribute('download', name);
        //将a标签添加到body中
        document.body.appendChild(downloadLink);
        // 触发a标签的点击,自动下载
        downloadLink.click();
        //下载完成后移除a标签
        document.body.removeChild(downloadLink);
        //释放下载链接
        url.revokeObjectURL(downloadHref);
    }
}
export default{
    install
}
  • 其中new Blob()的第一个参数是array,里面每项是一个二进制流,第二个参数是可选属性,其中type属性是文件的MIME类型,这个类型由后端决定是什么类型。

    常用的MIME类型如下

    后缀名MIME名称
    *.csvtext/csv
    *.docapplication/msword
    *.dotapplication/msword
    *.xlsapplication/vnd.ms-excel
    *.xlsxapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet

三、在导出接口地址上加参数直接打开下载

上面两种场景都是先要请求服务器,得到返回数据后处理后再下载。这种场景是在导出接口地址上加参数直接打开下载,例如:

window.location.href = '导出接口地址'?user='lhy'&date='2020-05';

上面的请求方法相当get方法,但是当请求导出接口时候参数太多了,使用get方法请求会导致参数缺少。这时候就想办法用post方法请求。

由于这种场景是直接打开导出接口地址下载,有点不好用post方法,那么这时候就要借助HTML<form> 标签和DOM Form 对象来解决。

我们封装一个组件来实现。

<template>
    <form :action="action" :target="target" :method="method" ref="exports">
        <template v-if="data.length">
            <input type="hidden" autocomplete="off" v-for="(item,i) in data" :name="item.name" :value="item.value"/>
        </template>
        <input type="hidden" autocomplete="off" readonly name="token" :value="token"/>
    </form>
</template>
<script>
    export default {
        name: 'formExport',
        props: {
            action: {
                type: String,
                default: '',
            },
            target: {
                type: String,
                default: '_blank',
            },
            method: {
                type: String,
                default: 'post',
            },
            token:{
                type: String,
                default: '',
            },
            data: {
                type: Array,
                default() {
                    return [];
                }
            }
        },
        methods: {
            submit() {
                return new Promise((resoleve,reject) =>{
                    if (this.token) {
                        this.$refs.exports.submit();
                        resolv()
                    }else{
                        reject()
                    }
                }
            }
        }
    }
</script>

组件文档

  • 参数
    参数说明类型可选值默认值
    action必填,导出接口地址String
    target规定在何处打开导出接口地址String_blank:在新窗口打开
    _self:在当前窗口打开
    _blank
    method请求方法Stringpost/getpost
    token必填,鉴权String
    data必填,传给服务器的参数
    {name:参数名称,value:参数值}
    String
  • 方法
    事件名称说明回调参数
    submit提交表单Promise对象
  • 示例
    <template>
        <formExport ref="export" :action="exportData.url" :data="exportData.url" :token="exportData.token"></formExport>
        <el-button @click="handleExport">导出</el-button>
    </template>
    <script>
        export default {
            data(){
                return{
                    exportData:{
                        url:'导出接口地址',
                        data:{
                            user:'lds',
                            page:1,
                            pageSize:20,
                            statTime:'2020-04',
                            endTime:'2020-05'
                        },
                        token:'12334f'
                    }
                }
            },
            components:{
                formExport: () =>import('./formExport.vue')
            },
            methods:{
                handleExport(){
                    setTimeout(() => {
                        this.$refs.export.submit();
                    }, 500);
                }
            }
        }
    </script>
    

四、后端只返回json数据前端生成Excel下载

这里要借助xlsx和file-saver两个插件实现。其中xlsx是生成Excel文件,file-save是保存下载Excel文件

  • 使用npm安装xlsx和file-saver插件,执行命令
npm install xlsx --save
npm install file-saver --save
  • 在servie文件夹中引入Export2Excel.js,在这脚本中对xlsx和file-saver插件中的方法进行封装。

  • 下面把方法封装一下,挂在Vue原型链上

    • 参数文档
    参数说明类型默认可选值示例
    header表格头数据array['一级客户', '申请客户', '支付宝账号', '提现金额', '申请人', '申请时间']
    data表格数据array[{},{}]
    filenameExcel文件名称string'excle1'
    opition额外配置object{}
    • 参数 opition 文档
    参数说明类型默认可选值示例
    filterVal过滤表格数据array['id','name']
    multiHeader表格头数据除最后一行表格头的数据,是个二维数据,不够的用''补全,只在bookType为xlsx或xls下有效。array[ ['序号', '客户信息', '', '', '', ''], ['', '客户姓名', '提现信息', '', '', '']]
    merges合并表格头的规则,只在bookType为xlsx或xls下有效。array['A1:A3', 'B1:F1', 'B2:B3', 'C2:F2']
    autoWidth表格内容是否自适应宽度booleantruetrue/false
    bookType生成文件类型stringxlsxxlsx/xls/csv
const install = function(Vue,opts){
    /**
    * json数据生成Excel并下载
    * @param header 表格头数据
    * @param data 表格数据
    * @param filename Excel文件名称
    * @param opition 额外配置
    */
   Vue.prototype.downloadExcels = function (header, data, filename, opition) {
       let defaultOpition = {
           filterVal: [],
           multiHeader: [],
           merges: [],
           autoWidth: true,
           bookType: 'xlsx',
       }
       if (header && Object.prototype.toString.call(header) != '[object Array]') {
           throw new Error('header请传入数组');
       }
       if (data && Object.prototype.toString.call(data) != '[object Array]') {
           throw new Error('data请传入数组');
       }
       if (opition && Object.prototype.toString.call(opition) == '[object Object]') {
           defaultOpition = Object.assign({}, defaultOpition, opition);
       }
       if (Object.prototype.toString.call(defaultOpition.filterVal) != '[object Array]') {
           throw new Error('filterVal请传入数组');
       }
       if (Object.prototype.toString.call(defaultOpition.multiHeader) != '[object Array]') {
           throw new Error('multiHeader请传入数组');
       }
       if (Object.prototype.toString.call(defaultOpition.merges) != '[object Array]') {
           throw new Error('merges请传入数组');
       }
       const formatJson = function (filterVal, jsonData) {
           if (filterVal.length == 0) {
               return jsonData;
           } else {
               return jsonData.map(v => filterVal.map(j => v[j]));
           }
       }
       data = formatJson(defaultOpition.filterVal, data);
       if (data[0].length > header.length) {
           throw new Error('data中每项数据长度大于头部长度');
       }
       defaultOpition['data'] = data;
       defaultOpition['header'] = header;
       defaultOpition['filename'] = filename;
       import('./Export2Excel').then(res => {
           res.export_json_to_excel(defaultOpition);
       })
   }
}
export default{
   install
}
  • 示例
handleExport() {
    const header = ['一级客户', '申请客户', '支付宝账号', '提现金额', '申请人', '申请时间'];
    const option = {
        bookType: 'xlsx',
        filterVal: ['firstCustomName', 'custom_name', 'withdrawals_bank', 'withdrawals_amount', 'do_username', 'add_time'],
        multiHeader: [
            ['序号', '客户信息', '', '', '', ''],
            ['', '客户姓名', '提现信息', '', '', '']
        ],
        merges: ['A1:A3', 'B1:F1', 'B2:B3', 'C2:F2']
    }
    this.downloadExcels(header, this.tableData, '审核记录', option)
},

五、通过PDF导出

这种导出适用可视化数据场景

  • 在index.html 引入生成PDF的脚本jspdf.js
  • 在index.html 引入截屏的脚本html2canvas.js
  • 下面把方法封装一下,挂在Vue原型链上
const install = function(Vue,opts){
     * 将页面导出成pdf文件
     * @param id 要生成PDF文件DOM区域的id
     * @param fileName 导出的文件名称
     * @param height 导出的pdf高度不够,需要设置额外高度,默认80
     */
    Vue.prototype.exportPDF= function(id, fileName, height = 80){
        //html2canvas只截取dom的可视区域,将dom的可视区域设置大解决导出视图不全的问题
        document.getElementById(id).ownerDocument.defaultView.innerHeight = document.getElementById(id).scrollHeight + height;
        html2canvas(document.getElementById(id), {
            scale: 2,//按比例增加分辨率 (2=双倍).
            dpi: 1080,//导出pdf清晰度 将分辨率提高到特定的DPI(每英寸点数)
            background: "#fff", //背景设为白色(默认为黑色)
            onrendered: function (canvas) {
                let contentWidth = canvas.width;
                let contentHeight = canvas.height;
    
                //一页pdf显示html页面生成的canvas高度;
                //a4纸的尺寸[595.28,841.89]
                let pageHeight = contentWidth / 592.28 * 841.89;
                //未生成pdf的html页面高度
                let leftHeight = contentHeight;
                //pdf页面偏移
                let position = 0;
    
                //html页面生成的canvas在pdf中图片的宽高
                let imgWidth = 595.28;
                let imgHeight = 592.28 / contentWidth * contentHeight;
    
                let pageData = canvas.toDataURL('image/jpeg', 1.0);
                let pdf = new jsPDF('', 'pt', 'a4');
    
                //有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
                //当内容未超过pdf一页显示的范围,无需分页
                if (leftHeight < pageHeight) {
                    pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight);
                } else {
                    while (leftHeight > 0) {
                        pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight);
                        leftHeight -= pageHeight;
                        position -= 841.89;
    
                        //避免添加空白页
                        if (leftHeight > 0) {
                            pdf.addPage();
                        }
                    }
                }
                pdf.save(fileName);
            }
        )
    }
}
export default{
    install
}