逸言

Post数据到服务端生成文件

| Comments

我们希望将客户端(ExtJS 4.0)浏览器中选择的记录post到服务端(ASP.NET MVC 3.0),然后在服务端将这些数据导出到CSV文件中,并传到客户端进行保存。最初,我们希望在客户端采用AJAX请求,将json数据post到服务端。例如:

postSelectedItems: function(gridId, callback) {
    var me = this;
    Ext.Ajax.request({
        method: 'POST',
        jsonData: Ext.encode(
            Ext.getCmp(gridId).getSelectedRecords().map(function (item){
                return item.data;
                })
        ),
        success: function(){
            callback();
        }
    );
}

在服务端,Controller中的Action可以处理post过来的json数据,然后以FileResult的方式返回。为了更好地处理CSV文件,我们定义了一个CsvActionResult,它继承自FileResult,并重写了它的WriteFile()方法。浏览器就可以获得CsvActionResult返回的Stream,从而就可以完成文件的下载和保存了。

然而,这种AJAX异步请求的方式是不工作的。因为发起者是AJAX,这种异步方式会导致浏览器无法收到处理后的Stream。我们在网上搜索了许多资料,都没有很好地解决这种异步方式对json数据的处理,完成文件下载。经过分析,我们大致想到有两种解决思路。

一是方案将导出文件的请求分为两步:一步是异步post数据,让服务端获得客户端传来的json数据,并将其存放到Session中;第二步则是在post成功之后,在异步回调中再次发出一个同步的Get请求,并定义对应的控制器Action响应该Get请求。例如,我们在ExtJS中的postSelectedItems()函数的success()回调中,执行downloadFile()函数,由该函数发出Get请求。同时,在服务端的Controller中定义DownloadFile()方法响应该请求。例如:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Export(int environmentId, List<SearchResult> selectedItems) {
    Session["SelectedItems"] = selectedItems;
    return new EmptyResult();
}

public ActionResult DownloadFile(int environmentId){
    var selectedItems = Session["SelectedItems"] as List<SearchResult>;
    var data = selectedItems.ExportCsvData();
    var metaData = CsvExportMeta.ExportMetaData();
    var exportTime = DateTime.Now;
    var fileName = String.Format("export_{0}.csv",exportTime.ToString("yyyyMMddHHmm"));
    return new CsvActionResult<SearchResultCsvView>(data) {
        FileDownloadName = fileName,
        MetaData = metaData;
    }
}

比较理想的一点是,由于我们在Global.asax.cs的Application_Start()方法中已经添加了如下代码:

ValueProviderFactories.Factories.Add(new JsonValueProviderFactory());

所以,客户端发过来的json数据可以转换为Export()方法接收的List对象。但是,这种方式将数据存放到了Session中,可能存在隐患。一方面,如果需要导出的数据较多,就可能占用过多的内存空间。一旦访问量增长,就会影响到服务器的稳定性和性能。如果服务端采用集群,则Session还要共享,否则二次请求的时候,可能会被Load Balancer路由到其他服务器。倘若同一个用户连续两次Post,也可能导致导出功能出现问题。整体而言,这种方案并不稳定。

第二种解决思路就是放弃AJAX异步请求的方式,而转用Form表单的Post请求。我们采用了临时编写表单的方式,将post的数据塞到Input的Value中,然后Submit表单。例如在ExtJS的Controller中,我们定义了这样的函数:

exportSelectedItems: function(gridId) {
    var exportWindow = window.open('');
    var exportDocument = exportWindow.document;
    var form = exportDocument.createElement('form');
    form.setAttribute('method', 'post');
    form.setAttribute('action', url);

    var hiddenField = exportDocument.createElement('input');
    hiddenField.setAttribute('value', Ext.encode(
        Ext.getCmp(gridId).getSelectedRecords().map(function (item)) {
            return item.data;
        }
    )));
    hiddenField.setAttribute('name','selectedItems');
    hiddenField.setAttribute('type','hidden');
    form.appendChild(hiddenField);
    exportDocument.body.appendChild(form);
    form.submit();
}

现在就可以在服务端的Controller中,只定义一个Export()方法了。此时的Export()方法不再需要List参数,因为客户端Post过来的数据是放在Request的Form中。我们可以根据设定的name值获得该数据。这个数据是json数组的字符串。现在,有另外一个问题又钻出来了。我们必须将该字符串转换为我们需要的List对象,然后就可以重用该对象的扩展方法ExportCsvData()方法,将其再转换为CsvActionResult可以接受的类型。回顾之前异步post的方式,在该种方式下,客户端post过来的json数据并没有进行转换,其原因在于我们在Global中,我们向ValueProviderFactories的Factories中注册了JsonValueProviderFactory对象,转换的执行过程事实上是一种反序列化。在JsonValueProviderFactory的私有静态方法GetDeserializedObject()方法中,调用了JavaScriptSerializer对象的DeserializeObject()方法。就是在这个类中,同时还定义了Deserialize泛型方法,而它正是我们所需要的:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Export(int environmentId) {
    var result = Request.Form.Get("selectedItems");
    var selectedItems = new JavaScriptSerializer().
        Deserialize<List<SearchResult>>(result);
    var data = selectedItems.ExportCsvData();
    var metaData = CsvExportMeta.ExportMetaData();
    var exportTime = DateTime.Now;
    var fileName = String.Format("export_{0}.csv",exportTime.ToString("yyyyMMddHHmm"));
    return new CsvActionResult<SearchResultCsvView>(data) {
        FileDownloadName = fileName,
        MetaData = metaData;
    }
}

现在这种方式不再需要向Session存放数据,并且只用了一次请求来完成。虽然是用同步的方式进行Post,但由于post的数据最多不会超过200k,用户不会感受到很明显的阻塞现象,是完全可以接受的。

Comments