NanoUI第二弹,怎么将图形序列化和反解析
NanoUI第二弹:如何将图形序列化和反解析
大漠穷秋 版权所有
2011-05-28 周日
书接上回,在第一篇文章中,介绍了NanoUI的详细设计和核心功能示例。其中遗留了一个非常关键的问题:界面图元的序列化和反解析。本篇按照从易到难的顺序详细解析其用法、原理、关键点。
目录
1. 先上实例 2
1.1. 序列化 4
1.1.1. 序列化鼠标绘制的组件 4
1.1.2. 序列化定制组件 13
1.2. 反解析 16
2. 核心原理 17
2.1. 反解析:AS3的反射 17
2.1.1. 核心原理 17
2.1.2. 注意点 18
2.2. 序列化:图形和XML 20
2.2.1. 核心原理 20
2.2.2. 注意点 21
3. 注意点和设计改良 22
1. 先上实例
首先请注意组件序列化的核心流程:
前台界面:组件解析成XML字符串?通过SysDataMgr提交到后台JSP;
后台:根据前台的文件名参数,在服务端写XML文件。
在上一篇文章的基础上,本篇新增两个JSP文件:
getFileList.jsp用来读取目前已经保存的页面文件;
savePage.jsp用来写新文件。
这两个JSP的代码非常简单,列举如下:
getFileList.jsp:
<%@ page language="java" import="java.util.*,java.math.*,java.io.*" pageEncoding="UTF-8"%> <jsp:directive.page import="java.math.BigDecimal"/> <% //获取文件列表 System.out.println("获取文件列表..."); ArrayList fileNames=new ArrayList(); String filePath=request.getRealPath("/"); File file=new File(filePath+"\\modules"); if(file.exists()&&file.isDirectory()){ File[] files=file.listFiles(); for(int i=0;i<files.length;i++){ File subFile=files[i]; String fileName=subFile.getName(); if(fileName.indexOf(".xml")!=-1){ fileNames.add(fileName); } } } StringBuffer result=new StringBuffer(""); result.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); result.append("<node label=\"文件列表\">"); for(int i=0;i<fileNames.size();i++){ String fileName=(String)fileNames.get(i); fileName=fileName.substring(0,fileName.indexOf(".xml")); result.append("<node label=\""+fileName+"\">"); result.append("</node>"); } result.append("</node>"); System.out.println(result.toString()); response.getWriter().println(result.toString()); %>
savePage.jsp:
<%@ page language="java" import="java.util.*,java.math.*,java.io.*" pageEncoding="utf-8"%> <% request.setCharacterEncoding("utf-8"); //获取flash的请求参数 String xml=request.getParameter("xmlData"); if(xml!=null){ xml=java.net.URLDecoder.decode(xml, "utf-8"); } System.out.println("xml内容>"+xml); String fileName=request.getParameter("fileName"); if(fileName!=null){ fileName=java.net.URLDecoder.decode(fileName, "utf-8"); }else{ return; } System.out.println("保存文件成功,文件名>"+fileName); String commonPath=request.getRealPath("/"); String xmlFilePath=commonPath+"\\modules\\"+fileName+".xml"; //写xml文件 File file=new File(xmlFilePath); if(!file.exists()){ file.createNewFile(); } OutputStream os=new FileOutputStream(file); OutputStreamWriter ow=new OutputStreamWriter(os,"UTF-8"); PrintWriter pr=new PrintWriter(ow); pr.println(xml); pr.close(); %>
请注意一下默认读写文件的目录,服务端会把XML文件都写在应用根目录下的modules目录下:
此路径当然是可以改变的,找到以上两个JSP文件中对应的部分,改成你需要读写文件的路径即可。当然,最好的方式是把这个路径做成配置文件,为了简化后台的代码,这里就不做这个配置了。毕竟,这里关注的是前端Flex的应用,而不是怎么去开发服务端。
1.1. 序列化
1.1.1. 序列化鼠标绘制的组件
以下例子会涉及到两个mxml文件,它们是位于examples/common/目录下的SavaPage.mxml和OpenFile.mxml。这两个文件的代码都不多,列举如下:
SavaPage.mxml
<?xml version="1.0" encoding="utf-8"?> <mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml" title="保存页面" width="400" height="350" close="PopUpManager.removePopUp(this)" showCloseButton="true" borderAlpha="1"> <mx:Script> <![CDATA[ import com.nano.core.JComponent; import com.nano.core.SysConfMgr; import com.nano.core.SysDataMgr; import com.nano.plugins.LightMsg; import com.nano.serialization.xml.CompXmlParser; import com.nano.util.FuncUtil; import com.nano.widgets.JPen; import mx.controls.Alert; import mx.managers.PopUpManager; public var pen:JPen; /** * 加载现有文件列表 */ private function getFileList():void{ SysDataMgr.req({ url:SysConfMgr.getUrl('loadPageListURL'), success:function(dataStr:String):void{ var objsXML:XML = new XML(dataStr); treeData = objsXML.node; }, failure:function():void{ LightMsg.alert("警告","无法保存数据,请检查网络"); } }); } private function expandTree():void { for each(var item:XML in fileTree.dataProvider){ fileTree.expandChildrenOf(item,true); } } /** * 解析对象,生成xml字符串 * 先将对象按类型分组,相同类型的对象放到同一个集合中 * 然后调用对应类的静态方法,将对象解析成xml字符串 */ private var lw:LoadingWin=null; private var xmlStr:String=""; private function encodeData():void{ lw.setMsg("开始解析视图数据..."); var xmlResult:XMLList=new XMLList(); this.pen.objs.eachValue(function(item:*):void{ if(item is JComponent){ xmlResult+=CompXmlParser.xmlEncode(item as JComponent); //lw.setMsg("正在解析>"+(item as JComponent).id); } }); xmlStr=xmlResult.toXMLString(); } /** * 提交按钮:保存页面 */ private function saveFile(e:MouseEvent):void{ if(fileName.text==""){ Alert.show("请输入或选择一个文件名","提示"); return; } lw=LoadingWin.show({ msg:'开始保存数据,请稍候...' }); //开始保存 var me:SavePage=this; FuncUtil.delay(function():void{ encodeData(); var paramObj:*={}; paramObj.xmlData=xmlStr; paramObj.fileName=fileName.text; SysDataMgr.req({ url:SysConfMgr.getUrl('savePageURL'), param:paramObj, success:function(dataStr:String):void{ lw.stopLoading(); LightMsg.alert("提示信息","保存数据成功"); dispatchEvent(new Event("save_success")); }, failure:function():void{ lw.stopLoading(); LightMsg.alert("提示信息","保存数据出错"); dispatchEvent(new Event("save_failure")); } }); },500); } private function selectionChange():void{ fileName.text=fileTree.selectedItem.@label; } ]]> </mx:Script> <mx:XMLList id="treeData"> </mx:XMLList> <mx:VBox width="100%" height="100%"> <mx:Tree id="fileTree" width="100%" height="200" labelField="@label" showRoot="true" dataProvider="{treeData}" valueCommit="expandTree()" creationComplete="getFileList()" change="selectionChange()"/> <mx:HBox width="100%" horizontalAlign="left" verticalAlign="middle"> <mx:Label text="请输入文件名:"></mx:Label> <mx:TextInput id="fileName" text="" click="{fileName.text='';}"> </mx:TextInput> </mx:HBox> <mx:HBox width="100%" horizontalAlign="left" verticalAlign="middle"> <mx:Label color="0xff0000" text="注意:文件名相同将会覆盖现有文件。"></mx:Label> </mx:HBox> <mx:HBox width="100%" horizontalAlign="center" verticalAlign="middle"> <mx:Button label="确定" click="saveFile(event)"/> <mx:Button label="取消" click="PopUpManager.removePopUp(this);"/> </mx:HBox> </mx:VBox> </mx:TitleWindow>
OpenFile.mxml
<?xml version="1.0" encoding="utf-8"?> <mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml" title="请选择一个文件" width="400" height="300" close="PopUpManager.removePopUp(this)" showCloseButton="true" borderAlpha="1"> <mx:Script> <![CDATA[ import com.nano.core.NanoSystem; import com.nano.core.SysConfMgr; import com.nano.core.SysDataMgr; import com.nano.core.SysEventMgr; import com.nano.plugins.LightMsg; import com.nano.serialization.xml.CompXmlParser; import com.nano.util.FuncUtil; import com.nano.widgets.JPen; import com.nano.widgets.link.JFluxLink; import mx.collections.ArrayCollection; import mx.controls.Alert; import mx.managers.PopUpManager; import mx.rpc.events.ResultEvent; public var pen:JPen; private function getFileList():void{ var lw:LoadingWin=LoadingWin.show({ msg:'正在加载,请稍候...' }); SysDataMgr.req({ url:SysConfMgr.getUrl('loadPageListURL'), success:function(dataStr:String):void{ var objsXML:XML = new XML(dataStr); treeData = objsXML.node; SysEventMgr.sendMsg(new Event("closeLoading"),lw); }, failure:function():void{ LightMsg.alert("警告","加载数据失败,请检查网络"); SysEventMgr.sendMsg(new Event("closeLoading"),lw); } }); } private function resultModelHandler(e:ResultEvent):void{ var objsXML:XML = new XML(e.result.toString()); treeData = objsXML.node; } private function expandTree():void { for each(var item:XML in fileTree.dataProvider){ fileTree.expandChildrenOf(item,true); } } private var lw:LoadingWin=null; private function loadFile(e:MouseEvent):void{ if(fileTree.selectedItem==null){ Alert.show("请选择一个模型","提示"); return; } lw=LoadingWin.show({ msg:'正在加载,请稍候...' }); var filePath:String="modules\\"+NanoSystem.httpEncoding(fileTree.selectedItem.@label)+".xml"; SysDataMgr.req({ url:SysConfMgr.getUrl("loadFileUrl"), param:{fileName:filePath}, success:xmlDecode, failure:function():void{ LightMsg.alert("警告","加载数据失败,请检查网络"); SysEventMgr.sendMsg(new Event("closeLoading"),lw); } }); PopUpManager.removePopUp(this); } /** * 加载页面时,xml解析 */ private function xmlDecode(str:String):void{ SysEventMgr.sendMsg(new Event("closeLoading"),lw); pen.clearAll(); var xmlList:XMLList=new XMLList(str); var modules:ArrayCollection=CompXmlParser.xmlDecode(xmlList); this.pen.addToView(modules); } ]]> </mx:Script> <mx:XMLList id="treeData"> </mx:XMLList> <mx:VBox width="100%" height="100%"> <mx:Tree id="fileTree" width="100%" height="200" labelField="@label" showRoot="true" dataProvider="{treeData}" valueCommit="expandTree()" creationComplete="getFileList()" selectedIndex="0" /> <mx:HBox width="100%" height="100%" horizontalAlign="center" verticalAlign="middle"> <mx:Button label="打开" click="loadFile(event)"/> <mx:Button label="取消" click="PopUpManager.removePopUp(this);"/> </mx:HBox> </mx:VBox> </mx:TitleWindow>
显然,这两个文件中最精华的部分,一个是把图元解析成XML的部分,一个是把收到的XML字符串解析成图元的部分。
实质上这两个文件自己并没有干活,而是把序列化和反解析的过程代理给了工具类CompXmlParser来完成:
CompXmlParser提供了两个核心工具函数xmlEncode()和xmlDecode()分别对应序列化和反解析两个过程。CompXmlParser的实现代码稍显复杂,在第二小节中会详细解析其中的核心原理,这里先来看使用效果:
在界面上用鼠标绘制两个图形
点击“保存”按钮弹出界面
保存数据成功
服务端创建的XML文件
查看一下XML文件的内容
从XML文件的内容很容易理解这里的过程,保存的时候会把图元的属性都获取出来,这些属性完整描述了图元的特性,因此,在后面加载的时候,利用这份XML文件一定可以把图元重新恢复出来。
当然,把图元序列化成XML串的过程并不是那么简单的事情,里面会涉及一定的取舍和包装,反解析的过程也是如此,在后面的第二小节中会详细解析。
1.1.2. 序列化定制组件
所谓“定制组件”,指的是使用FlashCS4制作的元件,然后以SWC库的方式导入到Flex应用中的那些组件。显然,这类组件和使用鼠标直接在“画板”上绘制的东西实质上是不同的。但是,序列化的过程和前面一模一样,还是用SavaPage.mxml这个公共的组件来保存界面,并不需要修改任何代码。请看运行状况截图:
拖一台主机到界面上
保存到后台
新增的文件
依然是一份XML文件
当然,如果你需要,可以同时在界面上用鼠标绘制和定制组件,保存的过程是一样一样的:
混合模式
保存数据成功
再来一个带连接线的
再来一个流动的连接线
注意:以上的“流动连接线”和简单的箭头线是不同的,流动连接线是更绚丽的一种连接效果,它内部的小块块可以“流动”起来,产生一种“数据流”的效果。并且,这种连接线的流速、颜色、宽度都可以根据实际数据流的状况发生相应的变化。因此,这个组件非常适合用来制作网络拓扑图。在后续的文章中将会详细解析这一组件的实现原理。
总结:从以上例子可以看出,对于使用NanoUI的代码来说,序列化鼠标绘制的组件和序列化定制组件的代码是完全一样的。因此,应用层的代码不会感知到底层处理的不同。
1.2. 反解析
OK,前面我们已经把很多界面保存到了服务端:
现在,就可以尝试把它们“加载”回来,在页面上恢复出“画面”了,请自行运行示例查看效果。
2. 核心原理
本节前置知识:熟练使用AS3操纵XML数据。(参考本文附件《flash官方指南》第十一章)。
通过上一节中的例子,我们已经可以轻松进行画面到XML字符串之间的双向转换,那么这个过程的核心原理是神马呢?本节将会从易到难解析这一过程。
本节实质上是解析CompXmlParser和SysClassMgr这两个工具类的实现思路,所有的序列化和反解析过程都是由这两个核心工具类配合完成的。
第一节中的例子是先序列化,然后再反解析,而本节的目的是解析这些操作背后原理,因此我们需要采用的过程恰好相反。只有明确了在反解析的时候需要哪些信息,才能指导序列化的过程如何进行处理。
以练功类比,所谓“设计”,是一个“意领气行”的过程;而“实现”的过程则是“气领意行”。做好一个设计,需要对底层的机制有非常充分的理解,并且需要根据实际应用做出取舍和妥协,这也是为什么能做设计的人如此之少,而代码工总是如此之多的原因。当然,这里并没有贬低Coder的意思,只是为了说明这两个过程的差异。毕竟,图纸画好了还需要有技艺精湛的工人去把它变成可以触摸的产品,工匠的技艺的高低会直接决定最终产品的质量。
OK,以上是一坨屁话,下面开始对核心原理和设计过程进行解析。
2.1. 反解析:AS3的反射
2.1.1. 核心原理
我们知道,在Java中有“反射”一说,通过一个类的类名可以获得类的类型定义信息,就像这样:
Class.forName(classNameStr);
在AS3中也有类似的机制:
flash.utils.getDefinitionByName(className);
getDefinitionByName这个工具函数会返回一个类型为Class的对象,我们使用这个对象就可以获得一个类的新实例,一般的用法如下:
var MyClass:Class= flash.utils.getDefinitionByName(className);
var instance: MyClass=new MyClass();
这样一来,只要我们拿到了一个类的名称,就可以根据这个名称创建类的实例。再来复习一下第一节中所产生的XML的内容:
XML文件里的类型信息
显然,我们只要把这样的XML字符串加载到前台,然后根据type属性所指定的类型名,就可以重新创建出图形实例啦!!!在实例创建之后,再把其它属性设置到新建的实例上,这样就可以恢复出图元啦!!!
但是,大湿有云:理想很丰满,现实很骨感。虽然我们明白了前进的大方向,但是实际的实施过程还是困难重重,以下是把理论投入实际所需要的注意点。
2.1.2. 注意点
2.1.2.1. 第一个注意点:编译器问题
我们首先遭遇的是该死的flash编译器,比如你定义了一个类:com.namo.Test,但是在你的应用中并没有使用Test这个类,flash编译器为了保证编译出来的swf文件足够小巧,它不会把Test编译到swf文件里面去。这时候问题就产生了:即使你知道了Test的完整类型名,还是无法通过flash.utils.getDefinitionByName(className)获取到Class对象!!!
那么,如何强制编译器把这些类都编译进来呢?
我们在SysClassMgr中定义一些空属性,这些属性分别指向我们需要反射的所有类型。虽然我们实际上并没有创建实例( 不占内存),但是编译器却以为我们使用了这些类,通过这样的“把戏”就可以顺利戏弄编译器,让它把我们需要反射的类都编译进来。详细代码实现请看SysConfMgr:
SysClassMgr中的小把戏
2.1.2.2. 第二个注意点:构造函数的参数
var MyClass:Class= flash.utils.getDefinitionByName(className);
var instance: MyClass=new MyClass();
如上所示,我们拿到MyClass这个类型之后,使用new MyClass()的方式创建实例,但是,如果MyClass定义的构造函数有参数,此时就会抛出异常啦!!!
这位说了,那还不简单,我们模仿Java,给每个类加一个默认的无参构造函数不就得了嘛。
But很可惜,AS3并不支持函数重载,构造函数也不行!!!因此,如下的代码是无法通过编译的:
package com.nano{ public class Test{ //无参构造 public function Test(){ } public function Test(config:Object){ } } }
那么,如何解决此问题呢?
这里唯一的解决方案是这样的:如果你的类需要带参数的构造函数,必须为构造函数的所有参数指定默认值,以便能给反射的时候提供方便。
对于如上的Test类,如果你需要给构造函数提供参数,最好写成这样:
public function Test(arg1:Object=null,arg2:Object=null){}
当然,这样处理之后,类的构造函数里面就不太适合做非常复杂的逻辑处理了,这需要你根据具体的应用场景做出权衡和取舍。
2.2. 序列化:图形和XML
2.2.1. 核心原理
flash提供了一个内置的工具函数,用来把图形对象解析成XML:
var xmlData:XML=flash.utils.describeType(图形对象);
你可以尝试这段代码,查看最终产生的XML串:
<?xml version="1.0" encoding="utf-8"?> <s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="955" minHeight="600" creationComplete="callLater(init)"> <fx:Script> <![CDATA[ import mx.controls.Alert; function init():void{ var s:Sprite=new Sprite(); var xmlData:XML=flash.utils.describeType(s); var xmlStr:String=xmlData.toXMLString(); this.log.text=xmlData; } ]]> </fx:Script> <mx:TextArea id="log" width="100%" height="100%" editable="false"/> </s:Application>
产生的XML字符串
嗯哼,有了这样的原生工具,我们距离目标似乎又近了一步,但是道路依然不是那么平坦。
2.2.2. 注意点
仔细观察以上的XML字符串你会发现,原生工具产生的XML非常庞大!!!里面包含了大量的信息,而我们的应用中并不需要这些东西。
这位可能会说:不用也没关系啊,放在里面不管就行了。
当然,如果你是内网应用,对网络带宽和最终产生的XML文件体积没有什么限制,直接无视这些冗余信息也是可以的。但是,如果是门户型应用,对传输速度和文件体积会有比较严格的要求。因此,我们最好还是对这些信息做出剪裁,这就是为什么在CompXmlParser.xmlEncode头部会有这样一堆delete调用的原因:
删掉我们不需要的信息
通过这一组delete,我们可以删掉大量冗余的信息---不是全局。当然,如果你愿意,还可以进行进一步的细致剪裁,在剪裁之前需要对产生的XML结构做出更详细的分析,以确定哪些信息需要保留。
这里,CompXmlParser为了能给反解析的过程带来便利,并没有使用原生获得的XML结构,而是在剪裁之后,把信息格式化成了这样的结构:
<instance type="com.nano.widgets::JRect" base="com.nano.widgets::JFilledShape"> <variable> <name> </name> <value> </value> <type> </type> </variable> </instance>
所有的属性都被格式化成name、value、type这三个属性。经过这样一番处理之后,所获得的XML字符串的体积已经大大减小,此时我们就可以把它提交到服务端啦!!!
3. 注意点和设计改良
至此,我们已经完成了图元序列化和反解析的整个流程。当然,在整个处理链条中一定还存在可以重构和改良的地方。这些,就留给聪明的你去完成吧!!!
NanoUI系列文章还会有如下主题,近期释出,敬请关注:
工具函数的设计;
如何设计高级语义事件机制;
UI组件的继承结构以及UI组件一般化设计;
如何设计自己的拓扑布局算法;
And so on...
有相关兴趣者可入脚本娃娃3号群进行讨论:91508669。注意:请仔细阅读脚本娃娃在线社区的交流协议,违者一律杀无赦。
最后的最后,请不要尝试联系哥,哥只是一个传说。