要解决的问题:导出文件超时。

解决思路:异步下载方式进行导出。先生成下载任务,然后轮询文件名,生成文件名的时候,再执行下载。

由于系统中需要执行导出的操作较多,因此将导出方法封装成了一个服务asyncExportFile,分别注入到各个需要执行下载任务的controller中。

(1)执行下载任务的asyncExportFile服务:

'use strict';

/**
 * @ngdoc service
 * @name  adminApp.asyncExportFile
 * @author  wqq
 */
angular.module('adminApp')
    .service('asyncExportFile', ['$timeout', '$q', 'toastr', 'asyncExportFileService', function ($timeout, $q, Toastr,  asyncExportFileService) {
        // var exportingFile = false;
        var service = {
            export: function (options) {
                // if(exportingFile === true){
                //     return; // 有正在流程中的任务在执行,就不再向后端发请求
                // }
                // exportingFile = true;
                return $q(function (resolve, reject) {
                    // console.log("入参", options);
                    Toastr.info("文件导出中");
                    var type = options.type;
                    var param = options.param;
                    genTask(type, param).then(resolve, reject);
                }); // 用$q,可以实现promise传回执行结果。但是下面要用到的3个函数,必须都用promise实现。resolve里面可以加参数,传给外面页面。

                function genTask(type, param) {
                    return $q(function (resolve, reject) {
                        // console.log("传给genTask服务的参数", JSON.stringify(param));
                        // JSON.stringify(param);
                        var genTaskPromise = asyncExportFileService.genTask(type, JSON.stringify(param));
                        genTaskPromise.then(function (rst) {
                            var taskId = rst.taskId;
                            return queryTask(taskId); // 必须return,才能在下面then的时候知道执行结果
                        }, function () {
                            // console.log("需要重新生成任务,此处让用户重新点击");
                            // exportFileFlag = false;
                            Toastr.error("导出失败");
                            reject();
                        }).then(resolve, reject);
//genTaskPromise执行完成,queryTask(taskId);也执行完成, genTaskPromise.then才执行完成,才执行genTaskPromise.then().then()里的resolve。
                    })
                };

                function queryTask(taskId) {
                    var taskId = taskId; // 方案一:放最外面也可以,避免闭包内变量提升导致找不到taskId。
                    return $q(function (resolve, reject) {
                     // var taskId = taskId; //报错:会找不到taskId。原因:变量提升,闭包内变量变成最前面的,外面同名的排在了后面,所以找不到外面的,即便是参数中的,本质是作用域链的问题。
                     // var taskId2 = taskId; //方案二:可以换个不同的名字。
                    //  console.log("queryTask---taskId22", taskId2);
                        var queryTaskPromise = asyncExportFileService.queryTask(taskId);
                        queryTaskPromise.then(function (rst) {
                            if (!rst.file) {
                                // console.log("轮询等待task任务");
                                $timeout(function () { queryTask(taskId); }, 2000);
                            } else {
                                var file = rst.file;
                                return downloadTask(taskId, file);
                            }
                        }, function () {
                            // console.log("未知taskId,需要重新生成任务,此处让用户重新点击");
                            // exportFileFlag = false;
                            Toastr.error("导出失败");
                            reject();
                        }).then(resolve, reject);
                    })
                };
                function downloadTask(taskId, file) {
                    return $q(function (resolve, reject) {
                        // console.log("下载文件时候传入的参数", taskId, file)
                        asyncExportFileService.downloadTask(taskId, file);
                        //  $timeout(function () { exportingFile = false; }, 2000);
                        //为了减少对服务端请求压力,可以对同一浏览器设置 一次下载完成2s后,才能再下载
                        resolve();
                    })
                };


            }
        };
        return service;
    }]);



备注:因为想在controller中得知任务执行结果,然后给按钮上文字修改,所以后面用了promise。这样里面的3个函数都必须定义为promise对象。  生成下载任务和轮询文件名生成结果这2个会失败,就定义了reject。下载的时候,location.href和window.open很快,而且没办法拿到返回值,所以直接按成功处理。下载方法执行后,就直接resolve()。


(2)向后端发请求查询数据和执行结果的asyncExportFileService服务:

'use strict';
angular.module('adminApp')
    .service('asyncExportFileService', ['$q','common',function($q, Common){
        var genTask = '/xhr/file/asyncDownload/genTask.json';
        var queryTask = '/xhr/file/asyncDownload/queryTask.json';
        var downloadTask = '/xhr/file/asyncDownload/downloadTask.json';
        var service = {
	    	genTask: function(type, param){
	    		var defer = $q.defer();
	    		var params = {
                    type: type,
                    param: param
                };
	            Common.post(Common.contextPath + genTask, params).success(function(res) {
	                if(res.code == 200) {
	                    defer.resolve(res.data);
	                }
	                else{
	                	defer.reject();
	                }
	            }).error(function() {
	                defer.reject();
	            });
	            return defer.promise;
	    	},
            queryTask: function(taskId){
	    		var defer = $q.defer();
	    		var params = {
	    			taskId: taskId
	    		};
	            Common.post(Common.contextPath + queryTask, params).success(function(res) {
	                if(res.code == 200) {
	                    defer.resolve(res.data);
	                }
	                else{
	                	defer.reject();
	                }
	            }).error(function() {
	                defer.reject();
	            });
	            return defer.promise;
	    	},
            downloadTask:function(taskId, file){
				console.log("下载文件时候传进来的参数",taskId,file)
	    		var params = {
	    			taskId: taskId,
                    file: file
	    		};            
                //window.open(Common.contextPath + downloadTask + '?' + $.param(params));
				location.href = Common.contextPath + downloadTask + '?' + $.param(params);
	    	}
        };
		return service;

    }])


备注:


因为 window.open 下载的新窗口总是被拦截 。 所以后面改为 location.href 下载 。location.href下载的时候,因为拿到文件下载,整个过程时间很短,所以用户感知不到页面跳转。


(3)页面中使用这个服务:

controller中引入:'$timeout','asyncExportFile'。

html中:

<div class="col-sm-2 text-right">
    <button type="button" class="btn btn-default" ng-disabled="!exportFlag" ng-click="exportChannelSku();">
       {{exportText}}
    </button>
</div>




controller中:


$scope.exportChannelSku = function () {
				var params = {
					param: {
						channelId: channelId,
						firstCategory: $scope.search.cateId,
						priceStatus: $scope.search.status
					}, // 原本的导出参数
					type: 2   // 导出类型
				}
				$scope.exportFlag = false;
				$scope.exportText = '导出渠道选品中...';
				asyncExportFile.export(params).then(function () {
					$scope.exportFlag = true;
					$scope.exportText = '导出渠道选品';
					console.log("导出成功");
				});
			};


$scope.exportFlag和$scope.exportText是为了提示用户文件正在下载,让按钮置灰,文案改变。最初要设置初始值。

$scope.exportFlag = true;
$scope.exportText = '导出渠道选品';


(4)后面QA测出用户连续点击两次会报400错误。

前端顶多设置这次在下载没执行完的时候,按钮不能点击,并提示正在导出中。没办法不让用户点两次。因为同一页面,这个item导出后,用户也可以选择其它item导出。关于函数节流和去抖,时间也不好设置。

后面自己去试了下,打开两个浏览器,同时点导出也会报400。

因为后端没有对生成下载任务的方法进行并发控制。最简单的,可以写一个manager的,一个任务执行完了,再去执行另一个任务。或者加sychronized锁。