日期:2014-05-16  浏览次数:20368 次

处理Extjs4 文件上传时若服务器出错带来的问题
一个典型的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 {
			// 尝试将respon