一个典型的Extjs4上传文件表单:

Ext.define('org.allenz.UploadFormPanel', { extend : 'Ext.form.Panel', initComponent : function() { var states = Ext.create('Ext.data.Store', { fields : [ 'name', 'value' ], data : [ { name : '成功', value : 0 }, { name : '失败', value : 1 }, { name : '模拟服务器错误', value : 2 } ] }); var me = this; Ext.apply(me, { width : 300, height : 100, items : [ { xtype : 'filefield', fieldLabel : '上传文件', name : 'upload', buttonText : '浏览', allowBlank : false }, { xtype : 'combo', fieldLabel : '上传结果', name : 'state', allowBlank : false, store : states, displayField : 'name', valueField : 'value' } ], dockedItems : [ { xtype : 'toolbar', dock : 'bottom', ui : 'footer', items : [ '->', { text : '上传', scope : me, handler : me.onUpload } ] } ] }); me.callParent(arguments); }, onUpload : function() { var me = this; var form = me.getForm(); if (!form.isValid()) { return; } // 浏览器提示 Ext.Msg.show({ msg : '正在上传...', width : 300, closable : false }); form.submit({ url : 'upload.do', scope : me, success : me.uploadSuccess, failure : me.uploadFailure }); }, uploadSuccess : function(form, action) { Ext.Msg.hide(); Ext.Msg.alert('提示', '上传成功!'); }, uploadFailure : function(form, action) { Ext.Msg.hide(); var errorText; switch (action.failureType) { case Ext.form.action.Action.CLIENT_INVALID: // 浏览器检查失败 errorText = '请选择上传文件!'; break; case Ext.form.action.Action.SERVER_INVALID: // 服务器检查文件失败 errorText = action.result.errors[0].message; break; default: // 网络或服务器错误 errorText = '因网络或服务器错误,上传失败!'; } Ext.Msg.alert('错误', errorText); } });
服务器端代码(基于Java Spring,只起返回作用不真正处理文件):
@Controller public class UploadTestController { @RequestMapping("/upload.do") void upload(HttpServletResponse response, int state) throws IOException { String result = null; switch (state) { case 0: result = "{success:true}"; break; case 1: result = "{success:false,errors:[{message:'文件格式错误!'}]}"; break; case 2: throw new RuntimeException("抛出异常"); } response.setContentType("text/html"); response.setCharacterEncoding("UTF-8"); response.resetBuffer(); PrintWriter out = response.getWriter(); out.write(result); out.close(); } }
这里约定,服务器应返回的JSON字符串格式如下:
{success:boolean,errors:[{message:string}]}
在下拉框里选择“成功”或“失败”,上传文件后浏览器都能弹出对话框并显示信息,但是当选择'模拟服务器错误'后上传,浏览器并没有如我们预想那样提示“因网络或服务器错误,上传失败!”,而是在“正在上传...”提示中卡死,控制台提示“uncaught exception: You're trying to decode an invalid JSON String: ...”为什么会这样呢?
其实Ajax只能够收发基于文本的数据,是不能够处理二进制数据的。之所以看上去能够“异步上传”,是因为Extjs针对包含上传的表单使用了iframe提交的方法,流程如下:
1.在页面创建一个iframe和隐藏的form,form的各字段和Extjs表单字段相同;
2.将form的target指向该iframe,并监听iframe的onload事件;
3.提交这个form,待iframe的onload事件触发后(加载完成),读取iframe的innerHtml,并保存到responseText;
4.默认情况下,尝试将responseText进行JSON解码;
5.根据解码结果,调用success或failure回调。
隐患在第3步已经产生,因为不是使用Ajax提交,Extjs无法获取Http状态码,只能假定iframe返回的是JSON数据。而服务器出错时,返回的一般是Html错误页面,Extjs尝试对Html内容进行JSON解码最后抛出异常。
有没有办法处理这种情况呢?答案是有的。负责表单提交的Ext.form.Basic有一个参数errorReader,指定了errorReader时,解释responseText的责任就落到该数据读取器身上。在该数据读取器中先对responseText进行JSON解码测试,若发生异常,制造一个失败JSON对象。代码如下:
// 定义错误信息数据模型 Ext.define('org.allenz.ErrorModel', { extend : 'Ext.data.Model', fields : [ 'message' ] }); // 定义上传错误数据读取器 Ext.define('org.allenz.UploadErrorReader', { extend : 'Ext.data.reader.Json', alternateClassName : 'Ext.data.UploadErrorReader', alias : 'reader.upload', // 设置错误信息根节点为errors root : 'errors', // 覆盖getResponseData getResponseData : function(response) { var data; try { // 尝试将responseText解码为JSON data = Ext.decode(response.responseText); } catch (ex) { // 不能解码,则服务器没有正常返回,制造一个包含错误信息的JSON对象 // 最终这个data会被Reader转换为 // result = {success : false, errors : [yourErrorModel]} // 从在回调函数中action.result读取 data = { success : false, total : 1, errors : [ { message : '因网络或服务器错误,上传失败!' } ] }; } return data; } });
在Extjs表单类的initComponent函数末尾(执行this.callParent(arguments);后)加入:
var form = this.getForm(); var errorReader = Ext.create('org.allenz.UploadErrorReader', { model : 'ErrorModel' }); Ext.apply(form, { errorReader : errorReader });
注意返回数据格式要改为Reader能够辨认的格式,如{success:boolean,total:integer,errors:[yourErrorModel,...]}