zoukankan      html  css  js  c++  java
  • 烦人的运营后台导出大批量数据

    线上运行的业务已经跑了一段时间了,运营需要定期导出数据作分析,领导把小D叫过来说这个需求比较紧急,需要尽快上线,小D信誓旦旦的说没问题,一会儿就搞定。

    小D水平还不错,果然,用了不到2小时时间就把导出做好了。小D是这么实现的,做了个新的接口,接口里面循环处理数据列表然后输出,浏览器收到response后根据header信息将文件下载下来,在测试环境试了下没问题就上线了,然后处理其他事情去了。

    第二天一大早,小D正在地铁上看着自己涨停的股票偷着乐呢,领导电话救过来了,让小D赶紧来公司,小D还在想难道领导因为我昨天做得好要表扬我?听刚才领导的语气不太像啊!

    一到公司小D就找领导去了,领导板着脸,面无表情地说,你看你昨天做的啥东西,导出的列表跟线上的列表差了好多,快看看出啥问题了。

    小D果然水平不错,用了不到10分钟就查到问题了,线上数据量太大,循环超过时间之后PHP脚本就退出了,导致数据导出一半就断掉了。小D分析了下,现在超时时间是5s,时间太短了,按现在数据量估算大概得20s左右,于是小D很快出了新的方案:导出脚本执行时间延长到60s,这样就不会有问题了。很快新的方案上线了,领导还特意亲自操作了下,果然完整的导出来了,领导脸上漏出了欣慰小笑容。

    由于大家的共同努力,业务发展迅速,过了2个月,导出不完整的问题又出现了。领导很生气,下班前,把小D交到办公室,让小D把刚他的实现思路讲一遍。小D巴拉巴拉.....一会儿就说完了。领导听完之后,沉思了几秒,对小D说,我给你讲个故事吧:”小李生病了,耳朵不太好,自己放屁的声音也听不见,就跑到医院去看大夫,大夫给小李开了3顿药,让小李每顿饭后吃。小李问大夫,医生,我吃完药耳朵就好了是吧,医生微微抬起头,扶了扶眼镜说,不是,吃完这个药之后屁声儿大。“。听到这里,不争气的小李没憋住,噗嗤笑出来了,领导气不打一处来,正要发火,这个时候小李手机摔地上了,碰到了手机按键,屏幕亮了,领导看到了小D的屏保,也就是小D的女朋友,心里一紧一颤一哆嗦,刚才的火又完全消下去了,心想,小D这个朋友我交定了。领导又语重心长的对小D说,我给你讲这个故事是想让你知道解决问题不能治标不治本,不能因为业务的发展,功能直接done掉,最好能做到不受业务发展影响,这样吧,我给你个思路,html中有个标签<a download="downlaod.txt" href="data:text/txt;charset=utf-8,download Test Data">download</a>,点击它就可以将这个文件下载下来,你可以在前端做个buffer,将所有内容请求完之后再一次下载下来。小D半信半疑的点点头,领导皱着眉头说你听明白了吗,如果没明白晚上去你家里给你辅导辅导。小D刷一下子回过神来,连连点头说明白了明白了。

    小D回家之后,没顾得吃饭就打开电脑,沿着领导的线索去解决问题。

    <!DOCTYPE html>
    <html>
    <head>
      <title></title>
    </head>
    <body>
      <a download="测试.txt" href="data:text/txt;charset=utf-8,你好,Hello world, 这是一个测试">下载</a>
    </body>
    </html>
    

    当运行上面的代码后,点击下载,果然下载了一个名叫测试.txt的,文件内容是你好,Hello world, 这是一个测试的文件,小D想,要是我把循环移到前端来,每次请求结果放到buffer中,请求完之后,将结果写入a标签的herf中,然后触发点击动作,不是就下载下来了吗,这样就不会因为数据量的增大而导出中断了。小D窃喜,很快,小D做了下面的模拟

    <!DOCTYPE html>
    <html>
    <head>
      <title></title>
      <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
      <script type="text/javascript">
        $(function() {
          var data = "";
          for (var i =0; i< 100; i++) {
            data += "" + i +"
    ";
          }
          $("#test").attr("href", "data:text/csv;charset=utf-8,"+data);
          $("#test")[0].click();
        });
      </script>
    </head>
    <body>
      <a id="test" download="测试.txt" href="#">下载</a>
    </body>
    </html>
    

    当我们打开页面的时候就会自动下载一个名为测试.txt的文件,其中for循环模拟的是ajax请求,每次请求是一页的数据。

    小D在github上面搜了下,发现还真有人已经对这个操作做了封装,开开心心的将https://github.com/eligrey/FileSaver.js项目下载到本地,结合自己的业务做了第一版出来,其中结合了bootstrap,加了进度条,导出时间比较长的时候心里有个数:

    # 需包含FileServer.js,并修改,去掉代码最前面的var
    function ExportData(dataUrl, params, paramPageName, pageStart, pageEnd, exportName) {
        this.dataUrl = dataUrl;
        this.params = params;
        this.paramPageName = paramPageName;
        this.pageStart = pageStart;
        this.pageEnd = pageEnd;
        this.exportName = exportName;
        this.buffers = [];
    }
    
    ExportData.prototype.toggleProgressBar = function (show, percent) {
        show = !!show;
        var id = "export-progress-bar";
        if (show) {
            if (percent) {
                var showPrecent = "" + percent + "%";
                $("#"+id + " div.progress-bar").css("width", showPrecent).html(showPrecent);
            } else {
                var html = "<div id='" + id + "' style='position:absolute;padding: 100px; 100%;height: 100%;top: 0;left: 0;background: #000;opacity: 0.8;z-index: 99'>" +
                    "<div class='progress' style='margin-top: 200px'>" +
                    "<div class='progress-bar' style=' 0%'>" +
                    "0%" +
                    "</div>" +
                    "</div>" +
                    "</div>";
                $("body").append($(html));
            }
    
        } else {
            $("#"+id).remove();
        }
    }
    
    /**
     * 导出
     */
    ExportData.prototype.export = function () {
        var _ExportData = this;
        function sleep (time) {
            return new Promise((resolve) => setTimeout(resolve, time));
        }
        (async function () {
            _ExportData.toggleProgressBar(true);
            await sleep(50);
            _ExportData.params[_ExportData.paramPageName] = _ExportData.pageStart;
            while (true) {
                if (_ExportData.params[_ExportData.paramPageName] > _ExportData.pageEnd) {
                    break;
                }
                var showPercent = 100 * Math.ceil(_ExportData.params[_ExportData.paramPageName] - _ExportData.pageStart + 1) / (_ExportData.pageEnd - _ExportData.pageStart + 1);
                showPercent = showPercent.toFixed(2);
    
                console.log("开始处理第["+_ExportData.params[_ExportData.paramPageName]+"]页数据");
                $.ajax({
                    url: _ExportData.dataUrl,
                    async: false,
                    type: "get",
                    dataType:"text",
                    data: _ExportData.params,
                    success: function(data) {
                        _ExportData.buffers.push(data);
                    }
                });
                _ExportData.toggleProgressBar(true, showPercent);
                await sleep(500);
    
                _ExportData.params[_ExportData.paramPageName]++
            }
    
            fileSaver(new Blob(
                _ExportData.buffers,
                {
                    type:"application/vnd.ms-excel"
                }),
                _ExportData.exportName
            );
            _ExportData.toggleProgressBar(false);
        })();
    };
    

    使用的时候也比较简单,如下所示:

    var exportData = new ExportData(
        "/path/to/api",
        {
    		pageSize: 100
    	},
        "pageNum",
        1,
        100,
        "export-org-list.csv"
    );
    exportData.export();
    

    这里要注意,我封装为了统一,接口返回的时候类型必须是text/plain,纯文本,不然解析可能会失败,导致最终处理失败,另外,如果是csv文件的话,需要在文件最前面加上BOMecho chr(239).chr(187).chr(191),不然可能会解析乱码。


    第二天一大早,小D就拿着这个方案去找领导,巴拉巴拉把原理和实现讲了一遍,听完之后,领导微微点了点头,漏出了欣慰又遗憾的表情,欣慰小D朽木可雕,遗憾没有机会接触小D的女朋友了。

    晚上回到家里,小D仔细一想,这个方案里面用到的新浏览器特性比较多,可能会有浏览器不支持,马上查了下新特性的兼容性。


    Blob是用来保存大量数据的,如果数据全放到url后面,可能有问题,比如双引号,单引号之类的,Blob中就不会,它是以二进制方式存储,herf后面只会是一个用createObjectURL生成的指向Blob内容的uri,比如blob:https://www.baidu.com/5d6222a7-cb7b-5f4b-8381-bde1cbed1b31


    async function是用来实现sleep的,让浏览器做到真正的sleep,不然进度条没法再多个ajax请求之间刷新。

    function sleep (time) {
        return new Promise((resolve) => setTimeout(resolve, time)); }
    

    结合上面的信息可以看到主流浏览器基本都是支持的,而且是后台运营使用的,可以满足条件了,如果要所有浏览器都支持,这个可能不是一个很好的方案。

    至此,问题基本解决了,小D脸上漏出了欣慰的笑容,终于可以和女朋友开开心心了。

    参考文章

  • 相关阅读:
    WebADI_数据验证1_建立基于PLSQL返回FND Message验证(案例)
    PLSQL_案例优化系列_探寻表设计对SQL优化的重要性(案例4)
    PLSQL_案例优化系列_学习左右SQL执行计划各种方法(案例14)
    PLSQL_案例优化系列_探讨该如何分析读懂析执行计划(案例9)
    WebADI_配置设定08_设定参数WebADI Parameters List(案例)
    WebADI_案例实施01_开发一个基于R12.1.3的简单WebADI Desktop(案例)
    PLSQL_案例优化系列_洞察表连接与SQL优化之间关系(案例8)
    WebADI_配置设定09_设定组件WebADI Components(案例)
    WebADI_配置设定07_设定显示WebADI Content / Mapping(案例)
    WebADI_案例实施03_利用FND_LOAD安装和迁移WEBADI以及设定(案例)
  • 原文地址:https://www.cnblogs.com/iforever/p/9151267.html
Copyright © 2011-2022 走看看