NanoUI第二弹,怎么将图形序列化和反解析

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文件:
 

NanoUI第二弹,怎么将图形序列化和反解析
 
    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目录下:
 

NanoUI第二弹,怎么将图形序列化和反解析
 
    此路径当然是可以改变的,找到以上两个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来完成:
 

NanoUI第二弹,怎么将图形序列化和反解析
 
    CompXmlParser提供了两个核心工具函数xmlEncode()和xmlDecode()分别对应序列化和反解析两个过程。CompXmlParser的实现代码稍显复杂,在第二小节中会详细解析其中的核心原理,这里先来看使用效果:
 

NanoUI第二弹,怎么将图形序列化和反解析
 
                                 在界面上用鼠标绘制两个图形
 

NanoUI第二弹,怎么将图形序列化和反解析
 
                                 点击“保存”按钮弹出界面
 

NanoUI第二弹,怎么将图形序列化和反解析
 
                                               保存数据成功
 

NanoUI第二弹,怎么将图形序列化和反解析
 
                                         服务端创建的XML文件
 

NanoUI第二弹,怎么将图形序列化和反解析
 
                                      查看一下XML文件的内容
    从XML文件的内容很容易理解这里的过程,保存的时候会把图元的属性都获取出来,这些属性完整描述了图元的特性,因此,在后面加载的时候,利用这份XML文件一定可以把图元重新恢复出来。
    当然,把图元序列化成XML串的过程并不是那么简单的事情,里面会涉及一定的取舍和包装,反解析的过程也是如此,在后面的第二小节中会详细解析。
   
1.1.2.    序列化定制组件
    所谓“定制组件”,指的是使用FlashCS4制作的元件,然后以SWC库的方式导入到Flex应用中的那些组件。显然,这类组件和使用鼠标直接在“画板”上绘制的东西实质上是不同的。但是,序列化的过程和前面一模一样,还是用SavaPage.mxml这个公共的组件来保存界面,并不需要修改任何代码。请看运行状况截图:
 

NanoUI第二弹,怎么将图形序列化和反解析
 
                                  拖一台主机到界面上
 

NanoUI第二弹,怎么将图形序列化和反解析
 
                           保存到后台
 

NanoUI第二弹,怎么将图形序列化和反解析
 
                                         新增的文件
 

NanoUI第二弹,怎么将图形序列化和反解析
 
                                    依然是一份XML文件
    当然,如果你需要,可以同时在界面上用鼠标绘制和定制组件,保存的过程是一样一样的:
 

NanoUI第二弹,怎么将图形序列化和反解析
 
                                      混合模式
 

NanoUI第二弹,怎么将图形序列化和反解析
 
                                       保存数据成功
 

NanoUI第二弹,怎么将图形序列化和反解析
 
                            再来一个带连接线的
 

NanoUI第二弹,怎么将图形序列化和反解析
 
                                               再来一个流动的连接线
    注意:以上的“流动连接线”和简单的箭头线是不同的,流动连接线是更绚丽的一种连接效果,它内部的小块块可以“流动”起来,产生一种“数据流”的效果。并且,这种连接线的流速、颜色、宽度都可以根据实际数据流的状况发生相应的变化。因此,这个组件非常适合用来制作网络拓扑图。在后续的文章中将会详细解析这一组件的实现原理。
    总结:从以上例子可以看出,对于使用NanoUI的代码来说,序列化鼠标绘制的组件和序列化定制组件的代码是完全一样的。因此,应用层的代码不会感知到底层处理的不同。
1.2.    反解析
    OK,前面我们已经把很多界面保存到了服务端:
 

NanoUI第二弹,怎么将图形序列化和反解析
 
    现在,就可以尝试把它们“加载”回来,在页面上恢复出“画面”了,请自行运行示例查看效果。
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的内容:
 

NanoUI第二弹,怎么将图形序列化和反解析
 
                                               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:
    

NanoUI第二弹,怎么将图形序列化和反解析
 
                                                       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>

 

    
NanoUI第二弹,怎么将图形序列化和反解析
 
                                              产生的XML字符串
    嗯哼,有了这样的原生工具,我们距离目标似乎又近了一步,但是道路依然不是那么平坦。
2.2.2.    注意点
    仔细观察以上的XML字符串你会发现,原生工具产生的XML非常庞大!!!里面包含了大量的信息,而我们的应用中并不需要这些东西。
    这位可能会说:不用也没关系啊,放在里面不管就行了。
    当然,如果你是内网应用,对网络带宽和最终产生的XML文件体积没有什么限制,直接无视这些冗余信息也是可以的。但是,如果是门户型应用,对传输速度和文件体积会有比较严格的要求。因此,我们最好还是对这些信息做出剪裁,这就是为什么在CompXmlParser.xmlEncode头部会有这样一堆delete调用的原因:
 


NanoUI第二弹,怎么将图形序列化和反解析
 
 
                                                    删掉我们不需要的信息
    通过这一组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。注意:请仔细阅读脚本娃娃在线社区的交流协议,违者一律杀无赦。
    最后的最后,请不要尝试联系哥,哥只是一个传说。

1 楼 50980487 2011-06-08  
很好很强大...学习了...
2 楼 fnet 2011-06-23  
有些问题一下子豁然开朗了,学习了~。
3 楼 wangyj0898 2011-06-23  
讲的好详细啊,不过工作中没有用到,先了解下·!
4 楼 pestwei1 2011-07-04  
太强大了啊 ,顶啊,再顶,还是要顶,楼主的无私奉献