EXmobi官方文档
ExMobi®从入门到精通
ExMobi门户:http://www.exmobi.cn
ExMobi论坛:http://bbs.exmobi.cn
支撑电话:400-110-1111 025-6677-7333
官方微博(新浪):@ExMobi
南京烽火星空通信发展有限公司
2014年
第1篇ExMobi基础篇
1 ExMobi概述
1.1 ExMobi概述
ExMobi是烽火星空公司推出的跨平台移动应用开发中间件产品。ExMobi通过全面的数据集成技术和丰富的跨平台客户端展现能力,将业务系统快速、安全、高效的移植于移动终端,并从开发(IDE环境)、集成(IT系统对接、云服务)、打包(各个操作系统的应用打包)、发布(应用的运行)、管理(日志管理,更新管理)上提供了一整套的解决方案。
1.2 ExMobi组成元素
ExMobi包含了一系列的技术和产品,主要包括:ExMobi客户端、ExMobi服务端、MBuilder集成开发工具以及ExMobi产品门户。
1.2.1 ExMobi产品门户
ExMobi产品门户网址为www.exmobi.cn ,它包括ExMobi产品中心、EDN开发者门户、BBS论坛(bbs.exmobi.cn)以及ExMobi开放平台。
ExMobi产品中心可以了解到ExMobi产品的功能和最新动态;EDN开发者门户为开发者提供应用的开发、发布和管理等一站式服务;BBS论坛为开发者提供可持续的学习和交流空间;ExMobi开放平台提供广阔的基于ExMobi的原生插件开发资源,开放平台开发者开发的插件可以作为ExMobi客户端引擎的一部分。
EDN开发者门户、ExMobi开放平台、BBS论坛和MBuilder均需要在ExMobi产品门户上进行注册成功后得到账号和密码方能登陆使用。
1.2.2 ExMobi客户端
ExMobi客户端负责应用在移动终端的展示和交互,以及与ExMobi服务端的通信。它主要包含:PC模拟器客户端、Android客户端、IOS客户端、Windows8客户端等。
ExMobi客户端实现跨平台的原理,是在不同移动终端上将同样的功能和交互封装成统一的接口,如:XHTML、JavaScript、CSS、主题、Native插件接口等。对于移动应用开发者来说实际上就像WEB开发一样开发一套XHTML的应用即可进行跨平台的数据展现和交互。而能够执行这种特殊应用的引擎我们称为“基座”。所以,对于一个完整的ExMobi客户端应该包含基座和应用。
而为了方便开发调试,ExMobi客户端存在两种状态,一种是基座状态,一种是打包状态。
基座状态主要在开发调试时使用,安装基座客户端的时候,里面是没有应用的。客户端安装好之后,打开基座客户端首先看到的就是基座,在基座的“设置”功能中配置好开发调试环境的IP和端口即可方便的安装和卸载应用,并对应用进行开发调试,而不是像Native原生开发一样每次都要编译,方便了调试也节省了编译的时间。
打包状态为应用开发完毕后将基座和应用一起打包生成最后发布安装包的状态。打包客户端实际上就是在打开客户端的时候把基座隐藏起来直接看到应用。打包客户端可以使用ExMobi开发者门户的云打包服务进行在线打包。
下图为ExMobi基座客户端效果图。
1.2.3 ExMobi服务端
ExMobi服务端负责对ExMobi客户端请求过来的数据进行处理,并把处理结果响应给客户端进行操作。所以,它主要的功能就是对数据的集成能力。
ExMobi服务端主要包含4大组件:ExMobi管理平台(EMP)、基本核心引擎(BCS)、统一推送引擎(PNS)、统一文档转换引擎(DCS)。
EMP为ExMobi的管理平台,对ExMobi应用和客户端的管理、终端用户使用授权、统计报表展现、其他引擎和服务的管理等。
BCS为数据集成的服务引擎,主要包括:HTTP请求的模拟、Web Service集成、数据库集成、标准接口集成、接口发布等。
PNS为统一推送引擎,实现与BCS的对接,通过UDP/TCP Push、二进制短信push、APNS/C2DM等通道实现应用的统一推送。
DCS为统一文档转换引擎,可以对标准OFFICE文档、压缩包、图片等格式进行支持。也支持对书生SEP、方正CEB、点聚AIP等特殊格式文档进行转换在客户端展示。
下图为部署了ExMobi服务端后在浏览器上看到的EMP管理平台的效果图:
1.2.4 MBuilder集成开发工具
MBuilde是为了方便开发者开发移动应用而研发的一款基于Eclipse的ExMobi移动应用集成开发工具。
MBuilder包括ExMobi的Eclipse插件、ExMobi的PC模拟器基座客户端以及ExMobi服务端的DEV开发版本(BCS-mini版本+PNS-mini版本+DCS-mini版本,不包含EMP)。
使用MBuilder就像使用Eclipse或者其他开发工具一样,能够方便的创建、编辑、调试和发布应用。
1.3 ExMobi的应用工作原理
ExMobi的应用包含客户端资源和服务端资源。客户端资源运行在ExMobi客户端;服务端资源运行在服务端。
应用的运行原理如下图所示:
ExMobi应用的运行原理实际上是ExMobi服务端将第三方系统数据源转换为ExMobi客户端识别的语言在不同的终端进行展示和交互的一个过程。而服务端转换的依据是通过客户端发起的请求指令经过MAPP路由传递到服务端的。
2 MBuilder的使用
MBuilder集成开发工具为开发者提供了简单易操作的开发环境。正确使用MBuilder可以极大的提高开发的效率。
2.1 安装、升级和卸载
2.1.1 安装前准备工作
安装操作系统要求:支持XP、WIN7(32bit、64bit)。
JDK版本要求:JDK1.6.0_20。
MBuilder版本要求:2.X.X版本。
JDK和MBuilder安装包可以通过下载和安装指引页面下载:
http://www.exmobi.cn/sdkdownload.jsp
2.1.2 安装MBuilder
运行安装文件,如下图所示:
注:文件名根据实际版本情况而定,其中2.3.6是版本号。
点击“下一步”后选择“安装目录”
安装目录后点击“安装”即可执行安装操作。
安装过程要求安装的组件也需要安装。
模拟器依赖组件:
WebKit组件库:
全部安装完毕,点击“完成”按钮即可完成MBuilder的安装:
2.1.3 运行MBuilder
安装完成后,会在桌面有MBuilder的快捷图标:
点击即可运行MBuilder,运行前需要选择工作空间,如下图所示:
由于卸载MBuilder的时候整个MBuilder安装目录的内容都会被删掉,为了保险起见,请在MBuilder外部创建工作空间,然后选择该目录。
选择完毕后,点击“OK”按钮进到MBuilder工作界面,由于首次使用需要进行进行登陆,在登陆前需要在ExMobi产品门户进行注册(www.exmobi.cn),使用注册的账号和密码进行登陆,如下图所示:
若账号、密码正确,即可进到MBuilder的工作界面:
2.1.4 升级MBuilder
MBuilder提供了在线升级的功能,点击主菜单-“help”-“about MBuilder”即可看到界面,如下图所示:
点击“check for updates”即可进行在线升级。
2.1.5 卸载MBuilder
在MBuilder安装根目录下,找到“uninst.exe”文件,点击执行即可进入卸载向导。
2.2 安装目录介绍
在MBuilder的安装目录下,存在如下目录:
MBuilder
|---eclipse 存放eclipse开发平台相关文件
| |--- dropins 存放MBuilder 插件目录
|---env 依赖组件的安装包(ExMobi/Webkit组件库)
|---ExMobi ExMobiPC模拟器基座客户端
|---ExMobi-server ExMobi服务端DEV版
|---apache-tomcat-7.0.22 自带服务依赖的tomcat
|---uninst.exe 卸载程序
MBuilder的ExMobi客户端和服务端是可以单独升级的。可以到ExMobi产品门户下载PC模拟器安装包和ExMobi服务端的压缩包,安装或者解压相应的目录到MBuilder的客户端和服务端的安装目录下即可。
2.3 首选项配置
首选项里面主要对配置文件编码、Tomcat容器(自动配置)、服务端及客户端配置(自动配置)、第三方服务引擎配置、文件模板配置。
首选项位于主菜单-“window”-“preferences”选项,如下图所示:
选择后会进入到首选项的配置页面,如下图所示:
本节的配置均在首选项中进行。
2.3.1 文件编码配置
ExMobi中所有文件的编码要求为“UTF-8”编码,如果编码不对可能会造成乱码和提交错误等问题。
在首选项中一般需要配置的编码的文件为:HTML、CSS、JS、JAVA、JSP等,如下图所示:
2.3.2 Tomcat配置
Tomcat默认已经是自动配置好的,此处只需要对其进行验证一下即可,如下图所示:
2.3.3 服务端及客户端配置
服务端及客户端的配置也是默认配好的,此处也只需要进行确认即可,如下图所示:
2.3.4 第三方服务引擎配置
服务引擎指的是ExMobi服务端的PNS、DCS、BCS引擎,由于ExMobi服务端支持分布式部署,所以这些引擎是相对独立部署在不同的服务器上的。
如果使用不同服务器上服务,则需要在此处进行配置。
而一般开发者只需要使用本机的服务即可,配置本机服务请看下图:
2.3.5 文件模板配置
MBuilder中提供了对所有可编辑文件格式的模板配置,在创建相应格式文件的时候就可以通过模板快速创建出带一定结构的文件。
本小节以XSL文件为例,说明文件模板的配置。
在首选项中找到XSL文件的templates选项,如下图所示:
点击“edit”按钮后,即可对相应的模板进行修改,也可以点击“new”按钮新建一个模板,如下图所示:
编辑好的模板可以在MBuilder中使用,如下图所示:
2.4 MBuilder主要功能介绍
2.4.1 MBuilder工作界面
MBuilder工作界面如下:
主菜单:MBuilder的所有功能操作菜单。
快捷菜单:常用的MBuilder功能。
应用程序目录结构:应用的完整结构,对应用资源的管理。
代码编辑区域:应用中可编辑资源的编辑区域,可对资源内容进行修改。
控制台和工具区域:主要是开发辅助调试的工具展示区域。
2.4.2 启动Tomcat
Tomcat是ExMobi服务端的运行容器,Tomcat的启动、停止和重启就是ExMobi服务端的启动、停止和重启。
Tomcat的启动、停止、重启操作依次在快捷菜单中,如下图所示:
启动 停止 重启
点击第一个按钮即可启动Tomcat,启动成功会在控制台中看到成功日志,如下图所示:
如果有客户端要接入该服务端,需要配置对应的IP和端口,比如:
这里的IP和端口就是TOMCAT所在服务器的IP和TOMCAT启动的端口。如果是同一台机器,比如PC模拟器可以使用127.0.0.1的IP,当然,使用实际的IP也可以,从MBuilder的控制台可以看出普通端口是8001,加密端口是8443,如果开启“使用数据加密”,需要填写加密端口;如果是用真机设备上的客户端,需要确保设备和MBuilder的网络是通的,并且填写的IP是同一网段的实际IP,端口仍然使用TOMCAT启动的端口。
2.4.3 新建和导入向导
MBuilder的新建和导入向导位于快捷菜单中,如下图所示:
点击该按钮即可新建一个应用或者导入一个已经存在的应用,如下图所示:
继续每一个“next”或有相应的配置,最后一个配置是在应用中使用模板和皮肤主题,如下图所示:
配置完成后点击“finish”即可完成应用的新建或者导入。
2.4.4 自动代码同步
ExMobi应用包含了客户端和服务端资源,而客户端资源是运行在客户端的,服务端资源是运行在服务端的。
在创建应用的时候应用的资源仅仅存在MBuilder的工作空间中,尚未部署到客户端和服务端,所以需要进行代码同步。
MBuilder中提供了自动代码同步的功能,可以把应用的客户端和服务端资源分别同步到PC模拟器基座客户端和ExMobi的DEV开发版中,这样才能在基座中进行开发调试。
自动同步按钮的图标位于快捷菜单中,图标有两种状态:启动和关闭。
如下图所示的状态为自动同步关闭状态,在该状态下应用不会自动同步:
如下图所示的状态为自动同步启动状态,在该状态下应用才会自动同步:
要同步哪个应用需要先选中该应用,然后再点击自动同步按钮,如下图所示:
2.4.5 应用导出
应用开发完成只是运行在MBuilder的开发环境中,要投入生产还需要将完整应用包部署到ExMobi的工程环境中,并且需要在EDN开发者门户中将应用的客户端资源包和基座打成打包客户端。
这里涉及两个包的导出:一个是完整应用包的导出,一个是客户端资源包的导出。
一、完整应用包的导出功能在MBuilder的快捷菜单中,如下图所示:
点击按钮前需要先选中要导出的应用,选中后点击该按钮,会弹出如下导出向导界面:
点击“finish”即可导出指定应用的完整应用包到指定的目录,格式为zip。
二、客户端资源包的导出也是位于快捷菜单,如下图所示:
点击该按钮,即可进入导出客户端资源包的导航页面,如下图所示:
第一步:选择应用的版本:
第二步:选择要生成客户端资源的应用,以及要生成的分辨率、存放目录等,如下图所示:
点击“finish”即可完成应用客户端资源的打包,包的格式为zip。
2.5 PC模拟器基座客户端功能
PC模拟器基座客户端是为了进行开发调试方便而研发的一款产品。它可以最大限度的模拟移动设备终端的效果,使开发达到所见即所得的目的。
2.5.1 打开PC模拟器
打开PC模拟器基座客户端的按钮也位于快捷菜单中,如下图所示:
他主要分为“菜单栏”、“模拟手机客户端界面”、“模拟手机按键”三部分。
2.5.2 菜单栏
菜单栏包含:设置、编辑、工具、性能测试和帮助功能
一、 “设置”部分主要进行三类设置
1) 模拟器模拟的终端平台和型号等,比如:android的三星手机、iOS的iPad2平板等。
2) 界面缩放。有时候模拟pad的设备,在PC机中会显示比较大,可以通过缩放功能对显示的界面进行等比缩放,方便开发查看和切换。
3) 横竖屏切换。横竖屏切换是开发中常遇到的问题,对模拟器进行相应的切换设置可以及时看到效果。
二、“编辑”部分可以对开发展示的界面进行截屏处理。
三、“工具”部分可以启动简易抓包工具、JSON格式数据、打开当前显示页面的源码、复制当前页面地址以及打开PC模拟器的程序目录。
四、“性能测试”部分可以对整个应用的运行性能进行测试,让开发人员了解运行的情况以进行应用的优化。
五、“帮助”部分可以查看模拟器的版本号等信息。
2.5.3 模拟器手机客户端界面
手机客户端界面主要分四部分:演示中心、参数设置、进入基座和关于我们。
一、演示中心:包含烽火星空公司的一系列开发模板、行业应用和成功案例,可以方便开发者或者项目经理对开发进行参考以及项目前期的演示等使用。
二、参数设置:可以设置应用访问相关的一些信息,如:ExMobi服务端的IP、端口、页面动画效果的开关、缓存的设置等。MBuilder中的PC模拟器默认设置的IP和端口使用的是本机的环境。
三、进入基座:该界面与开发者息息相关,在MBuilder中创建的应用经过同步或者应用的安装等操作后即可在该界面实时看到应用运行的效果,方便开发者开发调试。
四、关于我们:在该界面中,主要可以查看模拟器的版本号、IMSI、ESN、模拟的分辨率等信息。
2.5.4 模拟手机按键
手机按键主要有:菜单键、home键、返回键。下图是模拟键和手机按键的对应关系:
2.6 使用MBuilder创建第一个应用
第一个应用很容易就让人想起“Hello World”,没错我们现在就通过创建一个“Hello World”的应用来展示一个“Hello World”的页面。
2.6.1 点击新建应用按钮
在MBuilder中点击新建应用按钮,如下图所示:
点击后就会进到创建应用的导航页面。
2.6.2 配置应用信息
在创建应用的导航页面,填写应用信息,我们的应用名称为“helloworld”如下图所示:
其中:
· Project name 项目名;(必填项)
· Application ID 应用ID,必须和项目名一致;(必填项)
· Application Name 应用名。(必填项)
· Application Version 版本号。(必填项)
· Scope 应用支持的表现形式,是客户端方式的还是wap方式的,还是都支持;
· Access 该应用是否需要网络(network)、gps定位(gps)、拍照(camera);
· HomePage 该应用访问的第一个页面,可以是本地页面,也可以是网络地址。Res开头的为本地页面,http的地址是网络页面。这里使用的是本地页面。
点击“next”按钮进到“设置应用图片”面板
2.6.3 设置开发者信息
在开发者信息设置面板可以填写一些开发者的基本信息,如下图所示:
此步骤一般可以直接跳过。
继续点击“next”按钮,可以进入皮肤模板选择面板。
2.6.4 设置选择皮肤模板
进入“皮肤模板选择面板”后,可以选择使用某个模板的某个皮肤,如下图所示:
本次创建不使用模板,后面的章节中我们会单独对模板的开发和使用进行介绍。
所以该面板我们选择结果如下:
不选择模板则所有的模板选项均不可选择。
点击“finish”即可完成应用的创建。
2.6.5 应用同步和启动Tomcat
应用创建完成后,打开PC模拟器基座客户端,点击“进入基座”,这时候会发现创建的应用并没有在客户端中显示,如下图所示:
可以看到界面中左上角和右上角都有一个操作按钮。
其中左上角的按钮为“应用管理”,点击进去可以看到存在的应用列表;右上角的按钮为“更多选项”,点击进入可以对基座的一些信息和参数进行设置和读取。
点击左上角的按钮,可以看到提示无应用,如下图所示:
为什么我们创建好了应用,但是在应用列表中却看不到应用?
这是因为应用管理是客户端向服务端拉取应用列表。而应用创建于MBuilder的工作空间中,并没有同步部署到ExMobi服务端,所以这时候在客户端是请求不到应用的。
这时候我们就需要做一件事情,那就是“应用自动同步”。
在MBuilder中首先选中要同步的应用,这里选择“helloworld”应用跟目录,然后点击快捷菜单中的“自动同步”按钮,如下图所示:
将“不同步”状态改成“自动同步”状态,如下图所示:
退出基座界面到客户端首页,重新点击“进入基座”,这时候就可以看到新创建的应用已经同步到客户端,如下图所示:
如果继续点击左上角的“应用管理”按钮,可以对已经同步的应用进行卸载和安装,如下图所示:
然后点击“启动Tomcat”的图标把Tomcat启动好。
2.6.6 应用效果查看
当点击应用的“进入”按钮的时候,会提示“链接文件不存在”,如下图所示:
图2-6-6-1
为什么会报这个错误,这里先卖一个关子。我们将通过下一节2.7的内容进行讲解。
2.7 认识应用结构
在上一节中,我们看到图2-6-6-1中的报错,那是因为点击“进入”按钮,实际上是请求应用的首个页面,这个提示的意思就是说首个页面的地址文件不存在。
这就需要先来了解应用的结构。如下图所示是一个应用的基本结构:
应用根目录下有一个文件(config.xml)和两个文件夹(server、client)。
2.7.1 config.xml基本信息配置
config.xml文件为应用的基本信息配置文件,使用MBuilder默认生成的配置内容和说明如下:
这里需要注意的是,homepage项即为应用的入口节点,它支持本地文件(res:开头)也支持网络地址(http://开头)。当点击应用图标,也就是图2-6-6-1中点击”进入”按钮的时候会触发该首页地址。页面上提示“链接文件不存在”意思就是说我们设置的“res:page/index.xhtml”本地页面不存在,因为我们还没有创建该文件。
除此之外还有几个特殊配置项:
配置项 | 功能 | 说明 |
access.land | 横竖屏切换 (重力感应) | 取值为boolean值,false(默认值)代表强制竖屏,true代表横竖屏自动切换。 |
access.orientation | 默认显示方向 | 为应用默认显示的方向,有两个取值——port:设置应用为竖屏模式;land:设置应用为横屏模式;padland_phoneport:设置PAD为横屏模式,phone为竖屏模式;padport_phoneland:设置PAD为竖屏模式;phone为横屏模式。 |
config.theme | 皮肤设置 | 取值为对应皮肤包的id。 |
homepage.defaultsrc | 网络连接错误的处理页面 | 只支持本地静态页面,如果不设置,则跳转到基座的设置页面。 |
2.7.2 client客户端资源文件夹
client文件夹下包含page、css、script、theme等子目录,存放的都是客户端资源。所谓客户端资源就是运行在客户端的文件。
由于不同终端存在分辨率不同的问题,所以在client的子目录下都区分了phone和pad目录,再往下又分为default(默认资源)、ldpi(低分辨率资源)、mdpi(中分辨率资源)、hdpi(高分辨率资源)、xhdpi(超高分辨率资源),以image目录为例,如下图所示:
对于DPI的划分,可以参考附录14.1。
这时候客户端读取的资源就是client/image/phone/hdpi目录下的资源。到ExMobi的PC模拟器的apps目录,找到应用image的目录地址,比如:C:\developer\MBuilder\ExMobi\apps\helloworld\image,可以看到如下图片:
可以看到,同步到客户端的资源都没有了分辨率的信息。
其实,当打开PC模拟器的时候,在模拟器标题栏可以看到如下信息:
从右往左看,它表明的是当前模拟器模拟的是终端类型为phone,DPI为hdpi。
客户端使用什么资源就是根据客户端的信息来读取相应的资源的。
如果客户端要使用logo.png这个图片,中间的分辨率信息是不需要写的,也就是说正确的引用方法为res:image/logo.png即可引用logo.png图片。
需要特别说明default目录。该目录是公共文件目录,也就是说不管是哪个DPI的客户端都可以使用default目录的资源。Default目录的资源跟DPI下的资源一样也会同步到image目录下,不含有default目录本身。所以如果default目录和DPI目录如有有相同名字的资源,那么DPI下的资源会覆盖default下的资源。
所以可以总结客户端资源使用的规则为:客户端使用的资源根据客户端的类型(phone、pad)和DPI信息(ldpi、mdpi、hdpi、xhdpi)找到对应的dpi资源,同时也会去找default下的资源,如果DPI下有和default下同名的资源,那么使用的将会是DPI目录下的资源。
根据本节前面的提示信息“链接文件不存在”确定是因为尚未创建首页地址“res:page/index.xhtml”,所以接下来我们可以在client/page/phone目录下的default目录或者hdpi目录下创建index.xhtml文件。在default目录点击右键,如下图所示:
即可打开创建新XHTML页面的面板,如下图所示:
修改文件名为“index.xhtml”,然后点击“finish”即可创建完毕页面,并在MBuilder中打开默认页面,如下图所示:
再次点击“进入”应用就不会报错,并且显示该页面信息,如下图所示:
在开发的过程中,可以使用这些可以允许的客户端资源来进行展示和脚本操作。
2.7.3 server服务端资源文件夹
server文件夹用于存放在服务端执行的文件。其目录结构如下:
其中,JSP文件夹用于存放JSP文件;xsl文件夹用于存放xsl文件;mapp.xml文件为服务端的规则配置文件,是一个统一的全局配置。
mapp.xml配置文件其功能主要有:
1) 配置处理第三方系统的JSP文件。
2) 设置第三方系统的伪域名。
3) Push推送频道的配置。可执行Push消息的推送。
4) 数据库资源配置。可以指定多个数据源。可在JSP中使用配置好的数据源。
5) 定时器配置(定时执行某个请求)。
6) 多媒体类型映射(为特殊contenttype指定文档格式,如doc、ppt等)。
7) Session类型设置(默认是有session限制,如果为新浪网等新闻类系统可以设置无session限制)。
8) 文档预览和下载缓存配置。可以设置缓存的天数。
其常用配置如下表:
英文名称 | 中文名称 | 描述 |
用于标识符合MAXML规范的xml文件 | ||
用来表明路由节点信息元素, 其baseaddr属性为第三方系统的域名/ip地址,该域名可以为伪域名 | ||
_PAGE元素转发元素 | 用来对每个请求处理进行转发元素,通过pattern属性正则匹配第三方系统的地址,然后转向path属性设置的JSP进行处理。 | |
配置元素 | 应用相关配置元素 | |
应用伪域名元素 | 用于应用伪域名配置,address属性指定第三方系统的真实域名/ip,name属性指定伪域名。通过配置,整个应用不管是xhtml、jsp还是mapp.xml的route配置中都可以使用伪域名。 | |
<database/> | 数据源元素 | 数据库的数据源配置,用于在JSP中作为数据源直接调用。 |
推送元素 | 用来表明多组推送频道元素,其authpage属性 | |
<pushchannel/> | 推送频道元素 | 用来设置一个推送频道的处理策略,必须包含一个id作为频道的唯一标识,path为轮循推送必须的处理JSP,并且needsubscribed指明是否为订阅频道,同时需要设置corn子标签指明推送的周期间隔。 |
<services/> | 接口元素 | 用来表明多组接口服务元素。 |
<http-service/> | http接口服务元素 | 提供第三方系统调用的http接口服务,必须配置pattern属性指明访问路径,配置path路径指明中间件对接收到的数据的处理JSP页面。 |
一个mapp.xml最基本的内容一般为:
<maxml version="2.0" xmlns="http://www.nj.fiberhome.com.cn/map" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.nj.fiberhome.com.cn/map maxml-2.0.xsd"> <route baseaddr="http://domain"> <!-- 登陆页面get --> <forward pattern="/app/template/login.jsp" path="login.jsp"/> <!-- 登陆校验post带键值对 --> <forward pattern="/app/template/checkLogin.jsp" path="checkLogin.jsp"/> <!-- 新建任务get --> <forward pattern="/app/template/jsp/addTask.jsp" path="addTask.jsp"/> <!-- 保存任务post带附件 --> <forward pattern="/template/action/taskManagerAction.jsp\?handler=save" path="taskManager.jsp"/> <!-- 列表展示post带XML请求体 --> <forward pattern="/app/template/action/taskManagerAction.jsp\?handler=list&dataType=xml.*" path="taskManagerListXML.jsp"/> <!-- 列表展示post带JSON请求体 --> <forward pattern="/app/template/action/taskManagerAction.jsp\?handler=list&dataType=json.*" path="taskManagerListJSON.jsp"/> </route> <config> <!-- 为第三方系统的实际访问地址配置一个简写域名domain,以后所有请求前面部分都可以使用domain代替,route的baseaddr也可以写为domain --> <domain address="miap.cc:1001" name="domain"/> </config> </maxml> |
里面最常用的就是route请求路由配置。实际上,客户端的所有http请求并不是立即触发的,而仅仅是告诉服务端要触发http请求,真实的请求是在服务端的JSP里面触发的。那么客户端的http请求如何知道是哪个服务端的JSP来处理自己的请求?route配置其实就起到了桥梁的作用,通过route配置,可以为客户端的请求配置处理的JSP文件进行逻辑处理。
在helloworld应用的index.xhtml中增加一个“ExMobi门户网站”的超链接:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>Hello World</title> <script> <![CDATA[
]]> </script> </head> <body> <a href="http://www.exmobi.cn/">ExMobi门户网站</a> </body> </html> |
其效果如下:
当点击该超链接的时候,并没有实际的去请求“ExMobi门户网站”,而是先在mapp的route里面去找是否有该请求对应的处理JSP。因为还没有配置,页面会报错,如下图所示:
该错误信息可以在MBuilder的响应码查询中进行查看报错原因,如下图所示:
途中提到的“需要配置添加相应的应用处理页面”指的就是服务端的处理JSP。也就是说还没在route中配置处理的JSP文件。
现在可以在mapp.xml中配置route如下:
<?xml version="1.0" encoding="UTF-8" ?> <maxml version="2.0" xmlns="http://www.nj.fiberhome.com.cn/map" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.nj.fiberhome.com.cn/map maxml-2.0.xsd"> <config> <htmlformat wellformat="true" /> </config>
<route baseaddr="http://www.exmobi.cn"> <forward pattern="/" path="index.jsp"/> </route> </maxml> |
这就指明了“http://www.exmobi.cn/”这个请求处理的JSP名为“index.jsp”。所以还需要在sever目录的JSP子目录中新建index.jsp,如下图所示:
在弹出的新建面板中输入文件名,如下图所示:
即可创建默认的JSP文件,如下图所示:
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%> <!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>Hello World</title> <script> <![CDATA[
]]> </script> </head> <body> This is the page content. </body> </html> |
重新点击客户端的“ExMobi门户网站”链接就可以看到一个新的页面,如下图所示:
特别的,如果为第三方地址配置的JSP的文件名和第三方地址的文件名刚好一样,这时候是可以不用配置MAPP路由的,它会自动智能匹配处理的JSP文件。
比如客户端发起如下几种格式的URL请求,他们的特点是名称都为action:
http://domain/action.jsp
http://domain/action.jsp?type=1
http://domain/action.jsp?type=2
http://domain/action.do?type=1
http://domain/action.php?type=2
http://domain/action?type=3
如果在jsp目录新建一个文件action.jsp,由于其名称也是action,在不给上述URL配置MAPP路由的情况下,服务端会自动匹配该action.jsp进行处理。
所以对于basic@fiberhome应用里的几个配置项:
<!-- 登陆页面get -->
<forward pattern="/app/template/login.jsp" path="login.jsp"/>
<!-- 登陆校验post带键值对 -->
<forward pattern="/app/template/checkLogin.jsp" path="checkLogin.jsp"/>
<!-- 新建任务get -->
<forward pattern="/app/template/jsp/addTask.jsp" path="addTask.jsp"/>
其实是可以不用配置的,因为第三方系统的URL和处理的JSP名称一致。
所以对于一些名称一样,参数不一样的第三方系统URL,最好配置的PATH不要以该URL的名称命名MAPP路由的JSP文件,这样可能导致逻辑混乱。
3 准备工作
3.1 集成开发环境检查
以下各项请逐个检查:
n 检查ExMobi集成开发环境MBuilder是否已经安装好。
n 运行Tomcat看是否能正常启动。
n 启动客户端PC模拟器,在“系统设置”中配置为本机IP和Tomcat运行的端口(8001),并点击“应用管理”菜单看是否能正常进入。
注:没有安装、配置或者出现运行异常的请参照《MBuilder安装手册》。
3.2 辅助开发工具介绍
辅助开发工具主要是帮助开发人员在不写代码的情况来分析第三方系统的交互情况,并进行模拟和取值,方便在写代码前理顺思路。
名称 | 作用 | 下载方式 |
HTTPAnalyzerStdV2 | 抓包和网络请求分析工具 |
|
Regex Util | 正则校验工具 | 已经集成在MBuilder中 |
XMLSpy | XPATH校验工具,SOAP模拟请求工具 |
|
JMeter | 模拟http请求 |
|
3.3 使用二次开发手册
二次开发手册在MBuilder菜单中的“Help-》Help Contents”中,如下图所示:
点击后的帮助界面有一些二次开发的相关文档,其中就包含二次开发手册:
其中的“ExMobi二次开发手册”为客户端和服务端的API手册,开发者可以方便的查找API来实现不同的业务场景功能。
3.4 基础知识准备
使用ExMobi进行开发需要大致了解:HTML、JavaScript、CSS的基本语法;正则表达式的用法;XML的文档结构以及XPATH的基本语法;TCP/IP网络协议的原理和报文分析。
第2篇ExMobi编程基础
4 客户端XHTML编程
4.1 XHTML概述
XHTML是ExMobi客户端的标记语言。与W3C规范的XHTML不同的是,ExMobi的XHTML是对标准HTML的集成和扩展——继承适合在移动终端使用的控件,并扩展更多新的控件更方便移动应用的开发和移动终端的展示。
后续提到的XHTML均指代ExMobi的XHTML。
4.1.1 使用XHTML的好处
移动应用开发涉及多平台开发,不同平台开发语言不同,所以能够统一、简单、快捷的进行开发是选择使用XHTML的最大原因。
因为使用XHTML有诸多好处:
1) 开发语言统一。XHTML只是一种描述性标记语言,不同平台的ExMobi的客户端都可以解析正确语法的XHTML。所以开发者只需要编写一套XHTML代码即可在不同平台上运行。
2) 代码结构简单。XHTML代码结构采用的是XML结构,层次分明。
3) 代码简洁,表达丰富。XHTML只需要给指定的标签设置不同的属性和样式即可展现丰富的界面效果。
4) 可重用性好。XHTML由于其XML特性,故可以很方便的进行模板化以达到最大化的可重用性。
5) 可扩展性好。XHTML继承自ExMobi强大的组件内核,可以很方便的进行扩展出更多原本没有的控件,根据不同的使用场景进行插件的定制开发。
6) 纠错方便。XHTML代码必须符合ExMobi的规范,此规范是验证XHTML代码正确性的标准,在XHTML代码中只要声明使用此规范即可进行快速纠错。
4.1.2 XHTML的基本结构
XHTML遵循ExMobi的语法定义,并严格符合XML的语法规范。
其基本结构如下:
其中:
1) 第1-2行是文档规范声明,意思是文档内容必须符合ExMobi语法规范。
2) 第3行表示XHTML内容的开始,第18行表示XHTML内容的结束。
3) 第4-12行是head头信息声明区域,可以设置字符编码、title标题信息、JS脚本和CSS样式表等。
4) 第5行是声明页面编码。
5) 第6行是设置title标题头的信息。
6) 第7-11行是JS代码块,其中第9行为JS中的代码注释写法。
7) 第13-17行为body区域,用于显示页面的主体内容,其中第14行是XHTML中的代码注释写法。
除此之外,在XHTML中可以添加不同功能的控件、JS和CSS,后续章节中会有详细介绍。
4.1.3 符合XML规范的XHTML
XHTML是一种特殊的XML。所以,XML中的关键字需要被转义。XML中已经定义的转义字符包括:
实体 | 字符 | 含义 |
< | < | 小于号 |
> | > | 大于号 |
& | & | 和 |
' | ’ | 单引号 |
" | " | 双引号 |
修改helloworld应用的index.xhtml页面如下所示:
由于该页面中含有&没有转义为&所以导致页面不符合XML,所以打开此页面会报错,如下图所示:
他会明确告知是第14行错误,将&字符转义为&后再看页面效果:
CDATA块是XML中的特殊区块,CDATA中的内容不会被解析器解析,而是原样输出。所以一般JS的内容都是很不确定的,并且JS语法经常使用引号、大于小于号等XML的关键字,所以JS内容通常包在CDATA中,这些内容就不需要进行转义。
但是JS中有一个很常用的功能——innerHTML,改属性是给控件设置内部HTML内容。在ExMobi中,作为显示的HTML片段如果包含XML的关键字也是需要转义的。
而JS中如果是给某个控件赋值,比如obj.value = str;这样的赋值,如果value中包含XML的关键字是不需要转义的。
所以,符合XML规范的XHTML应该具备如下特征:
1) xml中使用
a) text内
' 与 ' 均代表 '
" 与 " 均代表 "
> 与 > 均代表 >
& 代表 &,不支持直接放置 &
< 代表 <,不支持直接放置 <
b) 属性内
' 与 ' 均代表 '
> 与 > 均代表 >
" 代表 "
" 当属性通过 ‘ ‘ 包裹时,可直接放置 “,当属性通过 ” “包裹时,必须用"
& 代表 &,不支持直接放置 &
< 代表 <,不支持直接放置 <
2) js中使用 注:为防止xml转义引起歧义,js语句必须被<![CDATA[ ]]>包裹
a) 通过innerHTML构建控件,控件属性或者text中
< 代表 <
> 代表 >
& 代表 &
' 代表 '
" 代表 "
如:
function change(){ var ctrl = document.getElementById("mydiv1"); ctrl.innerHTML = "<textarea id=\"mytextarea1\"><>'"&</textarea><div id=\"mydiv2\" href=\"<>'"&\"><>'"&</div>"; var ctrl2 = document.getElementById("mydiv2"); alert(ctrl2.href); } |
结果为:<>/"&
b) 通过js设置控件属性
< 代表 <
> 代表 >
& 代表 &
' 代表 '
" 代表 "
如:
function change(){ var ctrl = document.getElementById("mytextarea1"); ctrl.value = "<>'"&"; } |
设置后结果为:<>'"&
\" 代表 "
\' 代表 ‘
& 代表 &
< 代表 <
> 代表 >
如:
function change(){ var ctrl = document.getElementById("mytextarea1"); ctrl.value = "\"\'&<>"; } |
设置后结果为:"'&<>
4.1.4 XHTML文档类型的声明
XHTML是一种语法规范,符合XHTML规范的文档称为XHTML文档。
有两种方式声明XHTML文档类型:
1) 静态文件后缀名为xhtml。该种方式仅针对静态文件,就是创建一个本地的页面,其后缀为xhtml格式。Helloworld应用中的首页地址index.xhtml就是一个xhtml文档。
2) 动态文件声明响应content type为“application/uixml+xml”。动态文件是指JSP、PHP、.NET等语言开发的页面,而content-type是http协议中规定文档格式的头信息。如果这些页面是要在ExMobi客户端中进行显示,必须声明响应的content- type为“application/uixml+xml”。比如helloworld应用的index.jsp就声明了content-type,如下所示:
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> |
但是无论是哪种方式,一旦声明了文档类型为XHTML,内容则必须符合XHTML的规范,否则在ExMobi客户端中将无法进行正确解析和展示。
4.2 XHTML与JS、CSS的关系
JS是JavaScript的简称,是一种客户端脚本语言,ExMobi客户端可以执行XHTML页面中的JS;CSS是层级样式表,可以渲染XHTML页面的控件显示效果。
4.2.1 JS的引用和使用
JS有两种使用方式,一种是直接在XHTML中的script代码块中编写JS代码;另一种是把JS代码写在本地的JS文件中,通过script标签引用本地的JS文件即可使用。如下所示:
注意:XHTML里的JS无法引用网络侧的JS文件。
比如下面的引用是错误的:
<script src="http://domain/script/index.js"></script>
|
4.2.2 CSS的引用和使用
CSS有三种使用方式,第一种是直接在XHTML中的style代码块中编写CSS代码;第二种是把CSS代码直接写在某个控件的style属性中;第三种把CSS代码写在本地的CSS文件中,然后通过link控件引用本地的CSS文件。如下所示:
通过方法一和方法三使用CSS,只能通过类选择器和派生选择器进行引用。而且派生选择器只支持一级。
上图方法一的就是类选择器的用法,即给控件设置class样式名,然后在创建一个以该名为引用的样式。
派生选择器的用法为控件的名称即为样式的引用,比如要给一个a控件写样式可以这么写CSS:
a{ font-weight:bold; } |
4.3 XHTML与JSP的关系
JSP是一种动态语言,属于服务端语言中的一种,并不直接运行在客户端。所以JSP的运行是在依赖于运行容器的,比如Tomcat。容器运行后会得到一个执行结果,执行结果可以返回给客户端进行操作。
所以ExMobi中的JSP是运行在ExMobi的服务端的,其响应的内容文档可以是XHTML,也可以是XML、JSON等一些比较常用的数据格式,还可以是doc、ppt、jpg等office文档格式或者图片格式。
通常,如果JSP响应结果直接作为页面展现,则需要输出为XHTML文档,这时候需要声明JSP的content-type为“application/uixml+xml”;如果只是作为数据需要进一步的处理,比如页面中发起的AJAX请求的JSP响应可以输出为XML或者JSON等方便JS操作的格式,同时也需要把content-type设置为相应数据格式的类型,比如JSON数据的头信息为“application/json”。
4.4 XHTML控件分类
XHTML是由不同控件组合而成,这些控件主要分为6大类:语义控件、顶级容器控件、布局控件、导航控件、表单控件和多媒体控件。
4.4.1 语义控件
所谓语义控件,就是指只有一定的含义,但是不会在客户端中进行展现的控件。比如:html、script、link、meta、style、head等控件都是语义控件。
其中html控件为XHTML文档的根容器控件,一个XHTML文档以html控件为开始。
4.4.2 顶级容器控件
顶级容器控件是XHTML中的主容器控件,这些控件都只能位于html根容器之下,并且互相不能嵌套。
顶级容器控件包括:body(内容主体容器)、dialog(对话框容器)、fix(绝对定位容器)、footer(固定底部容器)、header(固定顶部容器)、leftcontainer(左侧区域容器)、rightcontainer(右侧区域容器)、title(标题区域容器)。
其中,dialog和fix容器是浮动在其他容器之上的,不会占用其他容器的区域;而剩下的其他6个容器的区域共同铺满整个XHTML的屏幕页面,并且如果其中一个不使用或者不显示,其所占区域会让渡给其他的容器使用,其他容器仍然铺满整个XHTML的屏幕页面。
可以通过如下代码:
<html> <head> <meta charset="UTF-8"/> <title>title区域</title> </head> <header id="header" style="background-color:#aaaaaa"> header是固定在头部的容器,不出现滚动条 </header>
<leftcontainer id="lefter" style="background-color:#bbbbbb;width:70;"> <div style="height:1001;"> leftcontainer是固定在左边的容器,内容超出会显示滚动条 </div> 下面没有内容了 </leftcontainer>
<body style="background-color:#cccccc"> <div style="height:1001;"> body中的内容,超过会有滚动条。必须显示,不能隐藏。 <a href="document.getElementById('header').style.display='none'">隐藏header</a> <br/> <a href="document.getElementById('footer').style.display='none'">隐藏footer</a> <br/> <a href="document.getElementById('lefter').style.display='none'">隐藏leftcontainer</a> <br/> <a href="document.getElementById('righter').style.display='none'">隐藏rightcontainer</a> <br/> <a href="document.getElementById('fixer').style.display='none'">隐藏fix</a> <br/> 可以看到,除了header、footer、leftcontainer、rightcontainer以外剩下的区域都属于body,并且不受fix的影响,所以,如果这些容器不写,则body的空间就会更大。 </div> 下面没有内容了 </body>
<fixset> <fix id="fixer" style="top:70%;left:0;background-color:#ffffff;"> fixset可以包含多组fix容器,fix是绝对定位的容器,悬浮于窗口之上,所以可以看到其内容可以横跨<br/> header、footer、body、leftcontainer、rightcontainer<br/> 可以设置其left或者right之一以及top或者bottom之一来实现绝对定位 </fix> </fixset>
<rightcontainer id="righter" style="background-color:#dddddd;width:70;"> <div style="height:1001;"> rightcontainer是固定在右边的容器,内容超出会显示滚动条 </div> 下面没有内容了 </rightcontainer>
<footer id="footer" style="background-color:#eeeeee;"> footer是固定在底部的容器,不出现滚动条 </footer> </html> |
其效果如下:
将header、footer、leftcontainer隐藏后的效果:
继续隐藏rightcontainer和fixset后的效果:
可以看到原来header、footer、leftcontainer、rightcontainer的区域都给了body,而fixset只是隐藏对body没有影响。
title区域在页面中不能改变显隐状态,默认是显示的,如果需要隐藏可以给title控件加属性show="false"来隐藏。上面代码设置后的显示效果如下:
4.4.3 布局控件
布局控件提供不同粒度的控件来对页面布局提供强有力的保证。
独立布局控件为最小粒度的布局控件,它们只能对自身的布局进行控制。如:a、artfont(艺术字)、br、font、hr等。
整体布局控件本身是具有固定结构的一个整体,它可以对内部的特定元素进行布局。如:table、tree(树控件)、htmlgroup(页面组)等。
局部布局控件可以针对部分的其他控件进行布局调整,它不想整体布局控件那样有固定结构。比如:div、page(页容器)、scroll、slidingcontainer、h(横向布局)、v(纵向布局)等。
实际上,除了布局控件外,其他控件也都可以对自身进行布局。只是布局控件的作用是以布局为主。
4.4.4 导航控件
导航控件顾名思义就是做引导用的,方便用户能够知道如何进行操作、引导用户操作等。
导航类控件主要通过简单的标题和图标指示用户的操作。比如:list(动态列表)、listitem(单行、双行列表)、titlebar(标题栏,可代替title控件)等。
菜单类控件通常是提供一组相似功能的菜单给用户选择使用。比如:grid(九宫格)、animationmenu(动画菜单)、contextmenu(弹出菜单)、menubar(底部导航菜单)、tabbar(顶部导航菜单)等。
引导类控件通过一些简单的动作引导用户操作。比如:dragrefresh(拉拽刷新)、marquee(跑马灯)、toggle(显隐组)等。
4.4.5 表单控件
表单控件主要用于表单页面的展现以及数据的提交,可以提交值的控件都有value属性。
要作为表单提交的两个必要条件为:
1) 要作为表单提交的表单控件必须放置于form控件中;
2) form控件的enctype属性必须为: application/x-www-form-urlencode(普通提交方式)或者multipart/form-data(带有附件的提交方式),不写则默认为application/x-www-form-urlencode。
部分控件在form表单中的提交值如下:
控件标签 | 控件提交值 |
<eselect>可编辑选择框 | 编辑框value值 |
<handsign>手写签名 | 手写生成文件路径(sys 开头) |
<input:autocompletetext>记忆框 | 编辑框value值 |
<input:camera>拍照摄像 | 拍照生成文件路径(sys: 开头) |
<input:checkbox>复选框 | 相同name值value组成字符串,以&连接 如 奥迪&奔驰 |
<input:decode>动态解码 | 编辑框内解码结果 |
<input:file>文件选择框 | 已选中文件路径(sys: 开头) |
<input:password>密码框 | 编辑框内密码值 |
<input:radio>单选框 | 单选框控件value值 |
<input:text>输入框 | 编辑框内输入值 |
<object:date>日期选择框 | 已选中日期值 |
<object:time>时间选择框 | 已选中时间值 |
<select>选择框 | 已选中option项value值 |
<textarea>多行文本域 | textarea输入值 |
<handwriting>增强手写 | 手写完成后生成图片文件的完整路径(sys:开头) |
<photoupload>增强型拍照控件 | 手写完成后生成图片文件的完整路径(sys:开头),多个文件以&连接 |
<input:record>录音 | 录音完成后生成的录音文件路径(sys:开头) |
<switch>开关 | value值 |
可以看出来,部分表单控件具备了调用本地能力的功能,比如:input:camera可以调用摄像头、input:record可以调用录音功能等。
4.4.6 多媒体控件
所谓多媒体控件是指使用了多种元素进行展现的控件。比较常用的多媒体控件有:
控件标签 | 说明 |
<baidumap> | 百度地图 |
<browser> | 浏览器控件 |
<fileset> | 附件集,进行附件的预览、下载、打开、签批等 |
<gaodemap> | 高德地图 |
<img> | 图片控件 |
<photoupload> | 增强型拍照控件 |
4.5 XHTML常用界面展现
4.5.1 基本文字展现
中间件客户端支持对文字多样性的展现,比如:文字的大小、颜色、样式等,也能给文字添加超链接,使文字能够与其他页面进行交互。如下面的页面效果:
其代码实现如下:
<html> <head> <meta charset="UTF-8"/> <title>基本文字展现</title> </head> <body> <br/> <font>一般用font修饰字体,这是没有经过修饰的字体</font> <br/> <font style="font-size:large;">将文字变大</font> <br/> <font style="font-size:small;">将文字变小</font> <br/> <font style="color:#ff0000;">给文字添加颜色</font> <br/> <font style="font-weight:bold;">将文字加粗</font> <br/> <font style="font-style:italic;">将文字显示为斜体</font> <br/> <font style="text-decoration:underline;">给文字添加下划线</font> <br/> <font style="font-size:small;color:#00ff00;font-weight:bold;font-style:italic;text-decoration:underline;">综合使用样式的文字,可以任意组合</font> <br/> <div style="font-size:large;color:#0000ff;font-style:italic;">很多控件可以修饰文字,这是在div中修饰文字</div> <br/> <br/> <a href="res:page/interactive.xhtml">给文字添加超链接,不加样式</a> <a href="res:page/layout.xhtml" style="color:#ff0000;text-decoration:none;">这是一个超链接,去掉了下划线并设置了颜色</a> </body> </html> |
4.5.2 页面跳转和超链接
页面跳转可以通过超链接来实现,能够通过超链接打开的页面类型有两种:
页面类型 | 地址特征 | 示例 |
应用中的本地文件 | res:开头 | <a href="res:page/index.xhtml">打开一个本地页面</a> |
网络地址 | http://开头 | <a href="http://www.exmobi.cn">合作伙伴门户</a> |
超链接通常搭配target属性使用,指定为新打开一个页面(_blank)还是在本页面中打开(_self)。
任何页面类型都可以通过以下方式触发:
触发方式 | 示例 |
href | <div href="http://www.nj.fiberhome.com.cn">烽火星空门户网站</div> |
onclick | <input type="button" value="请点击" οnclick="res:page/index.xhtml"/> |
JS | window.open("res:page/index.xhtml"); |
任何具有href、onclick的控件都能触发超链接,而且超链接除了可以打开页面,也可以进行其他的交互,如下表:
交互类型 | 特征 | 示例 |
应用资源 | res:开头 | <img src="res:image/logo.png"/> <script src="res:script/exmobijs/base.js" /> <a href="res:page/index.xhtml">打开一个本地页面</a> |
客户端资源 | sys:开头 | <a href="sys:data/sys/help.xhtml">客户端帮助页面</a> |
终端资源 | file:开头 | <a href="file:SD/text.txt ">安卓系统文件</a> |
网络资源 | http://开头 | <a href="http://www.exmobi.cn">合作伙伴门户</a> |
FTP资源 | ftp://开头 | <a href="ftp://admin:111@192.168.100.231/home/gaea/ ">可以指定FTP账号密码ip目录等信息</a> |
JS函数 | JS语法 | <a href="window.open('res:index.xhtml')">用JS打开一个页面</a> |
拨打电话 | tel:开头 | <a href="tel:17584068201">拨打电话</a> |
发送短信 | sendsms:开头 | <a href="sendsms:17584068201,13813900000:true:节日快乐">短信群发</a> |
调用手机程序打开文件 | open:开头 | <a href="open:file:SD/ppt培训.pdf ">打开安卓文件 </a> <a href="open:http://192.168.100.131:8001/Test/file/mp4/cat.mp4 " target="_blank " >cat.mp4播放</a> |
调用浏览器 | browser:开头 | <a href="browser:http://wap.sohu.com">进入WAP主页</a> |
下载文件 | download@http://开头 | <a href="download@http://192.168.1.doc/test/tywt.doc">下载天下无天.doc</a> <a href="download@http://192.168.1.doc/test/tywt.doc">下载天下无天.doc</a> |
内置脚本 | script:开头 | <a href="script:close">返回上一页</a> <a href="script:exit">退出客户端</a> |
4.5.3 基本布局
ExMobi采用的是流式布局的原则对界面控件进行布局。所谓流式布局是指在容器(区域控件和布局控件)中,从左到右、至上而下对该容器里面的控件进行布局,当一行不能容纳的时候自动换行。
横向布局可以设置容器控件(如:div、font等)的text-align属性来指定子控件的布局,也可以使用控件本身的align属性指定本控件在父容器中的布局,他们的属性值是一样。比如:center为居中、left停靠左侧、right停靠右侧显示。
纵向布局只能通过div的text-valign属性来指定子控件的布局,比如:top为停靠容器顶部、middle垂直居中、bottom停靠容器底部显示。也可以通过br、hr对内容进行换行和分隔。
内外边距可以使容器和控件间产生一些间距。其中外边距margin可以使容器或者控件与其他容器控件产生间距;内边距padding可以使容器和它内部的容器或者控件以及文本产生间距。也可以给控件添加border边框,并设置边框的弧度。
需要注意的是,控件的宽度width和高度height已经包含了margin、padding和border的大小。而一般控件的margin、padding和border都会有默认值。所以实际显示的控件大小应该是宽度或者高度减去margin、padding和border后剩下的大小。
比如下面代码:
<html> <head> <meta charset="UTF-8"/> <title>基本布局</title> </head> <body> <font style="color:#ff0000">横向布局</font><font>:给div设置text-align(对子容器有效)和给button设置align(对自身有效)</font> <div style="border-size:1dp;padding:4 4 4 4;text-align:center"> <input type="button" value="给div设置text-align:center"/> </div> <div style="border-size:1dp;padding:4 4 4 4; "> <input type="button" value="给button设置align:center" style=" align:center;"/> </div> <font>下面的两个div在一行显示,各占50%,各div内部左边的内容占40%,右边的输入框占60%,并且div设置了外边距(margin),让div之间有间隔:</font> <div style="width:50%;border-size:1dp;margin:0 0 0 2%"> <font style="width:30%;text-align:right;">姓名</font> <input type="text" style="width:70%"/> <font style="width:30%;text-align:right;">性别</font> <input type="text" style="width:70%"/> </div> <div style="width:50%;border-size:1dp;margin:0 0 0 2%"> <font style="width:30%;text-align:right;">班级</font> <input type="text" name="class" style="width:70%"/> <font style="width:30%;text-align:right;">年级</font> <input type="text" name="grade" style="width:70%"/> </div> <font style="color:#ff0000">纵向布局</font><font>:div设置text-valign(对子容器有效):</font> <div style="border-size:1dp;height:100;text-valign:middle;"> <font>文字</font><input type="text" style="width:40%" /> </div> <font>下面的的div设置了上下内边距(padding)都为30,也可以达到垂直居中的效果:</font> <div style="border-size:1dp;padding:30 0 30 0;"> 这里的文字跟div有上下间距 </div> </body> </html> |
实现效果如下:
除了使用div布局,还有其他框架布局控件:page(页容器)、scroll(滚动容器)、 slidingcontainer(左右滑动容器)、table(表格容器)、tree(树容器)。
布局的基本原则是:
1) 框架布局控件多用于构建一个页面的框架结构;
2) 其他布局控件(如:div、font等)则是在进行微调的时候使用。
3) 应尽量避免使用div来实现大量布局和嵌套使用以提升界面显示的效率。
4) 表单控件具有布局能力。表单控件本身能布局的不要使用布局控件代替。
4.5.4 使用导航
常见的导航一般有位于底部的菜单导航(menubar)、九宫格导航(grid)、列表导航(listitem)、分页、位于顶部的tab导航(tabbar)。
通过下面代码认识它们:
<html> <head> <meta charset="UTF-8"/> <title>导航页面</title> <style> .tabbar{ height:40; border-size:1; padding:8 4 0 4; } .tab{ height:40; width:33%; border-size:1; border-radius:8; padding:4 4 4 4; margin:0 4 0 4; text-align:center; text-valign:middle; background-click-color:#dddddd; } .current{ background-color:#cccccc; } </style> </head> <header>
<tabbar showtype="text"> <tab text="tab1页" selected="true" bindpage="page1"/> <tab text="tab2页" bindpage="page2"/> <tab text="tab3页" bindpage="page3" />
</tabbar>
</header> <body> <page id="page1"> <grid style="color:black;click-color:#ff8800;color-current:#0000ff;cell-background-click-color:#cccccc;cell-background-radius:4;"> <cell text="列表控件" href="res:page/listitem.xhtml" icon="res:image/icon_man.png" /> <cell text="拖动列表" href="res:page/list.xhtml" icon="res:image/icon_man.png" /> <cell text="功能模块" icon="res:image/icon_man.png" /> <cell text="功能模块" icon="res:image/icon_man.png" /> <cell text="功能模块" icon="res:image/icon_man.png" /> <cell text="功能模块" icon="res:image/icon_man.png" /> <cell text="功能模块" icon="res:image/icon_man.png" /> <cell text="功能模块" icon="res:image/icon_man.png" /> <cell text="功能模块" icon="res:image/icon_man.png" /> <cell text="功能模块" icon="res:image/icon_man.png" /> <cell text="功能模块" icon="res:image/icon_man.png" /> <cell text="功能模块" icon="res:image/icon_man.png" /> </grid> </page> <page id="page2"> 这是第二页的内容 </page> <page id="page3"> 这是第三页的内容 </page> </body> <footer> <menubar showtype="mix" style="menu-background-current-color:#000000;"> <menu text="待办" bindpage="page1" icon="res:image/menubar-icon-daiban.png" currenticon="res:image/menubar-icon-daiban.png"/> <menu text="新建" bindpage="page2" icon="res:image/menubar-icon-xinjian.png" currenticon="res:image/menubar-icon-xinjian.png"/> <menu text="通讯录" bindpage="page3" icon="res:image/menubar-icon-tongxunlu.png" currenticon="res:image/menubar-icon-tongxunlu.png"/> <menu text="公告" icon="res:image/menubar-icon-tongzhigonggao.png" currenticon="res:image/menubar-icon-tongzhigonggao.png"/> <menu text="更多" icon="res:image/menubar-icon-gengduo.png" currenticon="res:image/menubar-icon-gengduo.png"> <submenu text="加班" icon="res:image/icon_jiaban.png" clickicon="res:image/icon_jiaban.png"/> <submenu text="请假" icon="res:image/icon_qingjia.png" clickicon="res:image/icon_qingjia.png"/> <submenu text="调休" icon="res:image/icon_tiaoxiu.png" clickicon="res:image/icon_tiaoxiu.png"/> </menu> </menubar> </footer> </html> |
效果如下:
其中tabbar和menubar可以与page控件进行绑定,达到可以通过左右滑动局部却换内容。
列表导航(listitem)一般分为单行(oneline)和双行(twoline),代码如下:
<html> <head> <meta charset="UTF-8"/> <title>列表展示</title> </head> <body> <!-- 单行列表 --> <listitem type="oneline" href="res:page/index.xhtml" icon="res:image/icon_user.png" caption="列表caption" ricon="res:image/icon_arrow.png" /> <listitem type="oneline" href="res:page/index.xhtml" icon="res:image/icon_user.png" caption="列表caption" ricon="res:image/icon_arrow.png" /> <!-- 双行列表 --> <listitem type="twoline" href="res:page/index.xhtml" icon="res:image/icon_user.png" caption="列表caption" sndcaption="列表sndcaption" ricon="res:image/icon_arrow.png"/> <listitem type="twoline" href="res:page/index.xhtml" icon="res:image/icon_user.png" caption="列表caption" sndcaption="列表sndcaption" ricon="res:image/icon_arrow.png"/> </body> <footer style="background-color:#cccccc;"> <input type="button" value="<<"/> <input type="button" value="<"/> <font style="align:center;">1/2</font> <input type="button" value=">" style="align:right;"/> <input type="button" value=">>" style="align:right;"/> </footer> </html> |
效果如下:
上面的是比较固定布局的list,如果需要进行简单的自定义布局,可以使用list控件,同时支持上下拖拽进行分页。需要注意的是list控件只支持通过注入JS数据来生成列表数据。代码如下:
<html> <head> <meta charset="UTF-8"/> <title>拖动列表</title> <script> <![CDATA[
function doLoad(){ showList(); }
function showList(){
var listview = document.getElementById('listview'); var dp = listview.getDataProvider(); var item = []; item.push('{"name":{"innerHTML":"黄楠"},"number":{"innerHTML":"18901596118"},"listitem":{"href":"alert(1)"}}'); item.push('{"name":{"innerHTML":"高明珠"},"number":{"innerHTML":"13655190214"},"listitem":{"href":"alert(2)"}}'); dp.appendItems('['+item.join(',')+']'); dp.refreshData(); } ]]> </script> </head> <body onload="doLoad()"> <list id="listview" onscrolltop="showList()" onscrollbottom="showList()"> <div data-role="listtopdrag" style="text-align:center;height:30dp"> <font style="font-size:20dp;color:#333333">向下拖动</font> </div> <div data-role=" listtoprelease" style="text-align:center"> <font style="font-size:20dp;color:#333333">释放即将刷新</font> </div> <div data-role="listtoprefresh" style="text-align:center" > <font style="font-size:20dp;color:#333333">正在刷新.....</font> </div>
<div data-role="listitem" style="background-click-color:#cccccc;"> <div style="width:10%;"> <img src="res:image/icon_user.png"/> </div> <div style="width:80%;"> <font id="name" style="color:#5f5f5f;font-size:1.5em">姓名</font> <br /> <font id="number" style="color:#5f5f5f;font-size:1.5em">电话号码</font> </div> <div style="width:10%;"> <img src="res:image/icon_arrow.png"/> </div> <hr/> </div>
<div data-role="listbottomdrag" style="text-align:center;height:30dp"> <font style="font-size:20dp;color:#333333">向上拖动</font> </div> <div data-role=" listbottomrelease" style="text-align:center"> <font style="font-size:20dp;color:#333333">释放即将刷新</font> </div> <div data-role="listbottomrefresh" style="text-align:center"> <font style="font-size:20dp;color:#333333">正在刷新.....</font> </div> </list> </body> </html> |
其效果如下,拖拽后释放可以继续追加数据:
其他常用的导航控件还有:
1) animationmenu动态菜单(必须置于fixset中);
收起状态:
展开状态:
2) contextmenu弹出菜单,必须通过内置脚本script:popmenu(id)才能打开菜单;
3) hdiv临界事件导航,即拖动到页面顶部或者底部的时候可以引导触发事件,比如:上下拖动加载上下页内容。
4.5.5 表单界面
ExMobi客户端的表单控件除了提供标准html的表单控件,也提供了比如:拍照、摄像、日期、时间、可编辑选择框、开关等控件,更适合在移动终端上使用。
在移动终端中由于屏幕大小有限,很多时候需要对表单内容进行分组,这时候可以使用div,为div设置边框大小(border-size)、边框颜色(border-color)和边框弧度(border-radius)等相关样式即可达到分组的效果,结合使用tabbar可以进行更复杂的分组。
比如下面的代码:
<html> <head> <title show="false"/> <style> .container{ border-size:1; border-color:#ababab; border-radius:4; background-color:#ffffff; margin:4 4 4 4; } </style> <script> <![CDATA[ function doSubmit(){ document.getElementById('form').submit(); } ]]> </script> </head> <header> <titlebar caption="返回" title="表单界面" rcaption="操作" riconhref="script:popmenu(oper)" iconhref="script:close"/> </header> <body> <contextmenu id="oper"> <option caption="保存" onclick="doSubmit()"/> <option caption="提交" onclick="doSubmit()"/> </contextmenu>
<form id="form" action="http://local/submit.jsp" method="post"> <eselect> <option selected="true">可编辑的select</option> <option>eselect控件</option> </eselect> <select> <option selected="true">select控件</option> <option>select控件</option> </select> <input type="text" prompt="文本提示"/> <div class="container"> <font style="width:25%;margin:0 0 0 8;">文本固定</font> <input type="text" prompt="只能输入数字" validate="num" style="width:75%;border-size:0;margin:1 0 1 0;"/> </div> <input type="file" prompt="file"/> <input type="camera" prompt="拍照"/> <div class="container"> 日期段 <br/> <object type="date" prompt="开始日期" style="width:50%;"></object><object type="time" prompt="开始时间" style="width:50%;"></object> <object type="date" prompt="结束日期" style="width:50%;"></object><object type="time" prompt="结束时间" style="width:50%;"></object> </div> <div class="container"> <font>内容:</font> <br/> <textarea rows="3" type="text"></textarea> </div> <div class="container"> 开关 <switch ontext="开" offtext="关" id="switch_autologin" checked="true" style="align:right;on-background-color:#ff8800;" /> </div> <div class="container"> 单选 <br/> <input type="radio" name="radio" caption="选项1"/> <input type="radio" name="radio" caption="选项2"/> <input type="radio" name="radio" caption="选项3"/> </div> <div class="container"> 多选 <br/> <input type="checkbox" name="checkbox" caption="选项1"/> <input type="checkbox" name="checkbox" caption="选项2"/> <input type="checkbox" name="checkbox" caption="选项3"/> </div> </form> </body> </html> |
其效果如下,点击操作可以调用JS打开contextmenu菜单,注意这里隐藏了title标题区域,取而代之的是在header中使用titlebar控件显示标题,作用是给titlebar添加操作按钮:
4.5.6 常用属性
一般常用的属性和使用场景如下:
属性名 | 说明 | 使用场景 |
id | 控件的唯一标识 | 通常用于对该控件进行JS操作的时候使用。 |
name | 表单控件的名称 | 当表单控件需要提交到服务端的时候需要给控件指定name,否则控件不会被提交。 |
href、onclick | 进行超链接操作 | 通常用于页面跳转、执行JS逻辑或者交互操作的时候使用。 |
style、class | 给控件设置样式 | 需要给控件设置样式的时候使用,style为直接写样式,class是使用外部的样式。 |
4.5.7 常用样式
每一个控件本身都有一些默认的样式,通常情况下是不需要修改的,所以了解一些常用样式可以灵活的对控件进行修饰。
样式名 | 说明 | 使用场景 |
设置文字 | ||
font-size | 文字尺寸 | 一般需要对文字进行强调或者弱化的时候使用。 |
font-weight | 文字加粗 | 一般需要对文字进行强调的时候使用。 |
font-style | 文字倾斜 | 一般需要对文字进行强调的时候使用。 |
text-decoration | 文字下划线 | 在A控件中设置是否下划线达到界面显示效果一致。 |
color | 文字颜色 | 一般需要对文字进行强调或者弱化的时候使用。 |
控件显示 | ||
width | 控件宽度 | 设置控件宽度 |
height | 控件高度 | 设置控件高度 |
display | 控件显隐 | 一般在表单中通常用于内容分组或者逻辑处理 |
背景(点击)颜色 | ||
background-color | 背景颜色 | 设置背景颜色 |
background-click-color | 点击时的背景色 | 给控件添加点击效果,通常用在div或者导航控件中 |
background-image | 背景图片 | 设置背景图片 |
background-click-image | 点击时的背景色 | 给控件添加点击效果,通常用在div或者导航控件中 |
内容布局 | ||
text-align | 子容器横向布局 | 一般在顶级容器或者div、font中使用,对内部的容器设置横向对齐方式 |
text-valign | 子容器纵向布局 | 一般在顶级容器或者div、font中使用,对内部的容器设置纵向对齐方式 |
align | 控件自身布局 | 一般用在表单控件、font、div中使用,用于指定控件本身在父容器中的横向对齐方式 |
可以通过样式设置点击效果的常用控件主要有:
控件名称 | 样式 |
div | background-click-image、background-click-color |
menubar | menu-background-click-image、menu-background-current-image、menu-background-click-color、menu-background-current-color、menu-click-color、menu-current-color |
tabbar | tab-background-click-color、tab-background-current-color、click-color、current-color、border-current-color |
grid | cell-background-click-image、cell-background-current-image、cell-background-click-color、cell-background-current-color |
listitem | background-click-color、background-click-image |
list | background-click-image、background-click-color |
input:button | background-click-color |
其中click代表点击的效果,current代表选中的效果。
可以通过给html标签设置样式实现页面切换动画效果:
样式名 | 含义 | 取值 |
openanimation | 页面打开的动画效果 | none:无;slideright:从左到右;slideleft:从右到左(默认);slidedown:从上到下; slideup:从下到上;zoom:从内向外;fade:渐显 |
closeanimation | 页面关闭的动画效果 | none:无;slideright:从左到右(默认);slideleft:从右到左;slidedown:从上到下;slideup:从下到上;zoom:从外向内;fade:渐隐 |
4.6 XHTML页面的互调
4.6.1 互传参数
有时候需要在页面间传值,来实现客户端数据的复用。比如:在a.xhtml页面中的input输入框的值需要传递到b.xhtml中使用,可以有两种方法:
n 在url中添加参数
可以在a页面打开b页面的超链接中添加参数,如:res:b.xhtm?param=111,然后在b中可以使用window.getParameter("param")得到传过来的值111。
n 使用客户端的session
可以在a页面中设置客户端的session,这个session只能供客户端使用与服务端和第三方系统均无关,只要应用没有关闭该session均会存在,比如:window.setStringSession("session","222"),那么在打开B页面的时候或者任意页面的时候都可以使用window.getStringSession("session")来获取到保存在客户端的session值222,多次设置,获取到的是最后一次设置的值。
jsA.xhtml代码如下:
<html> <head> <meta charset="UTF-8"/> <title>A页面</title> <script> <![CDATA[ function openB(){ var param = "param="+document.getElementById("param").value; window.setStringSession("session", document.getElementById("session").value); window.open("res:page/jsB.xhtml?"+param); } ]]> </script> </head> <body> 传参的输入框: <input type="text" id="param"/> 放到session的输入框: <input type="text" id="session"/> <input type="button" value="打开B页面" onclick="openB()"/> </body> </html> |
jsB.xhtml代码如下:
<html> <head> <meta charset="UTF-8"/> <title>B页面</title> <script> <![CDATA[ function getA(){ document.getElementById("param").value = window.getParameter("param"); document.getElementById("session").value = window.getStringSession("session"); } ]]> </script> </head> <body onload="getA()"> 获取参数的输入框: <input type="text" id="param"/> 获取session的输入框: <input type="text" id="session"/> <input type="button" value="关闭" onclick="script:close"/> </body> </html> |
效果如下:
4.6.2 获取窗口对象
每一个XHTML页面都是一个JS的Window对象。每个页面的控件和函数都存在于Window中。单个页面内部的控件和函数调用不需要声明Window对象,比如页面中存在某个id为divCtr的控件,在当前页面中可以使用下面方法获取到控件对象:
var divCtrObject = document.getElementById(“divCtr”); |
但是如果是两个页面之间的控件和函数的调用就必须声明Window对象。
每个XHTML页面的html控件都有一个属性id,作为当前页面Window的唯一标识,比如:
4.7 JS实现本地数据存储
本地数据的存储只要有3种方式:缓存存储、SQLite存储、本地文件存储。
本地数据存储是不随客户端的关闭而消亡的,所以只要缓存没有删除,在任何时候使用都是可以的。
4.7.1 缓存存储
缓存存储是最常用的一种本地数据存储方式,这种方式通过键值对的方式把数据存储到移动终端本地。
其具体实现是通过类document.cache来实现,其用法如下:
设置缓存:
document.cache.setCache(key, value);//以键值对方式设置缓存,即可把缓存存到本地 |
读取缓存:
var str = document.cache.getCache(key);//通过设置的key来获取已经设置过的值 |
清除缓存:
document.cache. remove(key);//清除某个key对应的值 document.cache.clear();//清除所有缓存 |
常见的登陆用户名、密码的保存、临时数据的存储等都可以采用这种方式。
4.7.2 SQLite存储
SQLite是一种轻型的关系数据库管理系统,一般用于嵌入式的系统,在移动设备中被广泛应用,所以也是比较常用的一种数据存储的方式。
SQLite跟大多数数据库一样,都需要连接、创建(数据库、表)、对表的增删改查、数据库关闭等一系列的操作。其基本步骤如下:
注:每个应用都会自动创建一个名称为应用id的数据库,所以一般情况下都使用var db = Util.getDB(appId);获取一个DB数据库对象,而不用再创建自定义的数据库。
常用SQL语句范例:
创建表createSQL: create table Keys(id INTEGER,name TEXT) 查询表querySQL: select * from Keys 插入表内容insertSQL: insert into Keys (id,name) values (21,'test1') 更新表内容updateSQL: update Keys set name=’aaa’ where id=21 删除表内容deleteSQL: delete from Keys where id=21 |
4.7.3 本地文件存储
本地文件存储涉及本地文件的操作,常用于一般文本内容的存储。
读写某个文本文件的内容方法如下:
var filePath = "res:page/home.uixml"; var content = Util.readFile(filePath);//获取指定路径文件的内容 Util.writeFile(filePath,content);//将文本内容写到本地文件中 |
与文件相关的操作还包括,文件(夹)的实例化:
var file = new File(filePath);//参数为文件夹路径则实例化一个文件对象 var folder = new File(folderPath, true);//参数为文件夹路径则实例化一个文件夹对象 |
常用的文件(夹)操作方法:
方法 | 描述 |
bool isFolder() | 判断file是否是一个文件夹。返回true,表示文件夹;false表示文件。 |
bool exists() | 判断文件或文件夹是否存在。返回true,存在;false,不存在。 |
bool deleteFile() | 删除文件/文件夹。返回true,删除文件成功;false,删除文件失败或文件不存在。 |
bool mkdir() | 创建此抽象路径名指定的目录。返回true,创建文件成功;false,创建文件失败。语法:file.mkdir() |
bool createFile() | 当且仅当不存在具有此抽象路径名指定名称的文件时,创建一个新的空文件。返回true(1):创建文件成功;false(0):创建文件失败。 |
bool renameTo(destPath) | 重新命名此抽象路径名表示的文件或文件夹。返回true:重命名文件或文件夹成功;false:重命名文件或文件夹失败 |
bool copyTo(destPath) | 复制文件或文件夹到指定路径。返回true:复制文件或文件夹成功;false:复制文件或文件夹失败 |
String getFilePath() | 获取文件路径,绝对路径。 |
String getFileName() | 获取文件名 |
4.8 JS客户端信息调用
移动应用开发跟设备信息息息相关,所以通过获取客户端的信息进行一些逻辑处理是很有必要的。
4.8.1 客户端设备信息调用
客户端设备也就是客户端所在的移动设备的信息,通常包括:操作系统信息、设备型号、设备的屏幕尺寸、设备的IMSI号、设备的ESN号、客户端clientId、网络信息等。
信息 | 方法 | 说明 |
获取ESN | String Util.getEsn() | 返回手机ESN号(iphone无法获得,返回的是内部自定义字符串) |
获取IMSI | String Util.getImsi() | 返回手机IMSI号(iphone无法获得,返回的是内部自定义字符串) |
操作系统信息 | String Util.getOs() | 获取操作系统平台。PC模拟器根据配置手机类型返回,如设置为Android则返回android,设置Iphone则返回ios。各个系统返回值如下:android、ios |
获取手机型号 | String Util.getPhoneModel() | 获得手机型号名,如(htc6950)。PC模拟器返回模拟器配置文件中的phonemodel节点属性name值 |
获取屏幕高度 | int Util.getScreenHeight() | 获取当前手机屏幕高度。返回值为屏幕绝对像素值;若重力感应切换为横屏模式,则该值会变为竖屏模式下的屏幕宽度; |
获取屏幕宽度 | int Util.getScreenWidth() | 获取当前手机屏幕宽度。返回值为屏幕绝对像素值;若重力感应切换为横屏模式,则该值会变为竖屏模式下的屏幕高度; |
获取客户端clientId | String Util.getClientId() | 获取客户端标识。返回值:返回客户端标识,字符串 |
获取设备当前网络连接类型 | int getConnectionType() | 获取设备当前网络连接类型。pc模拟器实现空方法,固定返回1 返回值: 0:设备无网络连接 1:设备连接方式为WIFI无线网络 2:设备连接方式为移动网络 |
获取网络分配的IP地址 | String getNetIp() | 获取当前网络分配ip地址。PC获取为空串 |
打开系统的APN设置界面 | void openApnSetting() | 打开系统的APN设置界面 |
调用系统的网络设置界面 | bool startConnectSetting() | 返回true,表示调用成功;false,表示调用失败。【注】该函数仅Android支持,pc模拟器及Iphone实现空方法即可 |
4.8.2 客户端应用信息调用
客户端应用指的是在MBuilder中创建的项目。应用最终会部署到ExMob的服务端中,一个服务端可以部署无限个应用,前提是服务器性能可以支撑。
所以如果需要对应用进行管理就要获取应用的信息。
获取当前XHTML页面所在应用的应用id的方法为:String Util.getAppId();该方法返回的是应用id的字符串
获取所有应用的信息方法为:Array Util.getApplicationInfos();该方法返回的是当前ExMobi服务器下的所有应用的信息数组,该数组的每一个元素都是应用信息对象ApplicationInfo。
ApplicationInfo对象包含的内容有:
属性 | 描述 |
objName | 返回该对象所属类名, 字符串全小写 只读。则返回applicationinfo |
status | 当前应用状态。getApplicationInfos方法只获取本地应用,状态固定为local local:纯本地已安装应用 setup:服务器存在已安装应用,无须更新 update:服务器存在已安装应用,且可更新 unsetup:服务器存在但未安装应用 |
type | 返回应用适用类型,字符串。只读 client:客户端应用(默认);wap:wap应用;all:客户端及wap应用均支持 |
appid | 返回应用id节点,应用唯一标识。只读。默认为空。在脚本里使用appid时,对iphone客户端,appid需要使用全小写 |
appname | 返回定义应用名称 字符串。只读。默认为空 |
description | 返回应用描述 字符串。只读。默认为空 |
localVersion | 返回本地应用版本号 字符串。只读。默认为空 |
serverVersion | 返回服务器应用版本号,字符串。只读。默认为空 若服务器不存在该应用,返回为空 |
date | 返回应用发布日期 字符串。只读。默认为空 |
homepageSrc | 返回主页指向地址 字符串。只读。默认为空 |
vendorUrl | 返回应用开发者主页 字符串。只读。默认为空 |
vendorEmail | 返回应用开发者邮件地址 字符串。默认为空。只读 |
iconMain | 返回应用主界面大图 路径。只读 |
iconLogo | 返回应用logo小图 路径。只读 |
iconSelectedLogo | 返回应用选中logo小图。只读。建议为png格式 |
size | 客户端下载时需要下载的应用包大小,服务端根据客户端屏幕尺寸来计算应用包下载大小后再动态加上。计算时遵循大于1M 就用MB ,精确到小数点后1位数,如1.2MB;小于1M 就用KB ,精确到小数点后1位数,如800.2KB。对于服务器上不存在的本地应用,size为空。 |
如果要获取当前应用的信息,可以先获取所有应用信息的数组,然后再根据数组元素ApplicationInfo对象的appid值与Util.getAppId()值进行比较,id值一样ApplicationInfo对象信息就是当前应用的信息。如下:
function getCurrentAppInfo(){ //先获取所有应用的信息 var list = Util.getApplicationInfos(); var currentAppInfo;//定义当前应用信息变量 for(var i = 0;i < list.length;i=i+1){ if(list[i].appid==Util.getAppId()) return list[i]; } return null; } |
调用上面的getCurrentAppInfo()函数即可获得当前应用的信息。
4.9 本地能力调用
本地能力是指移动设备特有能力,这里主要介绍:摄像头、GPS/Location定位、地图、发短信/发邮件/打电话、条形码/二维码扫描。
4.9.1 调用摄像头
这里的摄像头主要指拍照和摄像。在4.4.5一节我们已经了解到ExMobi中已经提供现成的控件支持:
<html> <head> <title></title> <script type="text/javascript"> <![CDATA[ //回调函数有默认参数可以获取文件的完整路径 function getFilePath(path){ document.getElementById("divid").innerHTML = "文件名称:"+path; } ]]> </script> </head> <body> <!-- mode属性可以指明调用的是拍照(still)还是摄像(videoaudio),默认为拍照 --> <input type="camera" mode=" videoaudio" savedcallback="getFilePath"/> <div id="divid"/> </body> </html> |
除此之外,通过JS对象CameraWindow也可以唤起摄像头进行拍照或者摄像:
var camerawindow = new CameraWindow();//创建一个摄像头对象,通常是全局变量 function show(){//可以在某个函数中来唤起摄像头 camerawindow.mode=”still”;//设置摄像头的模式是拍照(still)还是摄像(videoaudio) camerawindow.onCallback=callback;//设置拍照结束后的回调函数 camerawindow.pwidth = 800;//设置照片的尺寸 camerawindow.startCamera();//唤起摄像头 } function callback(){//摄像头使用完毕会调用回调函数 if(camerawindow.isSuccess()){//判断是否拍照或者摄像成功 //可以从camerawindow对象获取摄像的结果 //比如camerawindow.path为获取照片或者视频的路径 } } ]]> |
4.9.2 GPS/Location定位
定位功能在社交类、外勤、快消等行业中有广泛的应用。ExMobi中也提供了相应的API。
定位按照空间分类,可分为室内定位和室外定位;按照定位方式分类,可分为基站定位(LBS)和全球卫星定位(GPS)。
LBS定位是通过电信运营商的基站信号差异来计算出手机所在的位置,所以有一定的误差,并且需要有移动信号;GPS定位通过接收GPS卫星提供的经纬度坐标信号来进行定位,所以需要移动设备有GPS模块才能定位,并且室内不一定能搜星。
所以在开发定位功能的时候通常要考虑室内和室外因素,需要同时采用LBS定位和GPS定位来最终确定定位的经纬度。
ExMobi中实现GPS定位的JS对象是Gps;实现LBS定位的JS对象是BaiduLocation。
它们的基本用法和步骤都是相似的,如下:
步骤 | GPS | LBS |
实例化 | var position = new Gps(); | var baiduLocation = new BaiduLocation(); |
初始化属性 | position.setTimeout(2000); | baiduLocation.setTimeout(5000); |
设置回调函数 | position.onCallback = callback; | baiduLocation.onCallback = callback; |
执行定位 | position.startPosition(); | baiduLocation.startPosition(); |
回调函数处理定位结果 | function callback(){ if(position.isSuccess()){/*返回定位是否成功*/ var latitude = position.latitude;/*获得纬度*/ var longitude = position.longitude;/*获得经度*/ } | function callback(){ if(baiduLocation.isSuccess()){ var latitude = baiduLocation.latitude; var longitude= baiduLocation.longitude; } |
由于GPS定位比较准确,所以一般先进行GPS定位,然后在GPS定位的回调函数中判断是否定位成功,如果定位未成功继续进行LBS定位,这样就双重保证定位结果了。
让然,如果使用GPS定位,还需要判断GPS是否已经打开,判断的JS函数为:
var gpsState = Util.getGpsState();//true表示已打开,false表示未打开 |
4.9.3 地图
地图的使用可以在界面展现上更具体化,给人真实的体验。ExMobi中集成了百度和高德的地图SDK,将地图封装成控件,简单设置属性即可在ExMobi中进行地图的展现。
同时,地图有时还依赖于定位功能,因为地图展示需要的条件是经纬度信息,而经纬度通常需要通过定位功能获取。
由于百度和高德地图的SDK和用法类似,下面以百度地图为例说明地图的使用方法。
百度地图控件一般用法如下:
<baidumap id="baidumapid" key="076E0EB3C38AA7CAC23E5CE9BE5EC43C827FF95E" zoom="10" maptype="satellite"> <mark id="markid1" name="明基医院" description="江苏省南京市建邺区河西大街2号" longitude="118.725487589807" latitude="31.9906975918588" /> <mark id="markid2" name="元通" description="江苏省南京市建邺区江东中路341号" longitude="118.716228604352" latitude="31.99743100150875" /> </baidumap> |
效果图如下:
其主要的属性为:
元素名称 | 描述及取值说明 |
baidumap | 初始化地图 |
id | 标准属性 规定元素的唯一id |
key | 百度地图使用key。百度地图使用时需要key的,考虑到安全及合法性影响,ExMobi不预置key,若使用该控件需用应用预制key,key是一个字符串,如: 076E0EB3C38AA7CAC23E5CE9BE5EC43C827FF95E字符串,申请地址http://dev.baidu.com/wiki/static/imap/key/ |
maptype | 地图显示方式,normal:普通地图(默认); satellite:卫星地图 |
center | 设置初始预制显示在地图中心的起始坐标点。若该属性不存在或值设置失败则采用当前定位点为中心点。格式为:longitude(经度),latitude(纬度) 如center="116.232323,39.021521" |
zoom | 设置地图缩放比例。初始地图zoom值控件自动完成计算,保证当前点与至少一个标注点在同一屏幕内zoom="15"。取值范围为:3~18。3:地图缩放比例最小;18:地图缩放比例最大。取值数字 |
onload | 百度地图加载完毕后执行脚本。地图标注类js操作需要在此属性设置函数中执行 |
mark | 设定标注点 |
id | 标准属性 规定元素的唯一 id |
longitude | 标注点经度。如longitude="116.232323" |
latitude | 标注点纬度。如latitude="39.021521" |
name | 标注点名称。如name="南京夫子庙总店" 1:若单个标注点则地图标注时即弹出; 2:若多个标注点,显示最近的1个标注点提示信息,点击其他标注点时切换弹出提示; 3:若name属性为空或未设置,则标注tip提示不弹出。 4:name属性和city属性需要同时使用才能显示标注点 |
description | 标注点描述。如description="南京市白下区中山南路22号" 1:若单个标注点则地图标注时即弹出; 2:若多个标注点,显示最近的1个标注点提示信息,点击其他标注点时切换弹出提示 |
city | 标注点所在城市。根据mark标签标注时,若存在经纬度定义,则按照经纬度进行标注,若不存在经纬度信息则采用name+city方式进行标注(单个标注)。name属性和city属性需要同时使用才能显示标注点 |
注意:百度地图的使用需要到百度开发者中心获得授权key方能使用。
4.9.4 发短信/发邮件/打电话
发短信、发邮件、打电话都是移动终端常用的功能,在移动应用中通过JS可以对用户体验有很大提升。
1) 发短信有两种方式,一种是打开系统发短信界面发送,一种是直接后台无感知发送。
//打开系统发短信界面,phone为接收人手机号码,content为短信内容 Util.openSystemSms(phone,content); //后台无感知发短信,phone为接收人手机号码,content为短信内容 Util.sendSms(phone,content) |
2) 发邮件是通过MailObject对象实现调取系统的发邮件页面,所以需要系统本身已经内置了邮件系统,其用法如下:
var mail=new MailObject(); //创建一个MAILOBJECT对象 mail.to="12@sina.com;34@123.com;4@ss.com";/*收件人地址列表,如果有多个,用";"分隔*/ mail.cc="chen@nj.fiberhome.com.cn;qiu@njavascript.com";/*抄送地址列表,如果有多个,用";"分隔*/ mail.bcc="66@43.com"; /*密送地址列表,如果有多个,用";"分隔*/ mail.subject="hello987"; /*邮件的标题会变成主题设置的文字,默认会提示"新邮件"*/ mail.body="hello,world,678.yuie623485892347"; /*邮件正文内容*/ mail.show(); /*调出系统邮件界面*/} |
3) 拨打电话可以通过Util.tel(phone)函数来实现,其中phone为手机号码。
4.9.5 条形码/二维码扫描
条形和二维码扫描是当前比较流行的一种社交形式,因为条形码和二维码本身可以存储一定的信息,通过移动终端扫描即可轻松获取。ExMobi中在控件和JS对象中都对其进行了封装统一称为“动态解码”。
动态解码控件的一般写法为:
<input type="decode" id="decodeid" name="decodeName"/> |
Input控件设置type为decode即为动态解码控件。如下图所示:
点击右侧图标可以启动摄像头扫描解码,解码后的内容会显示在输入框中。
通过JS也可以调用Decode对象使用动态解码功能:
var decode = new Decode();//实例化一个Decode对象 function startdecode(){ decode.onCallback = callback;/*设置解码结束回调函数*/ decode.startDecode();/*开始解码*/ } function callback(){ var text='objName:'+decode.objName;/*objName*/ if(!decode.isSuccess()){/*返回解码是否成功*/ vr.innerHTML = text+'解码失败'; return; } text = text+"解码结果:"+ decode.result+"解码时间:"+decode.time; } |
4.10 JS调用基座能力
基座提供了获取应用列表、应用下载、IP和端口设置、注册信息提交、文件下载管理等功能,如下图所示:
而一般应用开发完毕,需要把开发好的应用打包成一个客户端(IPA、APK等格式),这时候打开客户端是看不到基座的,如果需要使用基座的能力,就需要使用一些基座对象来进行操作。
ExMobi提供5个基本的基座对象来调用基座能力:
1) ApplicationInfo对象:ExMobi中的应用信息即为一个ApplicationInfo对象,比如应用的版本号,appid等,ApplicationInfo对象通过Util工具类函数获取Util.getApplicationInfos()。
2) AppManager对象:用于实现ExMobi应用相关信息获取和操作,比如:应用下载、应用卸载等。
3) DownloadInfo对象:通过ExMobi客户端下载的文件就是一个DownloadInfo对象,可以通过Util. getDownloadInfos(tag)获取已下载、下载中和全部文件的数组,每个数组对象就是一个Down。
4.11 简单逻辑处理
客户端中可以使用JS进行简单逻辑处理,如:表单校验、动态页面、应用信息调用、终端能力调用、本地数据库操作等场景。
4.11.1 可以触发JS的事件
JS都是通过一些事件来触发才能开始执行,比如单击、长按、加载完成等事件,目前客户端支持的事件主要有:
属性名 | 事件含义 | 支持的控件 |
onload | 页面加载完毕后执行的事件,一个页面只执行一次 | body |
onstart | 当页面处于激活状态时执行的事件,页面多次被激活则会执行相应次数 | body |
onstop | 当页面转变为不可见的时候执行的时间 | body |
ondestroy | 当页面被销毁的时候执行的时间,一个页面只会执行一次 | body |
onl2rscroll
| 手势从左滑到右的时候触发的事件 | body/footer/header |
onr2lscroll | 手势从右滑到左的时候触发的事件 | body/footer/header |
onresize | 屏幕横竖屏切换的时候触发的事件 | body |
menubind | 点击终端的菜单按键的时候触发的事件 | body |
backbind | 点击终端的返回按键的时候触发的事件 | body |
href | 点击控件的时候触发的事件 | a/img/menubar(menu)/menubar(submenu)/div/grid(cell)/h/item/jiugong(cell)/listitem-oneline/listitem-twoline/tree(item)/v/fileset(item)/base/marquee/ |
onclick | 点击控件的时候触发的事件 | eselect(option)/checkbox/radio/select(option)/switch/contextmenu(option)/button/menu/menu(option)/ |
onlongclick | 长按控件的时候触发的事件 | img/button/div/item/listitem-oneline/listitem-twoline/ |
onscroll | 当页面进行上下滑动的时候触发的事件 | hdiv |
onscrollbottom | 当容器滑动到底部并向下拖拽的时候触发的事件 | list |
onscrolltop | 当容器滑动到顶部并向上拖拽的时候触发的事件 | list |
onchange | 当控件输入值改变时调用的触发事件 | autocompletetext/password/radio/text/object:date/object:time/select/textarea/ |
ontextchanged | 自动完成编辑文本框激活状态下,输入值改变时触发脚本事件 | autocompletetext/text |
liconhref | 点击左侧图片链接的触发事件 | password/text |
riconhref | 点击右侧图片的链接的触发事件 | password/text/item/listitem-oneline/listitem-twoline/titlebar |
iconhref | 点击左侧图片链接地址触发事件 | item/listitem-oneline/listitem-twoline/titlebar/ |
collapsehref | 点击+或者-时触发的链接事件 | tree(item)/ |
checkboxhref | 点击checkbox图标触发的链接事件 | tree(item) |
4.11.2 表单校验
在表单校验中,通常使用JS的DOM模型获取到控件的值后对其进行准确性校验。
示例:要验证一个登陆信息是否填写完整,我们可以在点击登陆按钮的时候触发点击事件,通过document.getElementById(objId).value来获取控件的值,并判断是否为空。
代码示例如下:
<html> <head> <title>表单验证</title> <script> function doSubmit(){ if(document.getElementById("username").value==""||document.getElementById("password").value==""){ alert("用户名或者密码不能为空!"); return; } document.getElementById("form").submit(); } </script> </head> <body> <form id="form" action="http://local/login.jsp" method="post"> <font style="width:30%">用户名</font> <input type="text" style="width:70%" id="username" name="username"/> <font style="width:30%">密码</font> <input type="password" style="width:70%" id="password" name="password"/> <br size="10"/> <input type="button" style="width:70%;align:center" value="登入" onclick="doSubmit()"/> </form> </body> </html> |
效果如下:
4.11.3 动态页面
动态页面中最常见到的是某个控件的显隐(如:document.getElementById(objId).style.display = "none")、某一个容器里的内容改变(document.getElementById(objId).innerHTML="<a href='http://www.baidu.com'>百度</a>")或者某一个控件值改变(document.getElementById(objId).value="取消")。
示例:切换两个tab的显隐展示,并动态修改tab的内容。[使用批处理]
代码示例如下:
<html> <head> <meta charset="UTF-8"/> <title>导航页面</title> <style> .tabbar{ height:40; border-size:1; padding:8 4 0 4; } .tab{ height:40; width:50%; border-size:1; border-radius:8; padding:4 4 4 4; margin:0 4 0 4; text-align:center; text-valign:middle; background-click-color:#dddddd; } .current{ background-color:#cccccc; } </style> <script> <![CDATA[ function showHide(s,h){ //显隐批处理可以提高界面显示效率 beignPreferenceChange(); //修改tab的选中状态 document.getElementById(s+"_tab").className = "tab current"; document.getElementById(h+"_tab").className = "tab"; //显隐div document.getElementById(s).style.display = "block"; document.getElementById(h).style.display = "none"; //修改底部的显示信息 document.getElementById("msg").innerHTML = "现在显示的是"+document.getElementById(s+"_tab").innerHTML; //批处理最后结束,否则所有显隐都将不生效 endPreferenceChange(); } ]]>
</script> </head> <header> <div class="tabbar"> <div href="showHide('left','right')" id="left_tab" class="tab current" target="_self">交通违法信息</div> <div href="showHide('right','left')" id="right_tab" class="tab">电子监控记录</div> </div> </header> <body> <!-- 交通违法信息查询 表单 --> <div id="left" style="display:block;"> <div> <form name="form1" method="post" id="form1" action=""> <!-- 参数jszh --> <div> <font>驾驶证号:</font> <input name="jszh" id="jszh" type="text" prompt="请输入驾驶证号" promptcolor="#c1c2c2" value="421239206413" /> </div>
<!-- 参数dabh --> <div> <font>档案编号:</font> <input name="dabh" id="dabh" type="text" prompt="请输入档案编号" promptcolor="#c1c2c2" value="123123123" /> </div>
</form> </div> </div>
<!-- 电子监控记录查询 表单 --> <div id="right" style="display:none"> <div> <form accept-charset="UTF-8" name="form2" method="post" id="form2" action=""> <div> <font>号牌种类:</font> <!-- select列表 --> <select name="driverType" > <option value="1">小客车</option> </select> </div>
<div> <font>号牌号码:</font> <input name="cphm" id="cphm" type="text" prompt="请输入号牌号码" promptcolor="#c1c2c2" value="苏A123456" /> </div> </form> </div> </div> </body> <footer style="background-color:#cccccc;"> <div id="msg" style="text-align:center;">现在显示的是交通违法信息</div> </footer> </html> |
界面效果和JS的对应关系如下:
4.12 客户端通用配置
移动应用开发经常会涉及重力感应(横竖屏切换)、响应码拦截、应用主题使用等问题。
客户端的通用配置均在应用根目录下的config.xml文件中。
4.12.1 重力感应配置
重力感应有两方面的配置,一方面是配置应用是否支持重力感应;另一方面是配置非重力感应时候的显示方向。
默认应用是不支持重力感应的,默认显示方向是竖屏(正方向)。
重力感应的配置在access节点,其中land属性设置是否支持重力感应,orientation属性设置非重力感应时候的显示方向。
land属性有2个值:true和false。值为true则指明应用支持重力感应,可以横竖屏切换;值为false则指明应用不支持重力感应,无法进行横竖屏切换。
orientation属性有4个值:port、land、padland_phoneport和padport_phoneland。如果phone和pad的显示方向一样,可以使用port(均为竖屏)和land(均为横屏);如果phone和pad的显示方式不同,可以使用padland_phoneport(pad横屏、phone竖屏)和padport_phoneland(pad竖屏、phone横屏)。
假设应用需要支持重力感应,就只需要设置land属性为true,而orientation属性则不需要设置(设置为默认值port也可以)。比如:
<access orientation="port" land="true"/>
|
假设应用不需要支持重力感应,并且pad要横屏显示,phone需要竖屏显示,就需要设置land属性为false,orientation属性设置为padland_phoneport。比如:
<access orientation="padland_phoneport" land="false"/>
|
4.12.2 响应码拦截配置
ExMobi在交互过程中会对一些异常进行分类并响应给客户端,如果不进行相应的处理,客户端就是把响应码信息提示到页面中。有时候这种提示不是很友好,并且对终端用户有指导性的意义,用户即使看到响应码也不知道如何操作。由于响应码在大多数情况下是做异常提示的,所以响应码又叫做响应码。
这时候就需要用到响应码拦截配置,该配置可以为不同的响应码配置一个本地XHTML页面进行展示,以代替客户端的生涩提示。
响应码拦截配置需要三步:
第一步:在confix.xml的根节点中增加一个响应码的配置faultconfig:
<faultconfig src="res:page/faultconfig.xml" /> |
其中src属性指向一个本地的响应码配置文件。
第二步:在相应位置创建faultconfig节点指向的配置文件“faultconfig.xml”。
第三步:编辑“faultconfig.xml”文件,增加对应响应码的拦截:
<?xml version="1.0" encoding="UTF-8"?> <faultconfig> <!-- ExMobi服务器返回响应码 --> <fault> <code>响应码</code><!—要拦截的响应码--> <description>描述信息</description> <!—指定要跳转的本地页面,如res:page/fault2008.xhtml --> <nextaction>本地页面</nextaction> </fault> </faultconfig> |
每一个响应码配置需要放在fault节点中,fault节点下包含3个子节点描述一个响应码的信息:code、description和nextaction。其中,code节点内容为具体的响应码(通常为数字);description是对响应码的描述,以方便开发人员能够快速分辨响应码的作用;nextaction是当服务端响应该响应码给客户端的时候客户端的处理页面,也就是说当对应的响应码出现的时候,就会去执行nextaction指定的地址打开页面进行相应的处理,而不是弹出默认的响应码信息。
比如,我们常见的一个错误:某个url没有配置mapp路由,就会报出响应码。如下所示:
这个响应码的含义我们可以在MBuilder的Error Code工具中查看响应码出现的原因:
这里提示的是缺少相应的应用处理页面。如果页面是需要适配的,但是没有适配,可以为请求的url配置mapp解决。但是有的时候我们不可能考虑到所有页面的适配,为了能够有更好的用户体验,我们可以通过拦截该响应码,当某些页面没配置mapp就跳转到另一个页面进行友好提示。
4.12.3 应用主题使用配置
应用主题是对应用整体风格的一个定义,使用了应用主题的应用,在默认的显示状态都采用主题的风格进行展示,可以大大的减轻开发者的工作量。
应用的主题是设置在config.xml的根节点config上的,其属性为theme,其值为主题文件的id。
主题文件应该置于client目录下的theme子目录,如下图所示:
其中cdf.xml文件即为主题文件,该主题文件中有一个id节点:
Config.xml配置的theme属性即为该id值,如下所示:
如何进行主题的开发我们会在独立章节进行讲解。
5 服务端敏捷适配开发
ExMobi服务端作为客户端与数据源(WEB页面、数据库、Web Service、标准接口等第三方系统)连接的桥梁,它主要实现与第三方系统的模拟,并且将第三方系统的响应数据进行拣选后重组为客户端识别的内容(XHMTL、XML、JSON、标准文档格式等)下行给客户端进行展示。
5.1 服务端MAPP路由规则概述
MAPP规则是服务端进行数据处理的基本规则。在第2.6.6章节中已经介绍过MAPP的常用功能。
其中最常用也是最重要的一个规则就是route路由规则,所以这里再赘述一下。
客户端与服务端的交互都是通过URL找到对应关系的,这个URL可以是一个真实的URL也可以是一个虚拟的URL。所以这个URL实际起到桥梁作用,并不会在客户端触发的时候立即请求该URL。而是,服务端根据MAPP的route路由配置去server目录找该URL对应的处理JSP文件,具体要不要往这个URL实际去发起请求由JSP决定。
所以,对于数据库这种没有URL的数据源,通常都是使用虚拟的URL,在对应处理的JSP中进行SQL执行和结果返回;而对于页面抓取或者Web Service等有URL的数据源,通常可以直接使用该URL来作为路由配置指定相应的处理JSP。
route路由规则的forward节点为每一个符合要求的URL指定处理的JSP。其中,pattern属性指明URL的匹配的规则,该规则是一个正则表达式;path是处理的JSP文件名。也就是说,匹配pattern属性指定正则规则的URL会进入path属性指定的JSP文件进行处理。
5.2 服务端的处理逻辑
服务端的处理逻辑为:服务端接收到客户端上行过来的请求中包含了第三方系统的URL、头信息、请求体等内容,服务端通过URL信息在服务端的MAPP路由配置中找到该URL对应的处理JSP,然后在JSP中向第三方系统发起请求并获取到响应,然后继续在JSP中对该响应通过拣选和重组后下行给客户端进行展示。
客户端和服务端间的关系如下图:
ExMobi处理步骤为:
第一步:客户端上行请求到服务端,其中携带要请求的第三方系统的URL、头信息、请求体等数据。
第二步:服务端获取到客户端的数据后,提取出其中的URL信息,根据MAPP路由智能匹配该URL对应的处理JSP。
第三步:在JSP中模拟第三方系统的URL并提交头信息和请求体等数据。
第四步:第三方系统接受到URL等信息后给JSP一个响应数据。
第五步:JSP将接收到的第三方系统的响应后进行拣选,并处理为客户端可以识别的格式然后下行给客户端进行展示。
注:MAPP路由为第三方系统URL与服务端对应处理的JSP的映射关系。通过MAPP路由可以为每一个第三方系统URL配置一个处理的JSP文件。
所以JSP在ExMobi中起到的是一个承上启下的作用。即:处理客户端上行过来的请求信息(通过抽取标签实现)和将请求回来的响应下行给客户端(通过拣选模式实现)。
5.3 拣选模式特点
JSP拣选模式的特点是可以通过多种抽取标签(<aa:http>、<aa:sql-excute>、<aa:datasource>)获取第三方系统的数据作为数据源,然后通过取值标签、推送标签、逻辑标签及一些扩展标签从该数据源中拣选想要的数据进行展示。所以,一个数据源必然对应一个抽取标签,数据源作为抽取标签的元数据,以供其他标签和函数从中拣选需要的数据。
所有抽取标签的根标签都有一个属性就是id,所有取值标签和逻辑标签都有一个属性dsId,其值就是要获取数据的抽取标签的id。非抽取标签向抽取标签取值就需要指定dsId,以形成一个对应关系。
比如:抽取标签<aa:http id="login"/>代表的是一个网络请求,请求结果就是一个数据源,通过该抽取标签获取到数据后,可以在取值标签<aa:value-of dsId="login" xpath="..."/>中通过设置dsId属性的值为login来指定xpath获取的值来自于id为login的抽取标签(数据源)。
5.4 敏捷适配JSP标签库
敏捷适配JSP标签库是一套封装了大量数据集成业务逻辑的标签库,通过该标签库可以方便对第三方数据进行集成,主要包括:数据的抽取、数据的获取、数据的格式化以及数据的输出。它主要包括:抽取标签、取值标签、推送标签、逻辑标签和扩展标签。
本节主要介绍抽取标签、取值标签、逻辑标签和扩展标签。推送标签将有独立模块进行讲解。
5.4.1 抽取标签
抽取标签用于获取第三方系统的数据,包括http请求的响应、数据库请求、自定义数据源等。常用的抽取标签有:
标签名 | 用途 |
<aa:http> | 通过http请求抽取第三方系统的数据,可以设置url、method、enctype、reqcharset、rspcharset、id等属性。 |
<aa:header> | 为<aa:http>的子标签,可以设置请求头信息。 |
为<aa:http>的子标签,可以设置请求体为键值对的参数。 | |
为<aa:http>的子标签,在http协议请求时设置请求cookie。 | |
为<aa:http>的子标签,可以设置请求体为非键值对的参数。 | |
<aa:datasource> | 将某一个String类型的JAVA对象转换为数据源,具有id属性可以供其他标签从中获取数据。其他标签通过设置dsId为该id值与该数据源关联。 |
<aa:sql-excute> | 执行一个SQL语句并获取响应,需要与路由配置中的数据源绑定。 |
需要注意的是,具有id属性的抽取标签,在实际请求的时候可以在临时文件目录里生成一个文件名为id的文件,所以对于不同请求,设置的id尽量不同,以方便分析临时文件的时候能迅速定位到想要的文件。
5.4.2 取值标签
取值标签主要用于从抽取抽取标签请求到的数据源拣选想要的数据。常用的取值标签有:
标签名 | 用途 |
<aa:copy-of> | 支持通过xpath或者regex从dsId(数据源)里将符合要求的数据原样输出。 |
<aa:value-of> | 支持通过xpath或者regex从dsId(数据源)里将符合要求的数据的文本值取到。 |
注意:通过<aa:value-of>取到的内容如果包含XML中的特殊字符如<、>、"、'、&等会被转义成<、>、"、'、&的等。
5.4.3 逻辑标签
逻辑标签用于进行一些逻辑判断,从而判断后面的处理逻辑。常用的逻辑标签有:
标签名 | 用途 |
<aa:if> | 支持通过xpath、regex或者JAVA表达式结果判断一个逻辑是否符合要求,并进行相应操作。 |
<aa:choose> <aa:when> <aa:otherwise> | <aa:choose>下的<aa:when>和<aa:otherwise>通过xpath、regex或者JAVA表达式结果进行逻辑处理。 |
<aa:for-each> | 通过xpath或者regex从dsId(数据源)中获取符合要求的数据,并进行循环处理。 |
5.4.4 扩展标签
扩展标签定义了一些特殊数据的处理的标签。比如:文档下载标签、文档预览把标签、xsl转换标签等。
标签 | 描述 |
<aa:file-download> | 文件下载标签,该标签实现将响应的内容以附件下载方式响应给客户端 |
<aa:file-preview> | 文件预览标签。该标签实现将响应数据进行预览处理并将预览后的结果响应给终端 |
<aa:file-signature> | 文档签批标签。该标签用于处理在签批模式预览文档后,用户对展现在客户端上的图片签批并点击提交所触发的请求,该标签对用户绘制的签批图片做签批处理 |
<aa:addwatermark> | 增加一个在指定图片上添加水印效果 |
<aa:xsl> | 数据源标签,支持xsl渲染数据并将渲染结果作为数据源或直接输出到终端设备 |
需要特别说明的是aa:xsl标签,它既是数据源标签也是取值标签,也就是说它本身的数据可以被其他标签获取,也可以直接作为数据输出,所以aa:xsl既有id也有dsId属性。
5.4.5 使用技巧
1) 所有取值标签和逻辑标签都可以通过XPATH或者正则进行操作。
2) 所有aa标签本质都是JSP标签,应该遵循JSP标签的规范。其属性值可以使用固定的文本或者通过JAVA表达式赋值,如:
<aa:http url="http://www.exmobi.cn"/> 或者 <aa:http url="<%=aa.getReqHeaderValue("url")%>"/> |
但是混搭是不允许的,比如:
<%-- 下面语句会报错,因为不允许固定文本和JAVA变量混搭实用 --%> <aa:http url="http://www.exmobi.cn/<%=aa.getReqParameterValue("myurl")%>"/> |
如果想要组合字符串只能把固定文本部分也放到JAVA表达式中,如:
<aa:http url='<%="http://www.exmobi.cn/"+aa.getReqParameterValue("myurl")%>'/> |
但是要注意由于JAVA表达式中只能用双引号,所以url的值外层只能使用单引号。如果想要url的值用双引号,只能把JAVA表达式定义到外面的一个变量中,再把该变量的值赋值给url,如下:
<% String myurl = "http://www.exmobi.cn/"+aa.getReqParameterValue("myurl"); %> <aa:http url="<%=myurl %>"/> |
一般这种写法通常还能解决JAVA表达式中必须有单引号的情况,否则JAVA表达示中的单引号都要转义。
5.5 敏捷适配JAVA工具集函数
敏捷适配JAVA工具集函数是作为JSP标签库的一个补充,让开发者能够通过JAVA函数的方式对数据源进行灵活的处理。
本节主要介绍请求取值函数、响应取值函数和通用功能函数。
所有工具集函数都封装在aa类中,故调用方法都是aa.方法名(参数),如:
String usr = aa. getReqParameterValue (“username”);//获取参数名为username的值
5.5.1 请求体取值函数
请求取值函数是主要功能是获取请求头信息或者请求体相关信息的函数,通常用在<aa:http>请求之前进行预处理,然后重置<aa:http>的请求信息。常用的请求取值函数有:
函数名 | 用途 |
getReqHeaderValue | 根据请求头的name获取上下文(pageContext)中的请求头信息。 |
getReqParameterValue | 通过键值对请求体的name获取对应的value值。 |
getReqParameterValueFromUrl | 获取url中参数名为name 的参数值。 |
5.5.2 响应体取值函数
响应取值函数主要功能是对抽取标签请求到的数据源进行拣选。常用的响应取值函数有:
标签名 | 用途 |
xpath | 通过xpath获取指定dsId对应的数据源(抽取标签)的数据。 |
regex | 通过正则获取指定dsId对应的数据源(抽取标签)的数据。 |
copyOfNodeAsStr | 将整个节点(含所有子节点)的内容转换为字符串 |
5.5.3 通用功能函数
通用功能函数提供了一些与请求无关的函数,主要是对数据的二次转换和处理等。常用的通用功能函数有:
函数名 | 用途 |
escapeXML | 用于对xml的特殊字符进行转义。 |
jsonToXmlString | 将json格式数据转换为xml格式。 |
xpathNode | 用于获取node节点下指定xpath节点。可用于判断节点是否存在。 |
regexFilter | 获取指定字符串中符合正则的部分 |
5.5.4 注意正则的使用
JSP的工具集函数中跟正则相关的函数,必须至少包含一个组即()包起来的部分,返回的内容就是组内的数据,比如:要取到<span οnclick="location.href='/getDetail.do?id=2423'">下载</span>中onclick里面的url,通常可以这么写:
aa.regexFilter("[^/]*([^']*)'", aa.xpath(./span/@onclick))
这样匹配到的结果即为/getDetail.do?id=2423。
5.6 XPATH扩展函数
XPATH扩展函数可以为数据处理提供便捷的操作。
函数名 | 用途 |
escapexml | 用于对xml的特殊字符进行转义。 |
escapejson | 用于对json数据敏感的特殊字符进行转义。 |
htmltoxml | 用于将html节点格式化为标准的xml。 |
比如:<aa:copy-of xpath="htmltoxml(//table)" dsId="content"/>,可以避免//table返回的不是标准xml格式导致客户端无法解析的错误。
5.7 JSP的基本使用
JSP主要就是将服务端获取到的客户端上行数据真实往第三方系统发起请求,并将第三方系统的响应进行拣选后下行给客户端。
下面以http://miap.cc:1001/app/template/login.jsp这个系统的登陆页面为例,进行说明。(用户名/密码:admin/111)
5.7.1 创建应用
创建一个名为hellojsp的应用,其首页地址为系统的登陆页面。如下图所示:
点击“Next”后,继续点击“Finish”后会创建应用的基本目录和文件,其中客户端配置文件config.xml的内容如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <config clientversion="4" scope="client"> <appid>hellojsp</appid> <appname>第一个jsp页面</appname> <description></description> <version>1.0.0</version> <date>2013-12-05</date> <homepage src="http://miap.cc:1001/app/template/login.jsp"/> <faultconfig src=""/> <vendor url="" email=""></vendor> <access orientation="port" land="false" network="true" gps="true" camera="true" certificate="true"/> <icon selectedlogo="res:image/selectedlogo.png" main="res:image/main.png" logo="res:image/logo.png"/> </config> |
5.7.2 配置路由
当应用创建好后,同步应用,并且打开客户端PC模拟器,就可以看到应用已经在模拟器中,点击“进入”按钮,就会报响应码为“5019”的错误。这个错误前面了解到是因为没有配置MAPP路由route的原因。
打开server目录下的mapp.xml,为该首页地址配置路由指向login.jsp文件进行处理,如下:
<?xml version="1.0" encoding="UTF-8" ?> <maxml version="2.0" xmlns="http://www.nj.fiberhome.com.cn/map" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.nj.fiberhome.com.cn/map maxml-2.0.xsd"> <config> <htmlformat wellformat="true" /> <domain address="miap.cc:1001" name="domain"/> </config>
<route baseaddr="http://domain"> <forward pattern="/app/template/login.jsp" path="mLogin.jsp"/>
</route> </maxml> |
注意:
1) 在这里使用了伪域名,即将第三方系统的ip:port(miap.cc:1001)部分用一个字串(domain)代替。一旦使用了伪域名,后面应用中请求第三方系统的地方都必须使用伪域名来代替。使用伪域名的好处是当第三方系统的ip或者域名变了,只需要修改伪域名的配置即可,不需要每个页面、每个请求都修改。
下面confi.xml中的首页地址也需要做相应的调整,如下:
<homepage src="http://domain/app/template/login.jsp"/> |
2) route节点的baseaddr属性和forward节点的pattern属性都是正则格式,所以其值如果包含正则的关键字是需要转义的,比如:问号?。
所以如果一个地址为http://miap.cc:1001/app/getdoc.do?id=4235,那forward的pattern可以写为:
<forward pattern="/app/getdoc.do\?id=4235" path="getdoc.jsp"/> |
但是一般更通用的写法为:
<forward pattern="/app/getdoc.do.*" path="getdoc.jsp"/> |
代表一系列的相似请求通过同一个JSP文件进行处理。
3) path指向的JSP文件位于server目录下的jsp子目录
5.7.3 新建JSP文件
从hellojsp应用的server/jsp目录邮件点击弹出的“菜单栏”开始选择“New”-》“JSP File”,如下图所示:
在弹出的新建JSP的对话框中把文件名填上“mLogin.jsp”:
点击“Finish”按钮则文件创建完毕。
JSP的默认内容如下:
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%> <!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>Hello World</title> <script> <![CDATA[
]]> </script> </head> <body> This is the page content. </body> </html> |
可以先尝试启动Tomcat,然后点击客户端的应用图标,可以看到默认的展示效果,如下图:
判断一个路由是否配置正确,可以通过MBuilder的控制台日志信息来进行验证,如果路由正确,会看到有这样的日志信息:
可以清晰看到请求的地址(login.jsp)和转向的地址(mLogin.jsp),如果跟路由配置的一致就是正确的,如果不一致就需要去看路由配置是否正确或者JSP文件是否正确。
下面开始真正进入JSP的使用。
5.7.4 模拟请求
JSP中的抽取标签<aa:http>提供了模拟第三方系统请求的方法。客户端向服务端上行请求的时候带有第三方系统的URL、请求头、请求体等信息,这些信息会内置到<aa:http>标签中作为默认值,一般不用设置。所以除非需要修改这些信息,否则一般情况下是不需要进行设置的。
所以首先需要在JSP中拼接html前添加抽取标签<aa:http>向第三方系统发送请求:
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%>
<aa:http id="login"></aa:http>
<!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>Hello World</title> <script> <![CDATA[
]]> </script> </head> <body> This is the page content. </body> </html> |
这代表的是发送一个默认请求,即客户端要求的是什么请求则执行什么请求。但是<aa:http>支持对请求进行重置,包括请求地址、请求方法、请求头、请求体以及请求/响应的编码等。
如果请求地址想改为http://www.exmobi.cn,则可以修改代码为:
<aa:http id="login" url="http://www.exmobi.cn"/> |
更多的http模拟请求将在第6章进行讲解。
JSP中也可以通过<aa:sql-excute>执行一个SQL语句并获取响应。同样的也会生成以其id为名称的临时文件。如:
<% String title = aa.getReqParameterValue("title"); String sql = "select * from tbl_task where title like '%"+title+"%'"; %> <aa:sql-excute id="selectAll" dbId="postgresql" sql="<%=sql %>"/> |
与<aa:http>不同的是,<aa:sql-excute>的响应服务端封装成了一个XML字符串。
更多数据库的集成请看第10章。
任何一个JSP可以跟第三方系统进行多次请求交互。
5.7.5 查看临时文件
临时文件是服务端向第三方系统请求后生成的响应文件,或者是开发者在开发过程处理某些数据生成的临时文件。它将请求和响应的数据存储为本地的固定格式文件,方便取值的时候有个参照物,也可以对生成的数据进行检验。
经过抽取标签(如:<aa:http>、<aa:sql-excute>)请求的数据都会在服务端生成临时文件,临时文件的地址一般为:ExMobi-Server根目录下的\data\bcs\tempfile\users目录,比如:D:\MBuilder\ExMobi-server\data\bcs\tempfile\users。经过模拟请求后可以看到请求到的临时文件为:
上图的几个文件顺序刚好反映了“客户端-服务端-第三方-服务端-客户端”的一个处理过程其中包含“reqdata”的文件是客户端请求服务端的时候生成的临时文件;“*.html”文件是服务端请求第三方后第三方响应给服务端的原始取到的页面文件,在JSP中写正则的时候需要以该文件为参照;“*-dom.xml”文件是服务端对原始文件进行默认转换后的文件,在写xpath的时候需要以该文件为参照。包含“rspdata”开头的文件是服务端最终响应给客户端的文件,客户端最终根据这个文件的内容进行界面展示,所以如果客户端界面显示错误或者报错,需要通过查看该文件来排查问题。
5.7.6 拣选数据
打开“http-login-dom.xml”文件,可以看到我们需要的登陆页面的信息:
<form id="frm" method="post"> <table align="center" border="0" cellpadding="0" cellspacing="0" class="right-table03" width="562"> <tr> <td width="221"><table border="0" cellpadding="0" cellspacing="0" class="login-text01" width="95%"> <tr> <td><table border="0" cellpadding="0" cellspacing="0" class="login-text01" width="100%"> <tr> <td align="center"><img height="97" src="images/ico13.gif" width="107"/></td> </tr> <tr> <td align="center" height="40">燶highlight40</td> </tr> </table></td> <td><img height="292" src="images/line01.gif" width="5"/></td> </tr> </table></td> <td><table border="0" cellpadding="0" cellspacing="0" width="100%"> <tr> <td class="login-text02" height="35" width="31%">用户名<br/></td> <td width="69%"><input class="easyui-validatebox" id="username" name="username" required="true" size="30" type="text"/></td> </tr> <tr> <td class="login-text02" height="35">密码<br/></td> <td><input class="easyui-validatebox" id="password" name="password" required="true" size="31" type="password"/></td> </tr>
<tr> <td height="35">燶highlight40</td> <td><input class="right-button01" id="submitBtn" name="submitBtn" type="button"value="确认登陆"/> <input class="right-button02" name="Submit232" onclick="doReset()" type="button" value="重 置"/></td> </tr> </table></td> </tr> </table> </form> |
其特点是我们想要获取的值(登陆信息)都在table下的td中。
但是form的提交地址是在JS中:
$(document).ready( function() { $('#submitBtn').click( function() { $('#frm').form('submit',{ url:'checkLogin.jsp', onSubmit:function(){ return $('#frm').form('validate'); }, success:function(data){ var msg = eval('(' + data + ')'); if(msg.status!='success'){ $.messager.alert('提示', msg.status, 'info'); changeRnd(); return; } window.location.href = 'index.html'; } }); }); |
所以可以确定拣选的方案是表单里的内容通过xpath取,而提交的地址通过正则取,同时根据需要进行一些过滤处理。
在做移动应用开发的过程中,需要注意的一个问题就是布局上跟WEB有很大的不同,不能直接照搬WEB上的布局来进行展示,而且ExMobi也是不允许的,因为并不是所有的HTML标签ExMobi都支持。所以,在进行取值的时候需要做一些过滤。
在XML工具中打开“http-login-dom.xml”文件,点击xpath工具如下图,在xpath输入栏中输入“//table[./tr/td/input]//td/node()[not(name()='br' or name()='a' or @type='button')]”,可以看到输出效果:
上面的xpath指明要取含有输入框的table下的所有节点元素,但是不包括标签名为br、a和属性的type为button的节点,因为这些节点暂时不需要。过滤后的结果就只剩下我们需要的登陆信息。
登陆提交的地址是在script标签中,所以首先要通过xpath获取到script标签下的所有内容,然后再通过正则获取url部分的内容:
<%=aa.regexFilter("url:'([^']*)',", aa.xpath("//script[not(@src)]", "login"))%> |
mLogin.jsp代码编写完成,其全部代码如下:
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%>
<aa:http id="login"></aa:http>
<!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>登陆——get请求</title> </head> <body> <%-- action为http://domain/app/template/checkLogin.jsp --%> <form action="http://domain/app/template/<%=aa.regexFilter("url:'([^']*)',", aa.xpath("//script[not(@src)]", "login"))%>" method="post" target="_self"> <%-- 包含有input的table就是登陆需要信息,对该表格td下的所有节点做循环读取 --%> <aa:for-each dsId="login" var="node" xpath="//table[./tr/td/input]//td/node()[not(name()='br' or name()='a' or @type='button')]"> <aa:if testxpath="name()=''" dsId="node"> <font color="#ff0000" style="width:30%;"><aa:value-of xpath="." dsId="node"/></font> </aa:if> <aa:if testxpath="name()='input'" dsId="node"> <input type="<%=aa.xpath("@type", "node")%>" id="<%=aa.xpath("@id", "node")%>" name="<%=aa.xpath("@name", "node")%>" style="width:70%;"></input> </aa:if> </aa:for-each> <input type="submit" value="登陆" style="width:100%"/> </form> </body> </html> |
需要注意的是,每个数据的处理都离不开数据源,比如<aa:for-each>的处理,是以<aa:http>为数据源的,处理的是这里面的数据,所以必须指定<aa:for-each>的dsId为<aa:http>的id。而<aa:for-each>内部数据的取值都是每次循环的结果,所以也必须要指定dsId为<aa:for-each>的var值(这里比较特殊,一般数据源的标识都是id)。
进入客户端,效果如下:
5.8 JSP的处理流程
经过上一节的了解,我们知道了一个JSP的基本使用,但是JSP具有强大的集成能力,所以了解JSP的处理流程会更有利于数据的集成开发。
JSP的处理流程有四步:请求信息预处理、请求第三方系统、第三方响应预处理和获取响应信息。
5.8.1 请求信息预处理
JSP中是通过抽取标签与第三方系统进行交互的,在交互之前的处理就称为请求信息预处理。也就是说,在与第三方交互前,把交互需要的准备的一些条件准备好。
可以预处理的数据主要来源于客户端的请求,包含客户端请求的头信息、请求参数、附件信息等。
这些信息通常可以在临时文件中带有“reqdata”的临时文件中,比如我们打开请求login的临时文件“reqdata.xml”,如下:
<clientReqMsg> <headers> <header name="host" value="127.0.0.1:8001"/> <header name="accept-language" value="zh-cn"/> <header name="user-agent" value="GAEA-Client"/> <header name="connection" value="close"/> <header name="dpi" value="hdpi"/> <header name="devicetype" value="phone"/> <header name="name" value=""/> <header name="ec" value=""/> <header name="imsi" value="100000000000001"/> <header name="esn" value="s00000000000001"/> <header name="os" value="pc"/> <header name="platformid" value="i9000"/> <header name="clientversion" value="4.8.0.0"/> <header name="osversion" value="1.0"/> <header name="clientid" value="gaeaclient-pc-000001"/> <header name="screenwidth" value="480"/> <header name="screenheight" value="800"/> <header name="msisdn" value=""/> <header name="cmd" value="URLREQ"/> <header name="appid" value="hellojsp"/> <header name="cookie" value="JSESSIONID=DABD95464BA87F6F9684C60F4F06744D.jvm1"/> <header name="content-length" value="65"/> <header name="url" value="http://domain/app/template/login.jsp"/> <header name="charset" value="GB2312"/> <header name="method" value="1"/> </headers> <parmters> <!-- <parmter name="username" value=""/> <parmter name="password" value=""/> --> </parmters> </clientReqMsg> |
其中,headers标签中存储的是客户端请求的一些头信息(客户端头信息说明请看14.2客户端头信息说明),parmters信息中存储了客户端请求的一些参数和附件信息。这些信息在JSP中都是可以获取的。
需要注意的是头信息的名称都是英文字母小写,而参数的名称是根据客户端设定的控件的name的具体值而定。
1) 获取头信息
获取某个header的头信息方法如下:
String header = aa.getReqHeaderValue(“headerName”); |
获取headers下的所有header的头信息方法如下:
HashMap<String, String> headers = aa.getReqAllHeader(); |
比如,要获取客户端请求的url是什么,可以通过如下方法:
String url = aa.getReqHeaderValue(“url”);// 得到http://domain/app/template/login.jsp |
2) 获取参数信息
参数分为键值对(形如:a=1&b=2)参数和非键值对(一般为XML、JSON等文本)参数,而键值对参数有可能是在URL中,也有可能是在请求体中。
URL中的键值对参数值获取方法如下:
String para = aa.getReqParameterValueFromUrl(“paraName”);//获取某个url参数值 Map<String, Object> pmap = aa.getReqParametersFromUrl();//获取全部url中的参数 |
请求体中的键值对参数值获取方法如下:
String para = String para = aa.getReqParameterValue(“paraName”);//获取某个请求体参数值 Map<String, Object> pmap = aa.getReqParameters();//获取全部请求体中的参数 |
获取非键值对参数值的方法如下:
String reqContent = aa.getReqContent();//获取请求体中的非键值对参数 |
比如:客户端请求一个地址为http://domain/index.jsp?type=mobile那么要获取type参数的值mobile的方法为:
String type = aa. getReqParameterValueFromUrl (“type”);//得到的值为mobile |
3) 获取附件信息
附件信息一般包含文件名、文件的字节流、文件的物理地址等信息。常用的方法有:
byte[] file = aa.getReqAttachBody("file");//获取某个文件控件参数下文件的字节流 String fileName = aa.getReqAttachName(“file”);//获得文件控件参数下文件的名称 boolean isDownload = aa.isDownload();//获得客户端请求的附件是否是做下载操作 |
5.8.2 请求第三方系统
只要具备了请求第三方系统的条件,就可以请求第三方系统,目前ExMobi的JSP中已经封装好的请求第三方系统的方法主要有两种:一种是http请求<aa:http>标签;一种是SQL脚本执行<aa:sql-excute>标签。
请求第三方系统常用的处理:
1) 设置基本信息
基本信息可以直接在相应的抽取标签中进行设置即可:
<aa:http id=“login” [ url, method, enctype, reqcharset, rspcharset, mimetype, unprocessurl, keepreqdata]/> <aa:sql-excute id="selectAll" dbId="postgresql" sql=""/> |
有些基本信息是在“请求信息预处理”中进行重置后重新赋值到请求中。
对于<aa:http>请求,如果不需要对基本信息进行设置,也就是说要原样请求客户端发送的信息,写个空标签就可以了,即:
<aa:http id=“login” /> |
2) 设置参数信息
参数信息通常都是在抽取标签下的子标签中进行,这些标签一般包括:
<aa:param [name,value,type, filepath, filename]/> <aa:content [bytes]>[content]</aa:content> <aa:weboffice /> <aa:sql-param [type,value]/> |
3) 设置头信息
设置头信息只有在<aa:http>请求的时候才会需要,对于一些特殊的请求需要特别的去设置头信息,如cookie信息、content type等。
<aa:header [name,value,type]/> <aa:set-cookie [value,scope] /> |
经过这一步骤的处理,会产生临时文件,以供开发者作为参照物从中取值。
临时文件的查看在5.7.5已经有介绍。
5.8.3 请求响应预处理
请求第三方系统结束后,可以通过临时文件直观的了解请求的结果。但是有时候响应数据内容不规范,不方便获取等。所以如果要获取响应信息,有可能需要现对请求进行一个预处理。
请求响应预处理通常使用的方法为:
1) 基本思路是先把响应当成普通文本(String)进行处理
2) 通过aa.regex或者aa.xpath获取到数据后采用aa.regexReplace、aa.regexFilter等方法或者java的replace、replaceAll等方法进行替换部分内容
3) 也可以将获取到的json转换为xml,如:aa.jsonToXmlString(string),或者直接使用java的JSONObject类来处理的JSON数据。
4) 基本上JAVA对字符串的处理技巧或者特殊格式数据的处理技巧都可以灵活运用。
数据预处理后如果还需要把数据作为XML格式数据源,可以使用<aa:datasource>标签进行格式化:
<aa:datasource id=“newLogin” value=“前面预处理的变量”/> |
格式化后,即可通过<aa:datasource>的id指定获取该数据源的数据。
特别的,<aa:datasource>内部也提供对数据的预处理,<aa: replace/>和<aa: filter/>是<aa:datasource/>标签的子标签,用于对<aa:datasource/>标签的输入内容做预替换处理,在<aa:datasource/>标签构造数据源之前,先做正则替换处理,替换完成后,<aa:datasource/>再构造数据源,如果有多个<aa:replace/>或<aa: filter/>标签,则依次顺序替换。比如:
<aa:datasource value='<%=str1%>' id="login" > <aa:replace replacement="div" pattern="scritpt " /><!—把script替换为div --> </aa:datasource> |
5.8.4 获取响应信息
获取响应信息的方法有很多种,这里主要是指对XML数据的获取除了XML以外的数据可以通过JAVA的相关方法获取,比如JSON的操作类JSONObject等。
XML数据的获取方法:
1) 循环获取
<aa:for-each var=“list” dsId=“login” [ xpath, regex]/> |
2) 取值和处理
<aa:copy-of [dsId,xpath,regex]/> <aa:value-of [dsId,xpath,regex]/> aa.xpath(string,dsId); aa.regex(string,dsId); aa.copyOfNodeAsStr(string, dsId); aa.regexFilter(string); |
除此之外,还有可能需要对附件进行处理:
<!—文件下载 --> <aa:file-download dsId=“file” [filename, iscache]/> <!—文件预览 --> <aa:file-preview dsId=“file” [filename, iscache, previewtype,zoom]/> <!—获取响应文件的字节流 --> byte[] file = aa.getRspAttachBody(“file"); byte[] file = aa.getWebOfficeRspOtherBytes(“file"); <!—将字节流专为输入流做进一步操作 --> InputStream is = new ByteArrayInputStream(file); |
第3篇ExMobi开发进阶
6 页面抓取集成
页面适配集成常说为页面抓取,就是通过模拟第三方系统的http请求抓取其web页面的内容有选择的进行展示。所以需要对HTTP协议有必要的了解。
中间件通过JSP中的<aa:http>标签实现了完全模拟http请求的功能,本节主要通过常见抓包分析不同抓包的特征及实现方式。
6.1 HTTP协议概述
HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。
HTTP协议的主要特点可概括如下:
1) 支持客户/服务器模式。
2) 简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
3) 灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
4) 无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
5) 无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
6.1.1 HTTP请求
http请求由三部分组成,分别是:请求行、消息报头、请求正文。
比如:
POST /app/template/checkLogin.jsp HTTP/1.1 Host: miap.cc:1001 User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; rv:25.0) Gecko/20100101 Firefox/25.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Referer: http://miap.cc:1001/app/template/login.jsp Cookie: JSESSIONID=A22FA8A31DF0766B066635D0CD137ED2; JSESSIONID=8E5534524D9F783B8A6C2487664651EB Connection: keep-alive Content-Type: application/x-www-form-urlencoded Content-Length: 27
username=admin&password=111 |
6.1.1.1 请求行
请求行以一个方法符号开头,以空格分开,后面跟着请求的URI和协议的版本,格式如下:Method Request-URI HTTP-Version CRLF
其中 Method表示请求方法;Request-URI是一个统一资源标识符;HTTP-Version表示请求的HTTP协议版本;CRLF表示回车和换行(除了作为结尾的CRLF外,不允许出现单独的CR或LF字符)。
请求方法(所有方法全为大写)有多种,各个方法的解释如下:
GET 请求获取Request-URI所标识的资源
POST 在Request-URI所标识的资源后附加新的数据
HEAD 请求获取由Request-URI所标识的资源的响应消息报头
PUT 请求服务器存储一个资源,并用Request-URI作为其标识
DELETE 请求服务器删除Request-URI所标识的资源
TRACE 请求服务器回送收到的请求信息,主要用于测试或诊断
CONNECT 保留将来使用
OPTIONS 请求查询服务器的性能,或者查询与资源相关的选项和需求
目前ExMobi中支持的是GET和POST方法。
6.1.1.2 消息报头
请求报头允许客户端向服务器端传递请求的附加信息以及客户端自身的信息。
常用的请求报头:
Accept
Accept请求报头域用于指定客户端接受哪些类型的信息。eg:Accept:image/gif,表明客户端希望接受GIF图象格式的资源;Accept:text/html,表明客户端希望接受html文本。
Authorization
Authorization请求报头域主要用于证明客户端有权查看某个资源。当浏览器访问一个页面时,如果收到服务器的响应代码为401(未授权),可以发送一个包含Authorization请求报头域的请求,要求服务器对其进行验证。
Host(发送请求时,该报头域是必需的)
Host请求报头域主要用于指定被请求资源的Internet主机和端口号,它通常从HTTP URL中提取出来的,eg:
我们在浏览器中输入:http://www.exmobi.cn/index.jsp
浏览器发送的请求消息中,就会包含Host请求报头域,如下:
Host:www.exmobi.cn
此处使用缺省端口号80,若指定了端口号,则变成:Host:www.exmobi.cn:指定端口号
User-Agent
我们上网登陆论坛的时候,往往会看到一些欢迎信息,其中列出了你的操作系统的名称和版本,你所使用的浏览器的名称和版本,这往往让很多人感到很神奇,实际上,服务器应用程序就是从User-Agent这个请求报头域中获取到这些信息。User-Agent请求报头域允许客户端将它的操作系统、浏览器和其它属性告诉服务器。不过,这个报头域不是必需的,如果我们自己编写一个浏览器,不使用User-Agent请求报头域,那么服务器端就无法得知我们的信息了。
6.1.1.3 请求正文
HTTP请求正文的作用是把一些自定义信息告诉服务器端,它可以包含各种格式的内容。常见的有键值对格式(形如a=1&b=2)、非键值对格式(XML、JSON等)或者字节流等等。
6.1.2 HTTP响应
在接收和解释请求消息后,服务器返回一个HTTP响应消息。
HTTP响应也是由三个部分组成,分别是:状态行、消息报头、响应正文。
比如:
HTTP/1.1 200 OK Server: Apache-Coyote/1.1 Content-Type: text/html;charset=utf-8 Transfer-Encoding: chunked Date: Sat, 07 Dec 2013 14:25:08 GMT
{"status":"success"} |
6.1.2.1 状态行
其格式如下:
HTTP-Version Status-Code Reason-Phrase CRLF
其中,HTTP-Version表示服务器HTTP协议的版本;Status-Code表示服务器发回的响应状态代码;Reason-Phrase表示状态代码的文本描述。
状态代码有三位数字组成,第一个数字定义了响应的类别,且有五种可能取值:
1xx:指示信息--表示请求已接收,继续处理
2xx:成功--表示请求已被成功接收、理解、接受
3xx:重定向--要完成请求必须进行更进一步的操作
4xx:客户端错误--请求有语法错误或请求无法实现
5xx:服务器端错误--服务器未能实现合法的请求
常见状态代码、状态描述、说明:
200 OK //客户端请求成功
400 Bad Request //客户端请求有语法错误,不能被服务器所理解
401 Unauthorized //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
403 Forbidden //服务器收到请求,但是拒绝提供服务
404 Not Found //请求资源不存在,eg:输入了错误的URL
500 Internal Server Error //服务器发生不可预期的错误
503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常
eg:HTTP/1.1 200 OK (CRLF)
6.1.2.2 响应报头
响应报头允许服务器传递不能放在状态行中的附加响应信息,以及关于服务器的信息和对Request-URI所标识的资源进行下一步访问的信息。
常用的响应报头
Location
Location响应报头域用于重定向接受者到一个新的位置。Location响应报头域常用在 更换域名的时候。
Server
Server响应报头域包含了服务器用来处理请求的软件信息。与User-Agent请求报头域是相对应的。下面是Server响应报头域的一个例子:
Server:Apache-Coyote/1.1
WWW-Authenticate
WWW-Authenticate响应报头域必须被包含在401(未授权的)响应消息中,客户端收到401响应消息时候,并发送Authorization报头域请求服务器对其进行验证时,服务端响应报头就包含该报头域。
eg:WWW-Authenticate:Basic realm="Basic Auth Test!" //可以看出服务器对请求资源采用的是基本验证机制。
6.1.2.3 响应正文
响应正文跟请求正文类似,是服务端响应给客户端处理的一些数据。通常一般是HTML、XML、JSON或者文件格式等。
6.2 常见抓包分析和处理
5.7.1一节中主要模拟了GET请求,本节继续对其他常见的请求进行抓包分析并进行处理。
对于不同的请求方式,基本处理技巧是在JSP将需要修改的抓包部分进行重置。
下面以oademo应用为例说明不同请求方式的抓包特征以及如何在JSP中进行模拟请求。
注:本章节内容均使用到伪域名,伪域名的使用请参考第5.7.2章节。
首先需要创建一个oademo的应用,其基本信息如下:
在创建好的应用中,配置伪域名和route路由如下:
<?xml version="1.0" encoding="UTF-8" ?> <maxml version="2.0" xmlns="http://www.nj.fiberhome.com.cn/map" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.nj.fiberhome.com.cn/map maxml-2.0.xsd"> <config> <htmlformat wellformat="true" /> <!-- 配置伪域名 --> <domain address="miap.cc:1001" name="oa"/> </config> <!-- 配置route路由,如果有多个域名可以配置多个route --> <route baseaddr="http://oa"> <!-- 此处配置客户端URL请求的处理JSP文件,pattern是匹配URL的正则表达式,path是对用的服务端的JSP文件名 -->
</route> </maxml> |
6.2.1 请求分析
上一章的例子我们其实是做了一个get请求的例子;而登陆页面的登陆提交操作实际上是一个post带键值对的请求;登陆测试OA系统后,可以看到左侧的菜单如下:
其中“创建任务”菜单点击进去的页面有个“保存”按钮,点击该按钮实际是进行了post带附件的请求;“任务信息查看[json格式]”菜单的请求实际是一个post带json非键值对参数的请求;“任务信息查看[xml格式]”菜单的请求实际是一个post带xml非键值对参数的请求。
下面我们就来一一了解这些请求的抓包特征以及再ExMobi中如何来模拟这些请求。
6.2.2 登陆页面——GET请求
GET请求就是最简单的请求一个URL地址。这种请求的特征是不带请求正文,如果有参数的话是拼接在URL地址中的,如:
http://www.nj.fiberhome.com.cn或者http://www.nj.fiberhome.com.cn/reg.jsp?type=login
6.2.2.1 客户端代码
在客户端中,一般通过超链接发起一个GET请求。比如oademo应用的首页地址:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <config clientversion="4" scope="client" devicetype="all"> <appid>oademo</appid> <appname>页面抓取集成</appname> <description></description> <version>1.0</version> <date>2013-12-11</date> <homepage src="http://oa/app/template/login.jsp"/> <faultconfig src=""/> <vendor url="" email=""></vendor> <access orientation="port" land="false" network="true" gps="true" camera="true" certificate="true"/> <icon selectedlogo="res:image/selectedlogo.png" main="res:image/main.png" logo="res:image/logo.png"/> </config> |
可以看到homepage的src请求的地址是http://oa/app/template/login.jsp,这就是一个GET请求的地址,oa是个伪域名,实际地址为:http://miap.cc:1001/app/template/login.jsp
在页面中也可以通过href或者onclick等可以执行超链接的属性中设置一个地址发起GET请求。
6.2.2.2 抓包特征
首先打开抓包工具,在PC浏览器上访http://miap.cc:1001/app/template/login.jsp
请求结果,这是一个登陆的页面,如下图:
可看到其抓包如下所示:
GET /app/template/login.jsp HTTP/1.1 Accept: text/html, application/xhtml+xml, */* Accept-Language: zh-CN User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0) Accept-Encoding: gzip, deflate Host: miap.cc:1001 Connection: Keep-Alive Cookie: JSESSIONID=5BE7331CD67DAC85C140ED09D7B1BCA1.worker1 |
GET请求的抓包特征为:没有请求正文。如果有参数一般在URL中。
6.2.2.3 模拟请求
一般GET请求都不需要对抓包进行重组,直接在JSP中使用<aa:http>发起默认请求即可。执行结果会在服务端生成临时文件,可以从临时文件中拣选数据。
在应用中为该地址配置好处理的JSP:
<!-- 登陆页面get --> <forward pattern="/app/template/login.jsp" path="mLogin.jsp"/> |
在mLogin.jsp代码中直接使用<aa:http>,不做任何重置即可正确模拟GET请求。
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%>
<aa:http id="login"></aa:http>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>登陆——get请求</title> </head> <body> <%-- action为http://oa/app/template/checkLogin.jsp --%> <form action="http://oa/app/template/<%=aa.regexFilter("url:'([^']*)',", aa.xpath("//script[not(@src)]", "login"))%>" method="post"> <%-- 包含有input的table就是登陆需要信息,对该表格td下的所有节点做循环读取 --%> <aa:for-each dsId="login" var="node" xpath="//table[./tr/td/input]//td/node()[not(name()='br' or name()='a' or @type='button')]"> <aa:choose> <aa:when testxpath="name()=''" dsId="node"> <font color="#ff0000" style="width:30%;"><aa:value-of xpath="." dsId="node"/></font> </aa:when> <aa:when testxpath="name()='input'" dsId="node"> <input type="<%=aa.xpath("@type", "node")%>" id="<%=aa.xpath("@id", "node")%>" name="<%=aa.xpath("@name", "node")%>" style="width:70%;"></input> </aa:when> </aa:choose>
</aa:for-each> <input type="submit" value="登陆" style="width:100%"/> </form> </body> </html> |
最终客户端展示效果为:
6.2.3 登陆提交——POST请求带键值对请求体
我们常见的POST一般都是带键值对请求体。比如登陆系统、查询列表等都能看到这类请求的身影。
这里要实现的点击登陆页面的“确认登陆”按钮,就是一个post带键值对的请求。
6.2.3.1 客户端代码
客户端代码见5.1.2.3一节,GET请求的数据经过拣选已经构成一个登陆的表单,该表单请求的地址为http://oa/app/template/checkLogin.jsp,实际地址为http://miap.cc:1001/app/template/checkLogin.jsp,请求的方法是POST。
6.2.3.2 抓包特征
在PC浏览器访问到的登陆界面中,输入用户名(admin)、密码(111),点击“确认登陆”,在抓包工具中可以看到抓包信息如下:
POST /app/template/checkLogin.jsp HTTP/1.1 Accept: text/html, application/xhtml+xml, */* Referer: http://miap.cc:1001/app/template/login.jsp Accept-Language: zh-CN User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0) Content-Type: application/x-www-form-urlencoded Accept-Encoding: gzip, deflate Host: miap.cc:1001 Content-Length: 45 Connection: Keep-Alive Cache-Control: no-cache Cookie: JSESSIONID=4EBCD5E41D5CA6A1AA0B6ECE05516C81.worker1
username=admin&password=111 |
POST请求带键值对请求体特征为:请求体格式为parameterName1=parameterValue1& parameterName2=parameterValue2……。
6.2.3.3 模拟请求
一般POST带键值对请求如果参数不复杂的情况不需要对请求进行重组,有的参数需要进行一些修改的可以在客户端通过JS进行修改后提交,或者先上行到JSP中,在JSP进行修改。
该请求属于简单请求,首先配置MAPP路由:
<!-- 登陆校验post带键值对 --> <forward pattern="/app/template/checkLogin.jsp" path="mCheckLogin.jsp"/> |
然后在mCheckLogin.jsp中模拟请求,执行结果会在服务端生成临时文件,可以从临时文件中拣选数据。代码如下:
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%>
<aa:http id="checkLogin"/>
<%/* 登陆成功的响应结果为: {"status":"success"} 登陆失败的响应结果不含success 所以可以根据响应结果是否包含succes来判断是否登陆成功 */%>
<% //登陆成功分支——登陆成功则显示菜单页面 if(aa.regex(".*", "checkLogin").indexOf("success")>-1){ %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>校验:post带键值对</title> </head> <body> <listitem type="twoline" href="http://oa/app/template/jsp/addTask.jsp" caption="创建任务 " sndcaption="post带附件的请求实例"/> <listitem type="twoline" href="document.getElementById('form').submit()" caption="任务信息查看[xml格式]" sndcaption="post请求体为XML实例,通过form提交"/> <listitem type="twoline" href="http://oa/app/template/jsp/newInfo.jsp" caption="发送信息" sndcaption="合并请求实例,一个JSP发起两次aa:http请求"/> <form id="form" action="http://oa/app/template/action/taskManagerAction.jsp?handler=list&dataType=xml&timeStamp=1340797760522" method="post"> <input type="hidden" name="page" value="1"/> <input type="hidden" name="rows" value="5"/> </form> </body> </html> <% }else{//登陆失败分支,登陆失败则进行提示 %> <html type="alert"> <body> <alert title="提示" icontype="alarm"> <msg>登陆信息错误,请重新输入!</msg> </alert> </body> </html> <% } %> |
该请求中根据响应结果做了判断,如果响应内容包含成功信息则把菜单列表显示出来,否则如果内容不包含成功信息则认为是错误的,会提示重新输入账号密码,关闭提示框会跳转到登陆页面。
成功登陆效果:
6.2.4 创建任务保存——POST请求带附件
POST请求带附件,一般都是通过form方式提交的,并且form必须要设置属性enctype的值为multipart/form-data。
6.2.4.1 客户端代码
在5.1.3.3的结果中,第一个listitem超链接的地址为http://oa/app/template/jsp/addTask.jsp,对应的实际地址为http://miap.cc:1001/app/template/jsp/addTask.jsp,由于这个也是一个GET请求前面已经分析过,这里掠过。
其在PC浏览器的效果如下图:
配置mapp路由
<!-- 任务创建get --> <forward pattern="/app/template/jsp/addTask.jsp" path="mAddTask.jsp"/> |
经过mAddTask.jsp处理后的代码如下:
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%>
<%-- 请求新建任务页面 --%> <aa:http id="addTask"/> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>新建——get请求</title> <script> <![CDATA[ function doSubmit(){ if(document.getElementById("title").value==""){ alert("标题不能为空!"); return; }else if(document.getElementById("days").value==""){ alert("工期不能为空!"); return; }else if(document.getElementById("begin_time").value==""){ alert("开始时间不能为空!"); return; }else if(document.getElementById("end_time").value==""){ alert("结束时间不能为空"); return; }else if(document.getElementById("uploadImg").value==""){ alert("附件不能为空!"); return; } document.getElementById("form").submit(); } ]]> </script> </head> <body> <%-- action为http://oa/app/template/action/taskManagerAction.jsp?handler=save --%> <form id="form" action="http://oa/app/template<%=aa.regexFilter("url:'..([^']+)'", aa.xpath("//script[not(@src)]", "addTask"))%>" method="post" target="_self" enctype="multipart/form-data"> <%-- 包含有input的table就是登陆需要信息,对该表格tr做循环读取 --%> <aa:for-each dsId="addTask" var="node" xpath="//fieldset/table/tr//td/node()"> <aa:choose> <%-- 获取文本标题 --%> <aa:when testxpath="name()=''" dsId="node"> <font color="#ff0000" style="width:30%;"><aa:value-of xpath="." dsId="node"/></font> </aa:when> <%-- 将有选择时间的input转成ExMobi的object控件 --%> <aa:when test='<%=aa.xpath("contains(./@name, \'_time\')", "node") %>'> <object type="date" name="<%=aa.xpath("./@name","node")%>" id="<%=aa.xpath("./@id", "node")%>" style="width:70%;"></object> </aa:when> <%-- 普通input照常输出 --%> <aa:when testxpath="name()='input'" dsId="node"> <input type="<%=aa.xpath("./@type", "node")%>" name="<%=aa.xpath("./@name", "node")%>" id="<%=aa.xpath("./@id", "node")%>" style="width:70%;"/> </aa:when> <%-- 普通textarea照常输出 --%> <aa:when testxpath="name()='textarea'" dsId="node"> <textarea name="<%=aa.xpath("./@name", "node")%>" id="<%=aa.xpath("./@id", "node")%>" style="width:70%;height:60;"></textarea> </aa:when> <%-- 普通select照常输出 --%> <aa:when testxpath="name()='select'" dsId="node"> <select name="<%=aa.xpath("./@name", "node")%>" id="<%=aa.xpath("./@id", "node")%>" style="width:70%"> <%-- 取select下的option --%> <aa:for-each var="option" xpath="option" dsId="node"> <option value="<%=aa.xpath("./@value", "option")%>"><%=aa.xpath(".", "option")%></option> </aa:for-each> </select> </aa:when> <aa:otherwise> <%-- 特殊的控件走此分支 --%>
</aa:otherwise> </aa:choose>
</aa:for-each> </form> </body> <footer> <div href="doSubmit()" style="padding:10 0 10 0;text-align:center;background-color:#cccccc;"> 保存 </div> </footer> </html> |
客户端模拟器效果如下图:
可以看到,这是一个带附件上传的表单,点击“保存”的时候,会把附件作为表单内容进行提交。
这里面做了一些表单校验的处理,对一些必填项做校验。
6.2.4.2 抓包特征
在PC浏览器上填写好必填信息后,点击“保存”按钮,可以得到抓包信息如下:
POST /app/template/action/taskManagerAction.jsp?handler=save HTTP/1.1 Accept: text/html, application/xhtml+xml, */* Referer: http://miap.cc:1001/app/template/jsp/addTask.jsp Accept-Language: zh-CN User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0) Content-Type: multipart/form-data; boundary=---------------------------7dc16ab30822 Accept-Encoding: gzip, deflate Host: miap.cc:1001 Content-Length: 6557 Connection: Keep-Alive Cache-Control: no-cache Cookie: JSESSIONID=7E8076B7D00AC132AEB9EDEBE2226136.worker1
-----------------------------7dc16ab30822 Content-Disposition: form-data; name="title"
任务7 -----------------------------7dc16ab30822 Content-Disposition: form-data; name="days"
7 -----------------------------7dc16ab30822 Content-Disposition: form-data; name="begin_time"
2012-05-29 -----------------------------7dc16ab30822 Content-Disposition: form-data; name="end_time"
2012-07-04 -----------------------------7dc16ab30822 Content-Disposition: form-data; name="executor"
王民 -----------------------------7dc16ab30822 Content-Disposition: form-data; name="priority_level"
急 -----------------------------7dc16ab30822 Content-Disposition: form-data; name="textarea"
任务7 -----------------------------7dc16ab30822 Content-Disposition: form-data; name="uploadImg"; filename="pclogo.jpg" Content-Type: image/jpeg
?? JFIF ` ` ? C
? C
? : w ? ? ? 黟 ??O啢?琶因3? k擾蓕?"跚因.灖峏圄赹n桺 腅t 阄‑'?N聴檣捘VW砰?蝈}zl完漾 i<囑ㄇ?\l揝韘A?b溇锧色#療4喒镒7H7蠺f?|v霘5堎Ue庬_u卣dZ蹗匳J 橃裭桐/8卜茮L菤 樼各?Xd_趲軳eGJ? 瑧汇縗眏蔂? 濽貇=顗聪}d/論暧 蚮 廊 g? % !0"2? 馯DG凃qK峏嶴{毃潸\ㄔ&VK彮jj瘓鞛鞭湝稼3‑資愦鞤挧窉€磁%闿阜嘡淚+譪氻栝[秥`貶?帔? +x锏Φ砆液D赠 L?稘梂縹幥 瓯e?mr籧帊斡?眥疄G睺tcM腏l玟憈c㏑??aR秿[y5?n瑄?{铋# 橆欙)衹岾[?~O Y? 挬;攌蘖涵ZXQR橫ZD$??杆?發+ωl@C▌キ%kb腊U腬r?蝧杍Ei???b?? 鬁??憧?? 7 !1AQ "aq?#2亼”?@BCb卵狁? ?鑱i?J?ㄗ?F@=?猐 ;Ff訃b--4~?M[p吞懑鵺,N摱??K犤??钟貨?>漿Y??辍椰僬攛鱸G挽,qk?覗B? BSt唷蜒?HN崆凵Cz:l驔 [k忳庠~S@鉚P颚⒁в8峜部g ??繇烃%喣G噚?up咛oX黷X晴`G??!踕趞挝K鲒vw?諸言w?冢c?SAG隯汁bk镂鏡c?鏡嘱qゃA:Z;杕柕蓭痰蓮痰f>w肮e队b?Q?G?+*禦1?詇v暕LfT#卞=<&:L6憝;淋Qf?S?=萑瓒p櫈魀鳫}y3濵#d2硱?u? < !1AQa?q亼”裂 "2B採3R#04@brs? ?鯥 T脺袰)S罳?i熻譔烡?栧\插[W??ZBV* G驍爛I?榆爎ye4kg钷}<釭H;&籑▉QP +L孠虻蝪? -----------------------------7dc16ab30822-- |
可以看到,POST带附件请求的抓包特征为:请求头信息中Content-Type:的值中包含multipart/form-data,并且请求体中,不同的参数通过“------------------------请求id”间隔开。
6.2.4.3 模拟请求
POST请求带附件跟带键值对是类似的,只有参数复杂的时候需要重置头信息,这里也是不需要重置的。
首先配置MAPP路由:
<!-- 保存任务post带附件 --> <forward pattern="/app/template/action/taskManagerAction.jsp\?handler=save" path="mTaskManager.jsp"/> |
在处理的JSP文件mTaskManagerAction.jsp中模拟请求,执行结果会在服务端生成临时文件,可以从临时文件中拣选数据。代码如下:
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%>
<%-- 提交保存操作 --%> <aa:http id="taskManagerAction"/>
<%-- 根据响应结果进行提示 --%> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html type="alert"> <body> <alert title="提示" icontype="alarm"> <%-- 为了简化说明,这里仅作一个提示,一般表单提交完可以做一些相应的处理 --%> <%-- 响应内容可以看下临时文件,也是一个json字符串,如果包含success则提交是正确,否则认为失败 --%> <msg><%=aa.regex(".*", "taskManagerAction").indexOf("success")>-1?"保存成功!":"保存失败!"%></msg> </alert> </body> </html> </html> |
在请完毕后,通过响应内容判断是否提交正确,效果如下:
6.2.5 任务信息查看——POST请求带非键值对请求体
POST请求带非键值对是目前WEB开发中比较流行的,其实现方式通常为AJAX。但是对于ExMobi中间件实现,可以通过在客户端发起FORM请求,然后在JSP中重置为AJAX请求。
6.2.5.1 客户端代码
回到5.1.3.3的请求结果,可以看到第二个listitem的点击效果是触发一个JS提交一个表单:
<form id="form" action="http://oa/app/template/action/taskManagerAction.jsp?handler=list&dataType=xml&timeStamp=1340797760522" method="post"> <input type="hidden" name="page" value="1"/> <input type="hidden" name="rows" value="5"/> </form> |
我们的目标是在JSP把POST提交带键值对的参数变换成非键值对的请求体。
下面我们来通过抓包特征分析为什么要通过表单来做。
6.2.5.2 抓包特征
5.1.5.1中提到的请求的格式,我们通过抓包看一下。
首先先看一下PC这一部分的效果:
其对应的抓包为:
POST /app/template/action/taskManagerAction.jsp?handler=list&dataType=xml&timeStamp=1340951115290 HTTP/1.1 Accept: application/xml, text/xml, */*; q=0.01 Content-Type: multipart/form-data X-Requested-With: XMLHttpRequest Referer: http://miap.cc:1001/app/template/jsp/listTask_xml.jsp Accept-Language: zh-cn Accept-Encoding: gzip, deflate User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0) Host: miap.cc:1001 Content-Length: 47 Connection: Keep-Alive Cache-Control: no-cache Cookie: JSESSIONID=4036B744C5E165D246C42B4334895FAD.worker1
<request><page>1</page><rows>5</rows></request> |
可以看到,提交的请求体为一个XML格式的数据。而一般非键值对的请求体常见还有JSON或者一段文本内容等。
虽然我们在WEB上看到的是点击一个链接,没有抓包前可能会首先认为是一个GET请求,单实际上是一个POST请求。所以我们就不能直接用href去触发一个URL,而是需要建立一个表单。
6.2.5.3 模拟请求
可以看到客户端代码中Form表单里的两个参数page和rows的值对应的就是XML中两个数字,那我们的目标很明确,需要把键值对的参数转换成XML字符串。
首先配置MAPP路由:
<!-- 列表展示post带XML请求体 --> <forward pattern="/app/template/action/taskManagerAction.jsp\?handler=list&dataType=xml.*" path="mTaskManagerListXML.jsp"/> |
然后在mTaskManagerListXML.jsp中重组请求信息,JSP模拟AJAX就是给<aa:content>标签填充请求体的内容,这时候<aa:param>的默认值自动失效。也就是说在改变请求信息前<aa:http>会以键值对的形式提交参数的,但是经过在<aa:content>里设置请求体后,不会以键值对的形式提交参数,而是把<aa:content>里的内容作为请求体提交,执行结果会在服务端生成临时文件,可以从临时文件中拣选数据。代码如下:
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%>
<%-- mimetype是强制转换aa:http请求的响应内容格式,由于在抓包的时候可以看到响应内容就是标准XML所以强制转换为XML格式 --%> <%-- 可以尝试去掉看下是什么效果(可能会导致XML结构乱掉) --%> <aa:http id="taskManagerListXML" mimetype="text/xml"> <%-- 该头信息的设置是因为抓包的实际content-type为multipart/form-data;而一般form表单的提交,默认content-type为application/x-www-form-urlencoded,所以需要重置。当然,如果直接在form表单设置enctype属性值为multipart/form-data也是可以的 --%> <aa:header name="Content-Type" value="multipart/form-data"/> <%-- 非键值对的请求正文一律放在aa:content中,当存在该标签的时候aa:param键值对参数自动失效。如果不写该标签的话,就本次aa:http请求而言是通过键值对的方式page=1&rows=5的方式提交的,将与抓包看到的内容不符,所以需要重置请求正文 --%> <aa:content><request><page><%=aa.getReqParameterValue("page") %></page><rows><%=aa.getReqParameterValue("rows") %></rows></request></aa:content> </aa:http> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>列表:post为XML</title> </head> <body> <aa:for-each dsId="taskManagerListXML" var="item" xpath="//item"> <listitem type="twoline" caption="<%=aa.xpath("./title", "item") %>" sndcaption="<%=aa.xpath("./executor", "item")+"|"+aa.xpath("./update_time", "item") %>"/> </aa:for-each> </body> </html> |
客户端效果如下:
6.2.6 抓包分析总结
模拟请求的本质是“要什么给什么”,也就是说通过浏览器访问的实际请求包里包含哪些内容,我们在JSP里模拟的时候就设置哪些内容。
所以客户端提交的数据未必就是最终提交的数据,而是可以灵活的在JSP中通过重置<aa:http>、<aa:header>、<aa:param>、<aa:content>中的url、method、头信息、参数、请求体等关键信息达到真实模拟实际请求的效果从而获取第三方系统的正确响应。
对于简单的数据,尽量做到在客户端提交的就是实际的数据,对于复杂的数据可以放到JSP里面进行重置。
但是不管怎样,如果模拟请求响应错误,则需要将模拟请求的抓包和PC浏览器的实际抓包进行对比,分析哪些地方不一致造成的模拟失败,然后重置相应的部分。
6.3 实现合并请求
有时候需要将多次请求的数据请求到之后一并格式化输出为一个页面,或者得到某个数据需要进行多步请求才能获取到,在JSP进行合并请求可以处理这些情况。
所谓合并请求就是在一个JSP中向多个URL发起请求(也有可能是数据库请求等),也就是说在JSP中多次使用<aa:http>对相应的URL进行请求,并根据其id使用取值标签或者工具集函数拣选出相应的数据。
下面以信息发送的为例进行讲解。
6.3.1 第三方系统场景分析
信息发送主页面可以输入文本和附件作为内容发送出去,其中附件是单独一个表单提交的,提交成功将附件名称和ID传回给主页面,在主页面发送信息的时候实际只提交文本内容和附件的id,而不是附件实体本身。
web页面如下:
通过临时文件可以看到其部分源码如下:
<html> <head> <title>My JSP 'addTask.jsp' starting page</title> <meta content="no-cache" http-equiv="pragma"/> <meta content="no-cache" http-equiv="cache-control"/> <meta content="0" http-equiv="expires"/> <meta content="keyword1,keyword2,keyword3" http-equiv="keywords"/> <meta content="This is my page" http-equiv="description"/> <link href="../css/base.css" rel="stylesheet" type="text/css"/> <link href="../css/css.css" rel="stylesheet" type="text/css"/> <link href="../css/style.css" media="all" rel="stylesheet" rev="stylesheet" type="text/css"/> <link href="../js/jeasyui/themes/default/easyui.css" rel="stylesheet" type="text/css"/> <link href="../js/jeasyui/themes/icon.css" rel="stylesheet" type="text/css"/> <script src="../js/jeasyui/jquery-1.7.2.min.js" type="text/javascript"/> <script src="../js/jeasyui/jquery.easyui.min.js" type="text/javascript"/> <script src="../js/My97DatePicker/WdatePicker.js" type="text/javascript"/> <script language="JavaScript" type="text/javascript"> function doSend(){ var title = document.getElementById("title").value; var to = document.getElementById("to").value; if(title==""){ alert("请填写标题!"); return; }else if(to==""){ alert("请填写接收人!"); return; } document.getElementById("form").submit(); }
function doUpload(){ var upload = document.getElementById("upload").value; if(upload==""){ alert("请选择附件!"); return; }
document.getElementById("uploadFile").submit(); }
</script> <style type="text/css"> <!-- .atten {font-size:12px;font-weight:normal;color:#F00;} --> </style> </head> <body class="ContentBody"> <div class="MainDiv"> <table border="0" cellpadding="0" cellspacing="0" class="CContent" width="99%"> <tr> <th class="tablestyle_title">发送信息</th> </tr> <tr> <td class="CPanel"> <table border="0" cellpadding="0" cellspacing="0" style="width:100%"> <tr> <td align="left"> <input class="button" name="Submit" οnclick="doSend()" type="button" value="发送"/> <input class="button" name="Submit2" οnclick="window.history.go(-1);" type="button" value="返回"/> </td> </tr> <tr> <td width="100%"> <fieldset style="height:100%;"> <legend>添加信息</legend> <form action="sendInfo.jsp" id="form" method="post" target="frame"> <table border="0" cellpadding="2" cellspacing="1" style="width:100%"> <tr> <td align="right" nowrap="" width="13%">任务标题:</td> <td width="41%"> <input class="easyui-validatebox text" id="title" name="title" required="true" size="40" style="width:250px" type="text"/> <span class="red"> *</span> </td> <td align="right" width="19%">接收人:</td> <td width="27%"> <input class="easyui-validatebox text" id="to" name="to" required="true" style="width:154px" type="text"/> </td> </tr>
<tr> <td align="right" height="120px" nowrap="">内容:</td> <td colspan="3"> <textarea class="easyui-validatebox" cols="80" id="textarea" name="content" required="true" rows="7"/> <input id="fileName" name="fileName" type="hidden"/> <input id="fileId" name="fileId" type="hidden"/> </td> </tr> </table>
</form> <form action="uploadFile.jsp" enctype="multipart/form-data" id="uploadFile" method="post" target="frame"> <table border="0" cellpadding="2" cellspacing="1" style="width:100%"> <tr> <td align="right" height="50px" nowrap="">附件:</td> <td colspan="3"> <input class="easyui-validatebox" id="upload" name="upload" required="true" style="width: 50%;" type="file"/> <input οnclick="doUpload()" type="button" value="上传"/> </td> </tr> </table> </form> </fieldset> </td> </tr> </table> </td> </tr>
</table> </div> <iframe name="frame" style="display:none;"/>
</body></html> |
可以看到页面中有两个form,分别是提交附件和发送消息的。而且由于都往一个隐藏的iframe提交所以页面没有刷新。
再看一下点击上传按钮的时候的请求抓包:
POST /app/template/jsp/uploadFile.jsp HTTP/1.1 Host: 192.168.4.46:8080 Connection: keep-alive Content-Length: 662 Cache-Control: max-age=0 Origin: http://192.168.4.46:8080 User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.7 Content-Type: multipart/form-data; boundary=----WebKitFormBoundarya3634AT1cII3H1PN Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Referer: http://192.168.4.46:8080/app/template/jsp/newInfo.jsp Accept-Encoding: gzip,deflate,sdch Accept-Language: zh-CN,zh;q=0.8 Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3 Cookie: JSESSIONID=182F5AA1C6E2B36638F7343AF31E4B20
------WebKitFormBoundarya3634AT1cII3H1PN Content-Disposition: form-data; name="upload"; filename="鍩硅璇剧▼.txt" Content-Type: text/plain
15
上午 |
以及响应抓包:
<script> parent.document.getElementById("fileName").value = "培训课程.txt"; parent.document.getElementById("fileId").value = "e1fe1a7f-d5c3-44e3-b4c3-35b0e8f245a9"; </script> |
可以看出上传只提交了附件,通过了multipart/form-data带附件方式提交,提交成功后会给父页面——也就是发送信息页面的fileName和fileId两个隐藏域赋值。
最后再看点击发送的时候的请求抓包:
POST /app/template/jsp/sendInfo.jsp HTTP/1.1 Host: 192.168.4.46:8080 Connection: keep-alive Content-Length: 144 Cache-Control: max-age=0 Origin: http://192.168.4.46:8080 User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.7 Content-Type: application/x-www-form-urlencoded Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Referer: http://192.168.4.46:8080/app/template/jsp/newInfo.jsp Accept-Encoding: gzip,deflate,sdch Accept-Language: zh-CN,zh;q=0.8 Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3 Cookie: JSESSIONID=182F5AA1C6E2B36638F7343AF31E4B20
title=ewqe&to=ewq&content=eqweqw&fileName=%E9%8D%A9%E7%A1%85%EE%86%84%E7%92%87%E5%89%A7%E2%96%BC.txt&fileId=e1fe1a7f-d5c3-44e3-b4c3-35b0e8f245a9 |
以及响应抓包:
<script> alert("发送成功"); </script> |
可以看出发送信息只是提交一些信息参数,并没有带附件实体,而且是简单的application/x-www-form-urlencoded表单方式提交。响应内容只是提示发送成功或者失败。
6.3.2 实现方案
从上面的场景来看,使用JSP进行合并请求其实就相当于只有一个表单,该表单内容应该包含附件上传的内容实体,也包含消息发送的基本信息。通过一个大而全的表单提交到JSP后在JSP中将信息重新进行拆分到两个不同的请求中进行模拟提交。
所以关键点有两个:一是组成一个大而全的只有一个form的信息发送页面;二是在JSP中将表单信息拆分为两个请求参数进行模拟提交。
6.3.3 代码实现
通过上面的分析,下面通过代码来实现。
6.3.3.1 发送信息页面展现
首先为发送信息的页面配置MAPP路由:
<!-- 新建信息页面 --> <forward pattern="/app/template/jsp/newInfo.jsp" path="mNewInfo.jsp"/> |
其次也就是在,mNewInfo.jsp中创建一个大而全的表单:
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%>
<aa:http id="newInfo"/>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title show="false">合并请求</title> <script> <![CDATA[ function doSubmit(){ if(document.getElementById("title").value==""){ alert("标题不能为空!"); return; }else if(document.getElementById("to").value==""){ alert("接收人不能为空!"); return; } if(document.getElementsByName("upload")[0].value==""){ document.getElementsByName("uploadUrl")[0].valu = ""; } document.getElementById("form").submit(); } ]]> </script> </head> <header> <titlebar caption="返回" title="信息发送" rcaption="发送" riconhref="doSubmit()" iconhref="script:close"/> </header> <body> <%-- form的action属性写了一个虚拟地址,这个地址是不存在的,只是为了给mapp路由做一个唯一URL进到指定的JSP中处理 --%> <form id="form" action="http://oa/app/allInOne.jsp" method="post" target="_blank" enctype="multipart/form-data"> <%-- 包含有input的table就是登陆需要信息,对该表格tr做循环读取 --%> <aa:for-each dsId="newInfo" var="node" xpath="//fieldset//td/node()[name()!='span' and not(@onclick)]"> <aa:choose> <%-- 普通文字照常输出,但是如果全部是空格则不输出 --%> <aa:when testxpath="name()='' and string-length(normalize-space(.))>0" dsId="node"> <font color="#ff0000" style="width:30%;"><aa:value-of xpath="." dsId="node"/><aa:value-of xpath="" dsId="node"/></font> </aa:when> <%-- 普通textarea照常输出 --%> <aa:when testxpath="name()='textarea'" dsId="node"> <textarea name="<%=aa.xpath("./@name", "node")%>" id="<%=aa.xpath("./@id", "node")%>" style="width:70%;"></textarea> </aa:when> <%-- 普通hidden照常输出,但是不设置宽度 --%> <aa:when testxpath="@type='hidden'" dsId="node"> <input type="<%=aa.xpath("@type", "node")%>" id="<%=aa.xpath("@id", "node")%>" name="<%=aa.xpath("@name", "node")%>"></input> </aa:when> <%-- 普通input照常输出 --%> <aa:when testxpath="name()='input'" dsId="node"> <input type="<%=aa.xpath("@type", "node")%>" id="<%=aa.xpath("@id", "node")%>" name="<%=aa.xpath("@name", "node")%>" style="width:70%;"></input> </aa:when> </aa:choose> </aa:for-each> <%-- 把两个表单的提交地址作为隐藏域提交,可以在JSP中获取分别进行请求 --%> <input type="hidden" name="formUrl" value="<%=aa.xpath("//form[@id='form']/@action", "newInfo")%>"/> <input type="hidden" name="uploadUrl" value="<%=aa.xpath("//form[@id='uploadFile']/@action", "newInfo")%>"/> </form> </body> </html> |
我们给form的action虚拟了一个提交的地址http://oa/app/allInOne.jsp,而把实际的两个请求地址放在隐藏参数中formUrl(发送信息)和uploadUrl(上传附件)。需要注意的是form必须要设置enctype为multipart/form-data,否则附件无法提交。其他信息的参数和文件控件都放置在form中。在提交的时候判断如果没有选择附件,则把uploadUrl的值置空,主要是为下一步处理做准备,该值为空的话就不调用附件上传的请求了。该页面只有一个发送按钮,没有上传按钮。其界面如下:
6.3.3.2 JSP合并请求处理
发送页面组好后点击发送按钮就会将表单页面提交到虚拟地址,所以给虚拟地址配置MAPP如下:
<!-- 发送信息页面 --> <forward pattern="/app/allInOne.jsp" path="mAllInOne.jsp"/> |
这一步的关键就是在mAllInOne.jsp中将表单数据进行筛选后分别进行提交。第一步是先把附件提交,从响应结果中获取到fileName和fileId然后在第二步提交的时候设置到请求体中,代码如下:
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <% String base = "http://oa/app/template/jsp/"; String uploadUrl = aa.getReqParameterValue("uploadUrl"); String formUrl = aa.getReqParameterValue("formUrl");
//先判断是否有附件,如果有附件则先进行附件提交,并将获取到的文件名和id保存到变量中 //如果没有上传附件,则文件名为空串,否则就会有文件名,长度大于0 if(aa.getReqAttachName("upload").length()>0){ %> <aa:http id="uploadFile" url="<%=base+uploadUrl %>"/> <% } /* 如果有附件提交,则会返回fileName和fileId,下面的赋值就相当于模拟抓包到的响应内容 <script> parent.document.getElementById("fileName").value = "培训课程.txt"; parent.document.getElementById("fileId").value = "e1fe1a7f-d5c3-44e3-b4c3-35b0e8f245a9"; </script> */ String fileName = aa.regex("fileName\"\\).value = \"([^\"]*)\"", "uploadFile"); String fileId = aa.regex("fileId\"\\).value = \"([^\"]*)\"", "uploadFile"); %> <%-- 一个JSP中多次使用aa:http如果不设置参数,则从客户端过来的请求体都会被提交,也就是说一个JSP中多个默认aa:http的请求都是一样的,url、method、参数等都完全一样 可以通过设置keepreqdata="false"相当于把aa:http请求清零,所以参数全部重新设置,组成一个全新的请求 ---%> <aa:http id="sendInfo" keepreqdata="false" url="<%=base+formUrl %>" method="post"> <% //获得原先提交的参数 Map<String, Object> map = aa.getReqParameters(); //原先在页面中提交的fileName和fileId是hidden的隐藏域,没有赋值,这里是将前面提交附件获取到的文件名和id赋值给输入框对应的参数 map.put("fileName", fileName); map.put("fileId", fileId); //去掉辅助/不必要的参数(有时候不去掉问题也不大) map.remove("upload"); map.remove("formUrl"); map.remove("uploadUrl"); //重新拼接提交的参数 for(String s : map.keySet()){ %> <aa:param name="<%=s %>" value='<%=map.get(s).toString()%>' /> <% } %> </aa:http> <html type="alert"> <body> <alert title="提示" icontype="alarm"> <msg> <aa:choose> <aa:when testxpath="//script[contains(. , '成功')]" dsId="sendInfo"> 发送成功! </aa:when> <aa:otherwise> 发送失败! </aa:otherwise> </aa:choose> </msg> </alert> </body> </html> |
我们知道通过客户端提交的数据在JSP可直接通过<aa:http>进行原样提交,但是JSP里的两次请求是不同的,所以需要在<aa:http>中设置属性keepreqdata为false,意思就是不按照客户端的请求体进行请求,而是发起一个全新的请求。这时候需要自己去设置请求的一些参数,比如mAllInOne.jsp里的第二个请求(id为sendInfo)。
经过该JSP请求后的结果为:
如果把第二个请求的keepreqdata去掉,可以看到提交是不成功的:
6.3.4 使用场景总结
合并请求的使用场景主要有两种:
第一种是本节示例场景,即完成一个操作,可能需要多个请求才能完成,它是强制性的,需要多少个请求都必须执行。
第二种是页面数据集成来源自多个请求,而这些请求本身不一定有任何关系,那么可以在一个JSP中进行多次请求,这种场景通常是为了优化业务需要。
比如:登陆后的首页通常会有很多信息,比如待办的个数、重要数据的列表展示等,如下图:
这里面的信息就来自于多个页面的信息,人为的糅合在一起,本身他们之间未必有关系。
7 AJAX的使用
AJAX是目前比较流行的WEB开发模式,在ExMobi应用开发中也是很重要的一种方式。
7.1 AJAX的请求特点
AJAX请求的特点主要有两点:无状态请求和无需配置路由。
7.1.1 无状态请求。
在5中提到的几种常见的抓包请求,对于AJAX其实就是一种,处理方式都是一样。
从请求的抓包看,抓包四个关键部分:请求地址、请求方法、请求头信息、请求体信息,任何一种请求都具备这四个元素。而AJAX就提供了这几部分的数据设置。
所以只需要把对应的抓包信息设置好,AJAX就能正确发起。
那么,在客户端中要想发起一个AJAX请求必须先new一个AJAX对象,比如:
var ajax = new Ajax(url,method,data,onSuccess,onFail,requestHeader,isShowProgress); ajax.send(); |
其中:
n url为请求的地址。
n method为请求的方法。
n data为请求体,键值对和非键值对都是支持的,这点很关键。
n onSuccess是一个函数,当AJAX取到正常响应后会调用该函数进行下一步处理。
n onFail是一个函数,当AJAX没有取到正常响应客户端会调用该函数进行处理。
n requestHeader为请求头信息。
n isShowProgress表明是否显示在客户端显示进度条。
可以看出该方法中跟抓包请求关联的有四个:url、method、data和requestHeader。下面的图示可以了解抓包信息如何对应设置到AJAX对象的参数中。
请求地址 | 请求方法 | 请求头信息 | 请求体信息 |
Ajax( | url | method | data | onSuccess | onFail | requestHeader | isShowProgress); |
7.1.2 无需配置路由
经过客户端发起的AJAX请求到达服务端后,如果不配置MAPP路由的话服务端也会发起默认的请求。
对于那些响应格式直接为JSON或者XML等可以方便通过JS操作的请求无疑可以省去配置路由和编写JSP的工作。
但是若第三方响应的不是JSON或者简单XML(主要指不带命名空间的XML)仍强烈建议经过JSP进行数据拣选后返回JSON或者简单XML。
7.2 AJAX实例
以ajaxdemo应用为例,重新把http://miap.cc:1001/app/template/login.jsp的登陆提交采用AJAX请求实现。
7.2.1 创建应用
创建一个名为ajaxdemo的应用,如下:
注意homepage这里设置了一个本地页面作为登录页面。如果一个页面的内容是相对固定的,其实是可以通过建立一个本地页面来展示,而不需要采用动态获取的方式展现,这样既减少了不必要的网络请求,也使展现效率有较大的提升。
同时,继续配置mapp路由:
<?xml version="1.0" encoding="UTF-8" ?> <maxml version="2.0" xmlns="http://www.nj.fiberhome.com.cn/map" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.nj.fiberhome.com.cn/map maxml-2.0.xsd"> <config> <htmlformat wellformat="true" /> <!-- 配置伪域名 --> <domain address="miap.cc:1001" name="oa"/> </config> <route baseaddr="http://oa">
</route> </maxml> |
7.2.2 登陆页面构造
上一节中我们已经创建好应用,接下来就需要创建login.xhtm页面来展现登陆效果。
经过分析,登陆页面主要有用户名和密码两个元素,所以我们在页面中需要构造这两个内容,并且提供一个button按钮点击后进行AJAX请求,代码如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>ajax登陆</title> <script> <![CDATA[ function doSubmit(){ //设置请求的URL var url = 'http://oa/app/template/checkLogin.jsp'; //设置请求的方法method var method = 'post'; //设置提交的参数,这里是键值对的参数 var username = document.getElementById('username').value; var password = document.getElementById('password').value; var data = 'username='+username+'&password='+password; //头信息以json格式存放,键值对的参数头信息通常为application/x-www-form-urlencoded var contentType = '{"Content-Type": "application/x-www-form-urlencoded"}'; //是否显示阻塞进度条 var isShowProgress = true; //构造AJAX函数 var ajax = new Ajax(url, method, data, doSuccess, doFail, contentType, isShowProgress); //设置传给回调函数的参数 ajax.setStringData('username', username); //发送AJAX ajax.send(); }
//ajax的回调函数(成功和失败回调均有)有一个默认的参数,通过该参数可以获得ajax请求的响应结果 function doSuccess(data){ //由于返回的内容是JSON字符串,可以直接转为JSON对象更适合JS操作 //登陆成功响应为{"status":"success"},登陆失败响应为{"status":"用户名或密码错误!"} var result = eval('('+data.responseText+')'); if(result.status=='success'){ //如果登陆成功,则取出之前传过来的username作为提示信息 alert('欢迎您:'+data.getStringData('username')+'!'); }else{ alert(result.status); } }
function doFail(data){
}
]]> </script> </head> <body>
<!-- 构造登陆元素:用户名 --> <font color="red" style="width:30%">用户名:</font> <input type="text" id="username" name="username" style="width:70%"></input>
<!-- 构造登陆元素:密码 --> <font color="red" style="width:30%">密码:</font> <input type="password" id="password" name="password" style="width:70%"></input> <!-- 点击登陆按钮触发ajax请求 --> <input type="button" value="登陆" style="width:100%;" onclick="doSubmit()"></input> </body> </html> |
效果如下:
在页面加载的完毕后,点击“登陆”按钮即可调用doSubmit函数发起AJAX请求。AJAX对象中设置信息为:
n url:http://oa/app/template/checkLogin.jsp。
n method:post。
n data:'username='+username+'&password='+password。
n onSuccess:doSuccess。
n onFail:doFail。
n requestHeader:{"Content-Type": "application/x-www-form-urlencoded"}。
n isShowProgress:true。
设置完参数后调用send方法即可发送AJAX请求。
由于该请求响应为一个JSON格式数据,方便直接进行JS处理,所以不需要在MAPP路由中配置JSP处理对数据进行特别的格式化。
当用户名密码错误的时候提示信息如下:
当用户名密码正确的时候会提示“欢迎+用户名”的字样,如下所示:
这里用到了回调函数的传参,将username作为参数传递,在回调函数中获取。
7.3 AJAX与普通表单提交的差别
第6章节讲解的表单提交方式属于同步模式,这种方式的特点是每次都要新开一个页面,所以通常返回的内容都是一个HTML页面。
而AJAX方式是异步模式,即在一个页面里发起异步请求,只是改变原页面的内容,所以返回的不是一个完整的HTML页面,一般返回的是JSON、XML或者一个XHTML代码片段甚至直接是文本。
另外,AJAX提交可以不同配置MAPP而可以直接对第三方响应的内容进行操作,而表单提交则必须配置MAPP路由使用JSP进行第三方响应内容的操作。
7.4 在表单提交中使用AJAX
ExMobi的客户端可以支持在Form提交中使用AJAX,使得表单提交可以进行异步处理。
这里需要说明的是,Form表单的AJAX提交跟AJAX对象请求有个很大的不同在于,Form表单的提交比较配置MAPP路由。
下面我们继续以登陆为例,采用Form表单进行AJAX提交。
7.4.1 重新构造登陆页面
要使用Form表单提交,很重要的一点是表单控件要置于Form控件中。而login.xhtml页面中并没有把登陆的元素包在Form控件中,所以我们首先需要构建一个Form。同时,我们增加一个按钮来提交表单,代码如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>ajax登陆</title> <script> <![CDATA[
//AJAX对象提交开始===================================== function doSubmit(){ //设置请求的URL var url = 'http://oa/app/template/checkLogin.jsp'; //设置请求的方法method var method = 'post'; //设置提交的参数,这里是键值对的参数 var username = document.getElementById('username').value; var password = document.getElementById('password').value; var data = 'username='+username+'&password='+password; //头信息以json格式存放,键值对的参数头信息通常为application/x-www-form-urlencoded var contentType = '{"Content-Type": "application/x-www-form-urlencoded"}'; //是否显示阻塞进度条 var isShowProgress = true; //构造AJAX函数 var ajax = new Ajax(url, method, data, doSuccess, doFail, contentType, isShowProgress); //设置传给回调函数的参数 ajax.setStringData('username', username); //发送AJAX ajax.send(); }
//ajax的回调函数(成功和失败回调均有)有一个默认的参数,通过该参数可以获得ajax请求的响应结果 function doSuccess(data){ //由于返回的内容是JSON字符串,可以直接转为JSON对象更适合JS操作 //登陆成功响应为{"status":"success"},登陆失败响应为{"status":"用户名或密码错误!"} var result = eval('('+data.responseText+')'); if(result.status=='success'){ //如果登陆成功,则取出之前传过来的username作为提示信息 alert('欢迎您:'+data.getStringData('username')+'!'); }else{ alert(result.status); } }
function doFail(data){
}
//表单AJAX提交提交开始===================================== function doFormSubmit(){ document.getElementById('form').submit(); } //表单提交成功回调函数 function onSuccess(data){ var result = eval('('+data.responseText+')'); if(result.status=='success'){//登陆成功分支 alert('登陆成功!'); }else{//登陆失败分支 alert(result.status); } } //表单提交失败回调函数 function onFail(data){
} ]]> </script> </head> <body> <!-- form表单的ajax请求关键在于设置success属性(必须)和fail属性(可选) --> <form success="onSuccess" fail="onFail" id="form" action="http://oa/app/template/checkLogin.jsp" method="post"> <!-- 构造登陆元素:用户名 --> <font color="red" style="width:30%">用户名:</font> <input type="text" id="username" name="username" style="width:70%"></input>
<!-- 构造登陆元素:密码 --> <font color="red" style="width:30%">密码:</font> <input type="password" id="password" name="password" style="width:70%"></input> <!-- 点击ajax登陆按钮触发ajax请求 --> <input type="button" value="ajax登陆" style="width:50%;" onclick="doSubmit()"></input>
<!-- 点击表单登陆按钮触发表单的ajax提交 --> <input type="button" value="表单登陆" style="width:50%;" onclick="doFormSubmit()"></input>
</form> </body> </html> |
其效果如下:
这时候如果点击“表单登陆”按钮,就会提示响应码5017的信息:
这是提示我们需要给form的action请求地址配置MAPP路由。
7.4.2 配置MAPP路由
所以,接下来我们需要给form的action配置MAPP路由,如下:
<!-- form的ajax提交 --> <forward pattern="/app/template/checkLogin.jsp" path="mCheckLogin.jsp"/> |
7.4.3 JSP中处理实际请求
虽然同样是登陆的处理,这里的处理有一个技巧。我们在介绍页面抓取集成的时候,登陆提交后的页面我们输出的是XHTML页面;而本例中我们响应回来的内容是给JS进行处理的,所以如何能够让JS操作方便是需要考虑的。
我们已经知道登陆提交的响应结果为JSON数据,所以我们现在只需要把响应结果原样输出即可,mCheckLogin.jsp代码如下:
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%>
<%-- 发起默认请求即可,因为基本信息已经在form中组好 --%> <aa:http id="checkLogin"/>
<%-- 直接通过正则获取全部内容(请思考为什么不用xpath?),.*代表全部内容 --%> <%=aa.regex(".*", "checkLogin")%> |
当用户名密码错误的时候就会提示:
当用户名和密码正确的时候就会提示:
Form表单的AJAX请求是不可以设置回调函数的参数的,所以如果要获得用户名、密码等信息,需要重新通过dom对象获取。
7.4.4 再次回顾合并请求
在“页面抓取集成”章节中,我们了解到了“合并请求”的概念。其实了解了AJAX,我们就可以很自然的考虑到是否可以使用表单的AJAX来解决“合并请求”的场景?答案当然是肯定的,我们可以通过两个AJAX请求分别处理附件上传的请求和信息发送的请求。如果上传了附件,可以触发上传的表单提交,并从响应中获取到fileName和fileId重置到信息发送的表单中,在信息发送的时候提交。
8 Web Service集成
Web Service是一种构建应用程序的普遍模型,可以在任何支持网络通信的操作系统中实施运行,是自包含、自描述、模块化的应用,可以发布、定位、通过web调用。各应用程序通过网络协议和规定的一些标准数据格式(HTTP、XML、SOAP)来访问Web Service。
Web Service提供一种可被调用的服务,该服务必须通过WSDL定义接口。理论上通过WSDL描述的Web Service可以有很多种不同的绑定(比如:HTTP、RMI、JMS),但是实际上经常使用SOAP HTTP绑定。
所以Web Service的集成基本原理是模拟SOAP HTTP请求。
8.1 调试工具Soap UI介绍
Soap UI提供了直接将WSDL导成项目,并且模拟SOAP HTTP请求的功能。Soap UI可以通过模拟SOAP HTTP请求而不需要编码即可方便检测Web Service接口是否正确,并且可以通过抓包工具抓到该请求的包。
以目标WSDL文件http://miap.cc:1001/app/services/webServiceTest?wsdl为例说明使用方法:
SoapUI主界面如下:
点击菜单“File”,选择“New WSDL Project”,如下图:
弹出新建的面板,粘贴WSDL地址到指定地方,如下图:
点击“OK”后就会“loading wsdl”,加载成功即可成功创建一个WSDL工程,如下图:
在请求参数区域的“username”填写“admin”,“password”填写“111”,然后点击“发送请求”按钮,即可在响应数据区域看到响应数据(切换到“XML”视图),如下图所示:
8.2 SOAP HTTP抓包特征
打开抓包工具,重新进行8.1中的操作,也就是发送Web Service请求,可以看到如下请求信息:
POST /app/services/webServiceTest HTTP/1.1 Content-Type: text/xml;charset=UTF-8 SOAPAction: "" User-Agent: Jakarta Commons-HttpClient/3.0.1 Host: miap.cc:1001 Content-Length: 664
<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ser="http://server.webservice.app.fh.com"> <soapenv:Header/> <soapenv:Body> <ser:checkUserIsExist soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <username xsi:type="soapenc:string" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">admin</username> <password xsi:type="soapenc:string" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">111</password> </ser:checkUserIsExist> </soapenv:Body> </soapenv:Envelope> |
以及响应信息:
HTTP/1.1 200 OK Server: Apache-Coyote/1.1 Content-Type: text/xml;charset=utf-8 Transfer-Encoding: chunked Date: Sat, 27 Oct 2012 16:06:32 GMT
<?xml version="1.0" encoding="UTF-8"?><soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><soapenv:Body><ns1:checkUserIsExistResponse soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:ns1="http://server.webservice.app.fh.com"><checkUserIsExistReturn xsi:type="soapenc:string" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"><?xml version="1.0" encoding="UTF-8"?> <response><status>login success,welcome admin</status></response></checkUserIsExistReturn></ns1:checkUserIsExistResponse></soapenv:Body></soapenv:Envelope> |
SOAP HTTP抓包特点是:
1) 请求方法为POST。
2) 请求头中固定有两个Content-Type: text/xml;charset=UTF-8和SOAPAction: ""必须要设置。注意SOAPAction的值一般至少包含一对引号,引号中可以有一些特定的值。
3) 请求体是一个带命名空间的XML,格式固定不变,某些属性值或者文本值需要动态设置。
4) 响应体也是带命名控件的XML,格式固定不变。
8.3 Web Service集成
下面我们通过两种开发模式介绍如何进行Web Service集成。
我们可以看到,提交的请求体内容
8.3.1 同步模式模拟请求
同步模式模拟的思路是在客户端把动态参数作为表单项可进行修改,然后在处理的JSP中将这些参数拼接成最终提交的XML请求体。
8.3.1.1 客户端代码
在客户端构造代码如下:
<!-- 因为ws --> <form id="form" action="http://domain/app/services/webServiceTest" method="post"> <input type="text" name="username" value="admin" prompt="请输入用户名"/> <br/> <input type="password" name="password" value="111" prompt="请输入密码"/> <br/> <input type="submit" value="通过form登陆" style="width:50%"/> <input type="button" value="通过ajax登陆" onclick="doAjax()" style="width:50%"/> </form> |
页面效果如下:
点击“通过form登陆”会将请求信息发送给服务端。
8.3.1.2 MAPP路由配置
MAPP路由配置一个处理的JSP文件,如下:
<!-- web service --> <forward pattern="/app/services/webServiceTest" path="webservice.jsp"/> |
8.3.1.3 JSP处理请求
在webservice.jsp中将请求重组,关键设置两个头信息(Content-Type和SOAPAction)和重组请求体为XML。经过<aa:http>后会在服务端生成临时文件,然后根据临时文件拣选数据,代码如下:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" contentType="application/uixml+xml; charset=UTF-8"%> <%@ include file="/client/adapt.jsp"%> <aa:http id="ws"> <aa:header name="Content-Type" value="text/xml;charset=UTF-8"/> <aa:header name="SOAPAction" value="\"\""/> <aa:content> <soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ser="http://server.webservice.app.fh.com"> <soapenv:Header/> <soapenv:Body> <ser:checkUserIsExist soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <username xsi:type="soapenc:string" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"><%=aa.getReqParameterValue("username") %></username> <password xsi:type="soapenc:string" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"><%=aa.getReqParameterValue("password") %></password> </ser:checkUserIsExist> </soapenv:Body> </soapenv:Envelope> </aa:content> </aa:http> <!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>高级课程实例</title> </head> <body id="content"> 登陆信息: <% String msg = aa.xpath("//checkUserIsExistReturn", "ws"); if(msg.indexOf("login success")>-1){ out.println("登陆成功。"); }else{ out.println("登陆失败。"); } %> </body> </html> |
效果如下:
8.3.2 异步模式模拟请求
客户端本身可以直接模拟第三方请求,但是由于Web Service不管是请求体还是响应体都是比较庞大的XML,所以一般不建议这么做。
通常的做法也是在AJAX中将动态参数作为AJAX的data参数提交到服务端在JSP中重新拼接请求体。
8.3.2.1 客户端代码
在客户端构造代码如下,为了区别上一个请求,我们在JS发起AJAX请求时给url加了一个特殊标识“?ajax”:
<html> <head> <meta charset="UTF-8"/> <title>web service登陆</title> <script> <![CDATA[ function doAjax(){ var data = "username="+document.getElementsByName("username")[0].value+"&password="+document.getElementsByName("password")[0].value; var ajax = new Ajax(document.getElementById("form").action+"?ajax", "post", data, onSuccess, onError, '{"Content-Type": "application/x-www-form-urlencoded"}', true); ajax.send(); }
function onSuccess(data){ var rs = data.responseText; alert(rs); }
function onError(){ alert("请求错误!"); } ]]> </script> </head> <body> <form id="form" action="http://domain/app/services/webServiceTest" method="post"> <input type="text" name="username" value="admin" prompt="请输入用户名"/> <br/> <input type="password" name="password" value="111" prompt="请输入密码"/> <br/> <input type="submit" value="通过form登陆" style="width:50%"/> <input type="button" value="通过ajax登陆" onclick="doAjax()" style="width:50%"/> </form> </body> </html> |
点击“通过ajax登陆”即可调用doAjax函数发起ajax,可以看到JS函数中将表单中的输入作为ajax的提交参数。
需要说明的是,由于我们模拟的是同一个请求,但是处理的JSP不同,所以给URL添加了?ajax作为标识在MAPP路由的时候能进行区分,实际开发中不需要这么做。
8.3.2.2 MAPP路由配置
MAPP路由配置如下,由于我们自行给第三方地址加了“?ajax”,主要是为了区分前面的非AJAX请求的URL,能够让不同的请求走不同的路由配置,这是一个处理技巧,所以MAPP的配置中也要加上,注意问号的转义:
<forward pattern="/app/services/webServiceTest\?ajax" path="webserviceAjax.jsp"/> |
8.3.2.3 JSP请求处理
在webserviceAjax.jsp中处理如下:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" contentType="application/uixml+xml; charset=UTF-8"%> <%@ include file="/client/adapt.jsp"%> <aa:http id="ws"> <aa:header name="Content-Type" value="text/xml;charset=UTF-8"/> <aa:header name="SOAPAction" value="\"\""/> <aa:content> <soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ser="http://server.webservice.app.fh.com"> <soapenv:Header/> <soapenv:Body> <ser:checkUserIsExist soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <username xsi:type="soapenc:string" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"><%=aa.getReqParameterValue("username") %></username> <password xsi:type="soapenc:string" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"><%=aa.getReqParameterValue("password") %></password> </ser:checkUserIsExist> </soapenv:Body> </soapenv:Envelope> </aa:content> </aa:http> 登陆信息: <% String msg = aa.xpath("//checkUserIsExistReturn", "ws"); if(msg.indexOf("login success")>-1){ out.println("登陆成功。"); }else{ out.println("登陆失败。"); } %> |
效果如下图所示:
9 数据库集成
数据库集成是在服务端的JSP中通过在抽取标签<aa:sql-excute>中编写SQL脚本操作数据。
目前ExMobi.0版本内置了mysql、postgresql、oracle、mssqlserver、db2数据库驱动可以直接使用。
下面均以postgresql数据库为例说明。操作其中的tbl_task表,其结构为:
9.1 数据库MAPP配置
9.1.1 数据源配置
在MAPP.xml中,根节点<maxml>下的<config>标签节点增加一个<database>的标签,该标签主要是配置数据库连接的基本信息。配置如下:
<?xml version="1.0" encoding="UTF-8" ?> < maxml version="2.0" xmlns="http://www.nj.fiberhome.com.cn/map" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.nj.fiberhome.com.cn/map ../../maxml-2.0.xsd"> <config> <!—数据库--> <database id="postgresql" dbtype="postgresql" ip="miap.cc" port="1006" dbname="app" user="sitest" password="123456" maxconn="10" minconn="2" defconn="5" /> </config> </ maxml> |
经过配置后,在JSP中就可以根据<database>的id属性获取对相应的数据库进行操作。
9.1.2 路由配置说明
由于数据库集成是直接与数据库对接,实际是没有URL的,所以为了能让客户端的请求能够走到服务端的JSP进行处理,需要虚拟一个第三方的地址URL。为了方便,我们给这个第三方地址统一一个任意的域名,比如为sql,不同的请求为其命名为一个jsp文件,所以虚拟的地址类似于http://sql/ query.jsp。这样就可以为这个地址配置JSP,比如:
<route baseaddr="http://sql"> <!-- 查询操作 --> <forward pattern="/query.jsp" path="dbQuery.jsp"/> </route> |
9.2 数据库集成
9.2.1 查询数据
9.2.1.1 客户端代码
在客户端构建一个查询页面search.xhtml请求地址为:http://sql/select.jsp,查询条件是title。
http://sql/select.jsp是一个虚拟地址。由于做数据库集成是没有URL地址的,为了能够经过我们的JSP进行处理,所以需要构造类似的虚拟地址。
代码如下:
<!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>数据库-查询</title> <script> <![CDATA[
]]> </script> </head> <body id="content"> <form id="form" action="http://sql/query.jsp" method="post" target="_self"> <input type="text" name="title" value="" prompt="请输入标题"/> <input type="submit" value="查询" style="width:100%"/> </form> </body> </html> |
客户端效果如下图:
9.2.1.2 MAPP路由配置
我们给http://sql/select.jsp这个虚拟地址配置一个处理的JSP,如下:
<!-- 查询操作 --> <forward pattern="/query.jsp" path="dbQuery.jsp"/> |
9.2.1.3 JSP处理请求
dbQuery.jsp中需要获取到URL中传递过来的参数title,然后拼接要执行的SQL语句,然后执行后会在服务端生成临时文件,然后可以从临时文件中拣选想要的数据,如下:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" contentType="application/uixml+xml; charset=UTF-8"%> <%@ include file="/client/adapt.jsp"%>
<% String title = aa.getReqParameterValue("title"); %>
<aa:sql-excute id="selectAll" dbId="postgresql" sql="select * from tbl_task where title like ?"> <aa:sql-param type="String" value='<%="%"+title+"%"%>'/> </aa:sql-excute> <!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>数据库-select</title> </head> <body id="content"> <aa:for-each var="list" dsId="selectAll" xpath="//datarow"> <listitem type="twoline" href="http://sql/show.jsp?id=<%=aa.xpath("./datacol[@name='id']", "list")%>" caption="<%=aa.xpath("./datacol[@name='title']", "list")+"|"+aa.xpath("./datacol[@name='executor']", "list") %>" sndcaption="<%=aa.xpath("./datacol[@name='end_time']", "list") %>"/> </aa:for-each>
</body> <footer> <input type="button" onclick="res:page/sql/add.xhtml" value="新增"style="width:50%"/> <input type="button" onclick="http://sql/query.jsp?title=<%=title%>" target="_self" value="刷新" style="width:50%"/> </footer> </html> |
需要注意的是,JSP发起的select查询语句返回的结果是一个XML,这个XML的一个datarow节点记录一条符合条件的数据,datarow下面的datacol节点记录的是该条数据包含的列信息和它的值。所有datarow组成了整个查询结果。
所以如果要展示查询的结果,可以对datarow做循环,然后取里面的datacol节点的值。
生成的XML文件格式如下:
客户端查询效果如下:
9.2.2 插入数据
9.2.2.1 客户端代码
在9.2.1.3中已经准备了一个“新增”按钮,它是一个本地页面sql目录下的add.xhtml,其代码如下:
<!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>数据库-insert</title> <script> <![CDATA[ function doSubmit(){ if(document.getElementById("title").value==""){ alert("标题不能为空!"); return; }else if(document.getElementById("begin_time").value==""){ alert("开始时间不能为空!"); return; }else if(document.getElementById("end_time").value==""){ alert("结束时间不能为空"); return; } document.getElementById("form").submit(); } ]]> </script> </head> <body id="content"> <form id="form" action="http://sql/insert.jsp" method="post" target="_self"> 标题: <br/> <input type="text" id="title" name="title" value=""/> <br/> 开始时间:<br/> <object type="date" id="begin_time" name="begin_time" value="" style="width:100%"/> <br/> 结束时间:<br/> <object type="date" id="end_time" name="end_time" value="" style="width:100%"/> <br/> 任务执行人: <br/> <select name="executor" id="executor"> <option selected="selected">==请选择==</option> <option value="李勇">李勇</option> <option value="陶万鹏">陶万鹏</option> <option value="陈文胜">陈文胜</option> <option value="梅璇">梅璇</option> <option value="黄清华">黄清华</option> <option value="郑桂端">郑桂端</option> <option value="李伟强">李伟强</option> <option value="潘家华">潘家华</option> <option value="卢雅辉">卢雅辉</option> <option value="黄伟丰">黄伟丰</option> <option value="史长春">史长春</option> <option value="陈丽娟">陈丽娟</option> </select> <br/> 优先级: <br/> <select name="priority_level" id="priority_level"> <option selected="selected">==请选择==</option> <option value="暂不">暂不</option> <option value="一般">一般</option> <option value="需要">需要</option> <option value="急">急</option> <option value="很急">很急</option> </select> <br/> 任务说明: <br/> <input type="text" name="remark" value=""/> <br/> </form> </body> <footer> <input type="button" onclick="doSubmit()" value="新增" style="width:100%"/> </footer> </html> |
其客户端效果如下:
9.2.2.2 MAPP路由配置
点击“新增”按钮,会将表单提交到http://sql/insert.jsp这个虚拟地址,所以需要为该地址配置处理的JSP。如下:
<!-- 插入操作 --> <forward pattern="/insert.jsp.*" path="dbInsert.jsp"/> |
9.2.2.3 JSP处理请求
dbInsert.jsp中需要将提交过来的表单内容插入到数据库表中,代码如下:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" contentType="application/uixml+xml; charset=UTF-8"%> <%@ include file="/client/adapt.jsp"%> <% String id = aa.getReqParameterValue("id"); //获取所有参数MAP映射 Map<String, Object> pmap = aa.getReqParameters(); //插入的列 String set = "id"; //插入的列对应的值,唯一标示id通过UUID自动生成 String values = "'"+UUID.randomUUID().toString()+"'"; if (null != pmap){ //遍历所有参数,并拼接成SQL片段 for (String s : pmap.keySet()){ values += ",'"+pmap.get(s).toString()+"' "; set += ","+s; } } String sql = "insert into tbl_task ("+set+") values ("+values+")"; %> <aa:sql-excute id="update" dbId="postgresql" sql="<%=sql %>"/> <html type="alert"> <body> <alert title="提示" icontype="alarm"> <msg><%=aa.xpath("//result", "update").equals("1")?"新增成功!":"新增失败!"%></msg> <nextaction>script:close</nextaction> </alert> </body> </html> |
需要注意的是,JSP经过insert、update、delete的命令执行后的结果为一个XML,该XML只有一个节点,即:<result>,该节点下的值如果为1则说明执行成功,为-1则说明执行失败。执行结果会在服务端生成临时文件,可以从临时文件中拣选数据。
该JSP执行后的结果,如下图所示:
点击“刷新”按钮后可以在列表中看到新数据如下:
9.2.3 更新数据
9.2.3.1 客户端代码
在9.2.1.3中已经为没一个listitem的href添加了修改的虚拟地址:http://sql/dbShow.jsp?id=参数,通过将id传递到JSP中指定可以修改哪条数据,这个过程其实也是一个select的语句,这里不做赘述,其处理的JSP为dbShow.jsp:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" contentType="application/uixml+xml; charset=UTF-8"%> <%@ include file="/client/adapt.jsp"%> <% String id = aa.getReqParameterValueFromUrl("id"); String sql = "select * from tbl_task where id='"+id+"'"; %> <aa:sql-excute id="selectOne" dbId="postgresql" sql="<%=sql %>"/> <!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>数据库-select</title> </head> <body id="content"> <form id="form" action="http://sql/update.jsp" method="post" target="_self"> <input type="hidden" name="id" value="<%=id %>"/> 标题: <br/> <input type="text" name="title" value="<%=aa.xpath("//datacol[@name='title']", "selectOne") %>"/> <br/> 开始时间:<br/> <object type="date" name="begin_time" value="<%=aa.xpath("//datacol[@name='begin_time']", "selectOne") %>" style="width:100%"/> <br/> 结束时间:<br/> <object type="date" name="end_time" value="<%=aa.xpath("//datacol[@name='end_time']", "selectOne") %>" style="width:100%"/> <br/> 任务执行人: <br/> <input type="text" name="executor" value="<%=aa.xpath("//datacol[@name='executor']", "selectOne") %>"/> <br/> 优先级: <br/> <input type="text" name="priority_level" value="<%=aa.xpath("//datacol[@name='priority_level']", "selectOne") %>"/> <br/> 任务说明: <br/> <input type="text" name="remark" value="<%=aa.xpath("//datacol[@name='remark']", "selectOne") %>"/> <br/> </form> </body> <footer> <input type="button" onclick="document.getElementById('form').submit()" value="修改" style="width:50%"/> <input type="button" onclick="http://sql/delete.jsp?id=<%=id%>" target="_self" value="删除" style="width:50%"/> </footer> </html> |
这个JSP中可以修改表单的内容,并提交到http://sql/update.jsp这个虚拟地址进行实际的修改。这就是本节中要讲解的实例。
需要注意的是,修改数据都是有条件的,这里将id作为修改的数据的依据,所以作为hidden参数不能修改。
点击第二条数据,其客户端效果如下:
9.2.3.2 MAPP路由配置
点击“修改”按钮,会将表单提交到http://sql/update.jsp这个虚拟地址,所以需要为该地址配置处理的JSP。如下:
<!-- 更新操作 --> <forward pattern="/update.jsp" path="dbUpdate.jsp"/> |
9.2.3.3 JSP处理请求
dbUpdate.jsp中需要将提交过来的表单内容更新到数据库表中,代码如下:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" contentType="application/uixml+xml; charset=UTF-8"%> <%@ include file="/client/adapt.jsp"%> <% String id = aa.getReqParameterValue("id"); //获取所有参数MAP映射 Map<String, Object> pmap = aa.getReqParameters(); //修改的键值对 String set = ""; if (null != pmap){ for (String s : pmap.keySet()){ //将不为id的参数拼接起来 if(!id.equals(pmap.get(s).toString())) set += ","+s+"='"+pmap.get(s).toString()+"' "; } } String sql = "update tbl_task set "+set.replaceFirst(",", "")+" where id='"+id+"'"; %> <aa:sql-excute id="update" dbId="postgresql" sql="<%=sql %>"/> <html type="alert"> <body> <alert title="提示" icontype="alarm"> <msg><%=aa.xpath("//result", "update").equals("1")?"修改成功!":"修改失败!"%></msg> <nextaction>script:close</nextaction> </alert> </body> </html> |
该JSP通过传递过来的id值为判断条件对符合id值的记录进行修改。执行结果会在服务端生成临时文件,可以从临时文件中拣选数据。
此处比如修改了结束时间为2012-06-30,如下图所示:
点击“修改”按钮后并在列表中“刷新”数据如下:
9.2.4 删除数据
9.2.4.1 客户端代码
在9.2.3.1中除了“修改”按钮,还有准备了一个“删除”按钮,通过虚拟地址http://sql/delete.jsp?id=参数指明删除符合id值的记录,id是从查询列表传递过来的参数,按钮代码如下:
<input type="button" onclick="http://sql/delete.jsp?id=<%=id%>" target="_self" value="删除" style="width:50%"/> |
其客户端效果如下:
9.2.4.2 MAPP路由配置
点击“删除”按钮,会将表单提交到http://sql/delete.jsp这个虚拟地址,所以需要为该地址配置处理的JSP。如下:
<!-- 删除操作 --> <forward pattern="/delete.jsp.*" path="dbDelete.jsp"/> |
9.2.4.3 JSP处理请求
dbDelete.jsp中需要将数据库表中id值等于传递过来的id值的数据删除,代码如下:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" contentType="application/uixml+xml; charset=UTF-8"%> <%@ include file="/client/adapt.jsp"%> <% String id = aa.getReqParameterValueFromUrl("id"); String sql = "delete from tbl_task where id='?'"; %> <aa:sql-excute id="update" dbId="postgresql" sql="<%=sql %>"></aa:sql> <html type="alert"> <body> <alert title="提示" icontype="alarm"> <msg><%=aa.xpath("//result", "update").equals("1")?"删除成功!":"删除失败!"%></msg> <nextaction>script:close</nextaction> </alert> </body> </html> |
点击“删除”按钮并刷新列表,会发现数据已经不存在,如下图:
10 实现PUSH推送
推送功能在移动应用中是比较常见的功能,它主要起到对移动设备用户的一个提醒作用。
10.1 PUSH推送概述
PUSH推送功能是当第三方系统有新消息需要在ExMobi客户端提醒的时候,由第三方系统主动调用ExMobi服务端的推送接口,ExMobi服务端在推送接口中对第三方系统提交的信息格式化为客户端能够识别的推送信息,然后再推送给客户端来接收和展示推送消息。
10.2 推送的关键要素
要实现推送,关键要素有两个:推送的来源、推送的途径。
10.2.1 推送的来源
推送的信息一般包括:要推送的内容和推送内容的接收人。
这就跟发送短信很类似,比如A(第三方系统)要发送消息给B(ExMobi客户端),需要把短信内容(推送消息)和B的手机号码(接收人)告诉(调用接口)运营商的短信网关(ExMobi服务端),这样B就可以接收到A的信息了。
只是在细节上稍有不同,发送短信的内容是一些文本或者图片、视频等多媒体格式,而推送的内容一般是一个客户端的本地页面地址和一些接收参数,以该页面作为载体获取参数进行相应的逻辑处理;发送短信的接收人是手机号码,而推送的接收人其实是ESN和clientId。这是因为ESN是移动设备的唯一标识,clientId是客户端的唯一标识,只有通过ESN和clientId才可以唯一确定消息推送给哪个设备的哪个客户端(同一个客户端可以安装在不同移动设备)。
这里面有一个值得思考的问题——推送内容和接收人的来源是什么?
还是继续拿短信来说明,A给B发短信,A作为发送方应该本身知道发短信的内容,重点是在A怎么知道目标接收人B的手机号码,A不可能直接告诉短信网关接收人是某个人的姓名,因为短信网关并不知道这个接收人的手机号码是多少。这里说的“来源”就是这个意思。通常要么B主动给了A,或者A问B甚至是从那些知道B手机号码的人获取到的,这就是来源。如果A和B是在同一个企业中,那么他们的手机号码可能会存在某个系统中(比如OA的通讯录功能),A就可以通过B的姓名、部门等信息搜索到B的手机号码,这也是一种来源。
通过前面的抛砖引玉,推送也是一样的,通常知道是否有新消息、要推送什么消息内容的是第三方系统,推送给谁通常第三方系统也会知道(比如用户名、密码等用户信息),但是具体接收人的ESN和clientId是什么它就未必知道(因为这两个在ExMobi中才有)。所以这里还有一个关键就是如何能够把ESN和clientId信息与第三方系统中的用户信息关联起来。
前面了解到不管是在ExMobi的客户端还是服务端都可以获取ESN和clientId(JS或者JSP都有方法获取)。所以通常的做法就是,当用户使用ExMobi客户端登陆的时候,就可以获取当前登陆的用户名、ESN和clientId,把这几个基本信息通过接口(web service、数据库等)方式储存起来。这里的接口可以由第三方提供,也可以存储于任意的一个可以获取的地方。这就类似于上面发短信的例子,A和B的信息在企业的通讯录中都有记录,互相可以查询到。
10.2.2 推送的途径
从前面的例子可以看出,发送短信的途径是通过短信网关;而推送信息的途径是ExMobi的服务端。
ExMobi服务端概念比较抽象,再具体一点其实就是ExMobi服务端提供的推送接口。这个接口跟短信网关一样,需要接收第三方系统调用接口时传递过来的一些跟推送有关的参数,比如:用户名、ESN、clientId等等信息。
10.3 直推式推送实现
推送开发实际上是一个业务逻辑的确定和实现的过程,这个过程可以很灵活。这里提出了一种常用的方法做抛砖引玉。
10.3.1 推送逻辑分析
进行推送开发的时候,其实就是要解决关键要素的问题及其之间的业务逻辑:
1、 用户名、ESN、clientId存储于何处,提供何种接口来存储,ExMobi何时调用?(解决“来源”问题)
2、 第三方调用ExMobi的什么接口来触发推送,传递什么参数给ExMobi,何时调用?(解决“途径”问题)
10.3.1.1 解决来源问题
通常可以与第三方协商开放一个公用的数据库(存在于双方都可以操作的服务器中),建立用户信息关联表。当用户在ExMobi客户端登陆成功后,获取用户名、ESN和clientId等信息保存到该表中。这样就完成了第三方系统的用户信息和ExMobi客户端信息的关联。
这里我们沿用第9章数据库集成使用的数据库信息作为公用的数据库,创建表tsbbm_user包含esn、username、clientid三列,其信息如下:
10.3.1.2 解决途径问题
途径问题的解决通常有两种方案:
方案一:
由于tsbbm_user表中已经记录了用户名username与esn和clientId的关系,所以当第三方系统在有新的推送消息要推送的时候,只需要告诉ExMobi服务端的推送接口推送内容和接收人的用户名即可,在接口中再去数据库中获取该用户名对应的esn和clientId。
该方案的好处在于第三方系统改动不大,但是需要ExMobi的推送接口做一定的查询工作,会导致资源的浪费。也就是说不管用户有没有使用ExMobi客户端登陆成功,都会调用ExMobi接口去推送。很显然,如果没有使用过ExMobi客户端的用户的推送将不会成功,但是仍然耗费资源。
方案二:
第三方系统可以通过tsbbm_user表中已经存在的用户关联查询自己系统中该用户是否有推送消息,如果有的话则调用ExMobi的推送接口。因为已经进行关联查询,所以在调用接口的时候不需要传递username,只需把消息内容、ESN和clientId等信息传递给接口即可。
该方案有针对性的调用推送接口,可以较大程度的减少资源的浪费,但是对第三方系统改造较大。
实际的开发过程方案绝不止这两种,应该根据具体的实际情况确定合理的方案。
为了方便讲解,此处选择方案一。
10.3.2 推送实例开发
为了跟前面的内容达到相互照应,我们这里仍然通过“来源”和“途径”来说明如何进行示例的开发。
10.3.2.1 准备工作
我们在第7章ajaxdemo示例的基础上,先创建一个类似的应用pushdemo,由于需要调用数据库存储用户关联信息,所以先增加一个数据库配置。
由于此处内容前面章节均有涉及,下面简单把相关代码说明一下:
MAPP路由中的配置如下:
<?xml version="1.0" encoding="UTF-8" ?> <maxml version="2.0" xmlns="http://www.nj.fiberhome.com.cn/map" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.nj.fiberhome.com.cn/map maxml-2.0.xsd"> <config> <htmlformat wellformat="true" /> <!-- 配置伪域名 --> <domain address="miap.cc:1001" name="oa"/> <!-- 配置数据库连接参数 --> <database id="postgresql" dbtype="postgresql" ip="miap.cc" port="1006" dbname="app" user="sitest" password="123456" maxconn="10" minconn="2" defconn="5" /> </config> <route baseaddr="http://oa">
<!-- form的ajax提交 --> <forward pattern="/app/template/checkLogin.jsp" path="mCheckLogin.jsp"/>
</route> </maxml> |
应用的homepage地址为一个本地页面login.xhtml,如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>ajax登陆</title> <script> <![CDATA[
//表单AJAX提交提交开始===================================== function doFormSubmit(){ document.getElementById('form').submit(); } //表单提交成功回调函数 function onSuccess(data){ var result = eval('('+data.responseText+')'); if(result.status=='success'){//登陆成功分支 alert('登陆成功!'); }else{//登陆失败分支 alert(result.status); } } //表单提交失败回调函数 function onFail(data){
} ]]> </script> </head> <body> <!-- form表单的ajax请求关键在于设置success属性(必须)和fail属性(可选) --> <form success="onSuccess" fail="onFail" id="form" action="http://oa/app/template/checkLogin.jsp" method="post"> <!-- 构造登陆元素:用户名 --> <font color="red" style="width:30%">用户名:</font> <input type="text" id="username" name="username" style="width:70%"></input>
<!-- 构造登陆元素:密码 --> <font color="red" style="width:30%">密码:</font> <input type="password" id="password" name="password" style="width:70%"></input>
<!-- 点击表单登陆按钮触发表单的ajax提交 --> <input type="button" value="表单登陆" style="width:100%;" onclick="doFormSubmit()"></input>
</form> </body> </html> |
点击“表单登陆”按钮会进入JSP中进行处理,这个我们在下一节中继续。
10.3.2.2 解决来源问题
ajaxdemo中我们仅仅是把登陆的信息返回给客户端进行提示。现在需要解决来源问题,那么从前面的分析来看,我们就需要在JSP中根据登陆结果来判断是否登陆成功,如果登陆成功则操作数据库,把用户名username、esn和clientId信息入库。
下面对登陆提交的处理JSP文件mCheckLogin.jsp进行改造如下:
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%>
<%-- 发起默认请求即可,因为基本信息已经在form中组好 --%> <aa:http id="checkLogin"/>
<% //直接通过正则获取全部内容(请思考为什么不用xpath?),.*代表全部内容 String result = aa.regex(".*", "checkLogin"); %> <%-- 判断是否为登陆成功 --%> <aa:if test='<%=result.indexOf("success")>-1 %>'>
<% //username是在请求正文中以键值对方式存在,所以使用getReqParameterValue获取 String username = aa.getReqParameterValue("username"); //esn和clientid存在于头信息中,所以使用getReqHeaderValue获取 String esn = aa.getReqHeaderValue("esn"); String clientid = aa.getReqHeaderValue("clientid");
//组成sql语句 String sql = "insert into tsbbm_user (username, esn, clientid) values ('"+username+"', '"+esn+"', '"+clientid+"')"; %> <%-- 执行sql --%> <aa:sql-excute dbId="postgresql" sql="<%=sql %>"></aa:sql-excute>
</aa:if>
<%-- 把响应内容回给客户端 --%> <%=result%> |
这时候我们如果登陆成功会在数据库中看到有新纪录。
客户端效果:
数据库信息如下:
10.3.2.3 解决途径问题
途径问题其实就是ExMobi如何创建接口。
ExMobi的MAPP中通过config根节点-services节点-http-service节点来发布接口,每个接口都有一个forward转向配置,设定接口的地址(pattern)和对应处理的JSP(path)。如下:
<services> <!-- http-service用于发布http接口 --> <http-service> <!-- 每一个接口对应一个forward配置, 其中pattern是接口的被调用的名称,完整的调用地址为:http://{ip}:{port}/process/notify/{appid}/{pattern} 所以针对此配置,本机的调用接口应该为http://127.0.0.1:8001/process/notify/pushdemo/itask 调用接口的时候可以传递一些参数,参数可以在url中也可以在请求头或者请求正文中,参数可以是键值对也可以是非键值对 path是当接口被调用时服务端的处理JSP地址,在JSP中就可以获取调用接口时传递的参数 --> <forward pattern="/itask" path="push/itask.jsp"/> </http-service>
</services> |
10.3.2.4 在途径中获取来源
配置好接口之后就需要在接口对应的处理JSP中获取来源来触发推送。
根据前面的方案一的介绍,第三方系统调用接口的时候传递一个用户名和推送的相关信息,在对应的JSP中通过用户名获取其esn和clientid格式化成客户端识别的格式。假设我们约定用户名等推送相关信息我们通过URL传递,比如:
http://127.0.0.1:8001/process/notify/pushdemo/itask?username=admin&title=新任务 |
通常有可能消息的内容是一个表单,那么可能需要把表单内容作为参数传递,也以给一个URL能够获取到表单的内容。
itask.jsp页面就需要获取到这些参数,并查询数据库获取esn和clientid,其代码如下:
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%>
<% //推送接口传递的键值对参数不管是在url中还是在请求正文中买都是通过getReqParameterValue获取 String username = aa.getReqParameterValue("username");//获取用户名 String title = aa.getReqParameterValue("title");//获取title参数
%> <%-- 如果用户名不为空则执行sql查询相关信息,并对信息格式化 --%> <aa:if test="<%=username.length()>0 %>">
<% //组成sql语句 String sql = "select * from tsbbm_user where username='"+username+"'"; %>
<%-- 执行sql查询 --%> <aa:sql-excute id="select" dbId="postgresql" sql="<%=sql %>"></aa:sql-excute> <%-- 使用直推方式格式化数据,是一种固定格式 --%> <%-- directPushType:app,本应用的直推消息,收到后打开page/task.html页面 --%> <%-- immediately:是否后续操作立刻执行,1标示该消息的后续操作立刻执行,0标示用户点击后执行page:用户点击后打开的本地页面。此参数PC、android有效 --%> <%-- page:directPushType=app时有效,用户点击后打开的本地页面task.xhtml,在该页面中可以获取<aa:push-param>设置的参数--%> <aa:direct-push directPushType="app" title="<%=title %>" titleHead="ExMobi消息" page="page/task.xhtml"> <%-- 设置接收人 --%> <%-- 根据查询到的信息组成格式化的消息,使用for循环是考虑用户可能在多个设备登陆,具体视实际场景而定,也可以给其中一个推 --%> <aa:for-each var="list" xpath="//datarow" dsId="select"> <% //获取esn和clientid String esn = aa.xpath("./datacol[@name='esn']", "list"); String clientid = aa.xpath("./datacol[@name='clientid']", "list"); %>
<aa:push-receiver esn="<%=esn %>" clientid="<%=clientid %>"></aa:push-receiver>
</aa:for-each>
<%-- 指定直推页面参数列表,task.xhtml页面需要的参数,这里传递username和title --%> <aa:push-params> <aa:push-param name="username" value="<%=username %>" /> <aa:push-param name="title" value="<%=title %>" /> </aa:push-params> </aa:direct-push>
</aa:if> |
< aa:direct-push>标签为直推标签,是把第三方传递过来的参数格式化为ExMobi可以识别的信息格式。
其中:
directPushType是推送的类型,如果取值为notify则该消息推送到客户端后仅进行提示,点击消息无后续处理;当取值为app时,必须设置page属性指明一个应用的本地页面,当消息推送到客户端后点击消息可以打开page指定的页面,在该页面中,可以获取<aa:push-param>设置的参数。
这里指定的是task.xhtml页面。接下来我们就来编写,其代码如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title show="false">title</title> <script> <![CDATA[ function doLoad(){ //获取传递的参数 var username = window.getPushParameter("usernmae"); var title = window.getPushParameter("title");
//对获取的参数进行相应的处理,这里修改标题内容 document.getElementById("titleBar").title = title;
/* 由于推送信息不能过多,很多信息是在推送页面中再获取的(苹果的推送通道就限制推送的内容大小) 比如:第三方调用的时候常传递的参数可能还有任务的id或者url等信息,是具体情况而定 比如传递一个id,可以获取id组成url发起ajax继续拉取数据,并进行相应的操作 var id = window.getPushParameter("id"); var url = "http://oa/getDetail.jsp?id="+id; var ajax = new Ajax(url ......);
*/ } ]]> </script> </head> <header> <titlebar id="titleBar" caption="返回" iconhref="script:close" title="" hidericon="true"></titlebar> </header> <body onload="doLoad()"> This is the page content. </body> </html> |
在这个页面中,可以通过JS函数window.getPushParameter(key);获取推送接口中<aa:push-param name=”” value=””>的参数,其中key对应的是name属性。
由于一些推送通道对推送消息的内容大小有限制(比如苹果的APNS通道),不能传参太多,所以通常有时候传递一些唯一标识作为参数,在push.xhtml页面获取后再次请求更多的信息。
10.3.2.5 第三方调用接口
ExMobi的推送功能开发好之后就可以让第三方系统来调用接口或进行测试。
我们这里就用浏览器来模拟ExMobi推送接口的调用,在浏览器中输入如下地址:
127.0.0.1:8001/process/notify/pushdemo/itask?username=admin&title=这是新任务 |
这里传递了两个参数username和title,浏览器中可以看到推送接口返回的信息:
这时候客户端就会看到有推送消息:
点击“确定”按钮,就会打开push.xhtml页面展示内容:
这就完成推送功能的调试。同样,第三方系统通过代码来调用该接口的效果也是一样的。如果达不到效果可能是由于前面的沟通不畅,导致一些参数等信息不匹配,需要继续互相调试。
11 附件预览和下载
烽火中间件是集成第三方工具进行转换附件,预览查看,有openoffice和永中可以选择,openoffice是免费的,永中是收费的,两种工具中必须装一个。
11.1 永中与openoffice的比较
(一) 性能比较
转换方式 | Openoffice(只能单线程) | 永中(2线程) | 永中(5线程-未忽略网络请求时间) |
50页word纯文字(166k) | 90089 | 23676 | 13292 |
5页word带图(5M) | 301793 | 67047 | 50890 |
openoffice在转换一个50页的word文档所需要的时间是永中(2线程)的4倍左右、是永中(5线程)的7倍左右。
openoffice在转化一个5页带图的文档所需要的时间是永中(2线程)的4倍,永中(5线程)的6倍。
显然在性能上永中office要优于open office。
(二) 可靠性比较
Openoffice在转换office的时候,会出现一页变两页,内容无法转换,乱码等现象,可靠性较差。在永中office测试过程中,出现的概率相对来说小一些。
尤其在WPSoffice转化中,永中office的转化效果远好于openoffice。
11.2 支持的预览格式
第三方工具openoffice、永中,必须装一个,下面介绍下两者对于附件预览的支持程度。
11.2.1 共同支持的格式
1) 图片文档文件类型
bmp;icon;ico;jpg;jpeg;psd;png;gif;tif;tiff;sep;gd
2) 压缩文档文件类型
zip;rar;7z;tar;gz;bz2;cab
3) 文本文档文件类型
txt;bat;sh;xml;html;xhtml;htm;mht;xsl;jsp;ini;inf
11.2.2 永中支持的格式
doc;docx;ppt;pptx;xls;xlsx;rtf;wps;ett;dps;pdf;html;ceb
11.2.3 Openoffice支持的格式
doc;docx;ppt;pptx;xls;xlsx;rtf;pdf;ceb;wps;ett;dps
11.3 文档转换服务配置
首先需要再Mbuilder的首选项“Preference”中配置第三方服务,如下图所示:
确定后需要重启Tomcat服务。
11.4 客户端代码实现
1、可以直接写附件控件,
控制菜单项,“下载|预览|打开,1表示显示,0表示不显示。支持格式如下:
000:无
001:打开
010:预览
011:预览 & 打开
100:下载
101:下载 & 打开
110:下载 & 预览
111:下载 & 预览 & 打开
代码示例如下:
<fileset caption="附件"> <item text="开发手册.chm" options=”111” href="实际下载地址"/> <item text="java学习教程.ppt" options=”111” href="实际下载地址"/> </fileset> |
2、也可以在有链接的地方,配urltype进行预览、下载、打开,支持如下取值:
normal: 普通打开页面(默认)
preview: 预览方式打开
download: 执行下载请求,类似fileset点击下载菜单
openfile: 下载后直接打开,类似fileset点击打开菜单
代码示例如下:
<a href=”” urltype=” preview” >预览文档</a> <a href=”” urltype=”download” >下载文档</a> <a href=”” urltype=”openfile” >打开文档</a> |
11.5 特殊控件或格式附件预览
ExMobi对特殊控件的预览支持还包括:金格控件加密的文件、书生SEP文件、方正CEB文件、点聚API文件的支持。
其中书生、方正和点聚的文件格式是依赖于原厂商的转换软件,所以需要购买相应的授权软件方能支持。
11.6 非标准格式响应附件预览
ExMobi对文档格式的识别,是通过文档请求的响应头信息Content Type来匹配的,如下:
application/msword=doc application/vnd.ms-word=doc application/rar=rar application/pdf=pdf application/vnd.ms-excel=xls application/vnd.ms-powerpoint=ppt application/x-javascript=js application/x-sh=sh application/x-tar=tar application/xhtml+xml=xml application/xml=xml application/zip=zip image/bmp=bmp image/gif=gif image/ief=ief image/jpeg=jpg image/png=png image/tiff=tif image/x-portable-pixmap=ppm text/css=css text/html=html text/plain=txt text/xml=xml text/uixml=xml image/vnd.png=png application/vnd.openxmlformats-officedocument.wordprocessingml.document=docx application/vnd.openxmlformats-officedocument.presentationml.presentation=pptx application/vnd.openxmlformats-officedocument.spreadsheetml.sheet=xlsx wps/doc=wpt wps/ppt=dpt wps/excel=ett application/sep=sep |
如果标准文档的响应格式不是标准文档格式,比如一个word文档响应的不是application/msword 而是text/plain或者编码错误或者文件名乱码等,那就需要轻质转换响应的Content Type,让ExMobi知道实际的文档格式。
11.6.1 在请求中设置
在<aa:http/>请求中增加属性mimetype为相应的类型可以设置请求响应的Content Type,比如:
<aa:http mimetype="application/msword"/> |
这样该请求的响应结果就强制转换类型为doc。
11.6.2 客户端设置文件格式
有很多具有附件预览或者下载的控件,比如img控件、fileset控件、input:button控件都有filename属性,该属性为文件名+后缀名格式。通过该属性可以重命名文件名,也可以指明文件的类型。比如:
<img src="http://domain/a.do" href="http://domain/a.do" urltype="preview" filename="test.jpg"/> |
上面的代码运行的结果就是点击这个图片的时候就会预览该图片,预览的时候图片的名字被强制命名为test.jpg,并且content-type强制设置为image/jpg。
11.6.3 服务端设置文件格式
该方法需要给文档请求地址配置MAPP路由,走jsp进行强制处理。当<aa:http>请求完毕后处理完毕后可以使用<aa:file-download>进行下载处理或者使用<aa:file-preview>进行预览。如何确定当前请求过来是下载请求还是预览请求,可以通过aa.isDownLoad()函数来确定返回true则为下载,否则为其他方式。
比如:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%> <aa:http> … </aa:http>
<aa:choose> <aa:when test='<%=aa.isDownLoad()%>'> <aa:file-download filename=”测试模板.doc”/> </aa:when> <aa:otherwise> <aa:file-preview filename=”测试模板.doc”/> </aa:otherwise> </aa:choose> |
这两个标签设置filename的结果也是除了重命名文件名,同时也会按照后缀格式重新设置content-type。
11.6.4 使用JSP的response设置
JSP的reponse内置对象中本身也可以设置响应的content-type,比如:
response.setContentType("application/x-tar"); |
第4篇ExMobi安装部署
12 部署应用
应用开发完毕后,如果已经部署好工程版的ExMobi(工程版有为正式安装部署的版本,可一键安装),就可以将应用导出并发布到后台管理系统,发布成功后就可以使用真机安装客户端使用。
12.1 导出应用
MBuilder集成开发工具提供了将开发版的应用导出为发布版的应用。
在MBuilder中选中要发布的应用的根目录,并找到发布的快捷图标点击进入发布对话框,如下图所示:
选择第二项“Export Zip Packpage”指明打包一个要发布的应用包,这里是把包保存到桌面,点击“Finish”后可以在桌面看到*.zip格式的应用包,如下图所示:
12.2 发布应用
发布应用前需要把ExMobi工程版本部署好。将导出的应用上传到工程版的EMP管理平台中。如何下载和安装ExMobi工程版本请访问ExMobi官网。
下面以培训服务器部署好的ExMobi工程版环境为例,说明应用发布过程。
1) 打开EMP管理平台,比如:http://miap.cc:1001/,其界面如下:
输入默认用户名/密码:admin/admin111,即可登录EMP系统。
2) 通过菜单“应用管理”-》“应用版本发布”-》“新增”,如下图所示:
打开发布界面,选择MBuilder生成的*.zip格式的包,并点击“上传”,如下图所示:
3) 上传完毕后的界面如下图所示,点击“下一步”:
4) 进入“应用信息确认”对话框,如果信息无误,点击“确认”后即可完成应用发布,如下图所示:
12.3 使用基座客户端测试
应用成功发布后,即可使用基座客户端访问测试,设置ip为服务器的ip地址,端口默认为8001,如下图所示:
配置好后即可使用客户端下载应用,并使用。
12.4 客户端打包发布
在第一章中上一节讲到的是使用基座客户端测试,如果测试通过后即可到EDN门户中打包,成为打包客户端,作为开发交付的一部分。
打包客户端可以在EDN门户的“项目打包管理”中进行,如下图所示:
打包前需要创建项目,然后在项目中创建应用,最后再创建的应用中可以打包,如下图所示:
该应用的历史打包记录可以在“历史打包”中查看,并且可以在里面下载已经成功打包的客户端。
第5篇ExMobi设计与扩展
13 主题设计和制作
ExMobi应用中支持主题的使用。并且在集成开发环境MBuilder已经内置了一些主题,在创建应用的时候可以选择使用某个模板主题。也可以开发好一个主题之后在任意ExMobi应用中使用这个主题。
13.1 内置主题的使用
MBuilder中已经内置了一些主题,内置主题的使用方法是在创建应用的时候进行选择的,以usetheme应用为例。
13.1.1 创建应用使用主题
首先创建一个usetheme的应用,如下:
接着连续点击多个“Next”之后,直到进到“选择模板主题”窗口,如下:
勾选使用模板,并且选好模板和主题,点击“Finish”即可,如下图所示:
可以看出主题目录下会自动生成刚才选择的主题的文件内容。其中cdf.xml文件的id为skin-colorful:
同时config.xml中也把主题自动配好:
13.1.2 创建文件使用模板
只有选择了内置主题的情况才能在创建文件的时候使用模板,因为MBuilder的内置模板设定了一些配套的模板,快速生成界面元素方便开发。自己开发的主题创建的文件只能使用默认模板,页面内容需要手动书写,但是主题仍然会生效。
usetheme应用的首地址为login.xhtml,下面就以该文件为例说明如何创建文件使用模板。
如上图所示,在需要创建文件的文件夹中点击右键-“New”-“XHMTL Page Template”即可弹出选择模板窗口。如下:
这里可以看出总共有9个模板页面,选择“登陆”模板并点击“Next”会弹出模板的预览窗口:
点击“Finish”即可使用该模板,可以看到page下创建login.xhtml文件成功:
同步应用后可以在PC基座模拟器上查看效果:
如果页面内容需要调整和改动,继续编辑login.xhmtl文件即可。
13.2 主题的开发和使用
除了使用MBuilder中的内置主题,开发者也可以根据自己的需要开发自己的主题。
开发主题之前需要先了解ExMobi的单位dp,可以参考附录16.1。
13.2.1 主题的目录结构
主题是有主题配置文件cdf.xml和图片文件组成。图片的根目录名为image,并且与cdf.xml文件同级。主题适用哪个分辨率就置于那个分辨率下,如下所示:
cdf.xml文件中可以使用主题内的图片,引用的方法为:
theme:图片路径 |
其中,图片路径为主题内的image目录下的文件路径,从image目录开始,比如image目录下的icon子目录有checkbox_overlay.png图片,则写法为:
theme:image/icon/checkbox_overlay.png |
13.2.2 开发默认主题
开发默认主题首先要创建一个测试应用,下面以maketheme应用为例。
首先创建一个空白应用(不使用MBuilder默认主题),如下:
可以看到空白应用默认也会有一个主题目录,但是对应的cdf.xml文件只有没有具体的配置。开发主题的过程实际就是完善cdf.xml的过程。
cdf.xml文件的节点和层级结构如下:
节点 | 节点描述 | 节点属性及属性描述 | 必选项 | ||
cdf | 应用主题配置文件的根节点 | 无属性 | 是 | ||
应用主题基本信息 | |||||
id | 应用主题标识,只能由英文字母、数字、下划线、@、连字符组成。id属性值与应用主题文件夹名称必须保持一致 | 无属性 | 是 | ||
name | 应用主题名称 | 无属性 | 否 | ||
description | 应用主题描述 | 无属性 | 否 | ||
date | 应用主题发布日期 | 无属性 | 否 | ||
应用主题开发者信息 | |||||
vendor | 应用主题开发者信息节点 | 包含属性:url,email | 否 | ||
url | 应用主题开发者主页,属性值可为空 | 否 | |||
| 应用主题开发者邮箱地址,属性值可为空 | 否 | |||
应用页面元素配置信息 | |||||
setting | 应用页面元素配置信息节点 | 无属性 | 是 | ||
attrs | 定义应用中页面元素的通用属性。优先级低于控件内部自定义样式。 | 包含border-color,border-radius,font-size,color,margin属性 | 否 | ||
border-color | 边框颜色 | 否 | |||
border-radius | 边框弧度 | 否 | |||
font-size | 文字大小 | 否 | |||
color | 文字颜色 | 否 | |||
margin | 控件四周间隔 | 否 |
具体各个控件可以设置的默认样式可以到ExMobi官网下载二次开发手册了解,下面以input:button按钮控件为例说明cdf文件的开发过程。
在login.xtml文件中先增加一个按钮,如下所示:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>主题开发</title> <script> <![CDATA[
]]> </script> </head> <body> <input type="button" value="默认样式的button"/> </body> </html> |
其效果如下:
可以看到显示的按钮样式是默认的样式。
现在需要给按钮增加主题以设置默认样式,就要先了解按钮有哪些可以设置的cdf默认样式,需要先查阅手册。
在手册中找到<input:button>按钮,可以看到“CDF设置”的索引,如下:
点击后即可看到按钮控件在CDF中可以设置的默认样式如下图所示:
了解了可以设置的样式,下面就可以编写CDF文件,如下所示:
为了方便查看,主题的目录和主题的id均设置为“testtheme”。cdf.xml配置文件内容如下:
<cdf> <id>testtheme</id><!-- 主题的id,用于config.xml中应用该主题 --> <name>测试主题</name><!-- 主题的名称 --> <description>给按钮增加默认样式</description><!-- 主题的基本描述 --> <date>2013-12-12</date><!-- 主题的开发日期 --> <vendor url="http://www.exmobi.cn" email="edn@exmobi.cn"></vendor><!-- 开发者信息 --> <!-- setting可以配置所有控件的默认属性以及各个控件的默认样式 --> <setting> <button overlay="none" overlay-click="none"border-color="#007a57" border-size="0" background-color="#eb9b34" background-click-color="#db7722" border-click-color="transparent" color="white" click-color="white"/> </setting> </cdf> |
在config.xml中使用testtheme的主题,如下所示:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <config theme="testtheme" clientversion="4" scope="client" devicetype="all"> <appid>maketheme</appid> <appname>主题开发</appname> <description></description> <version>1.0</version> <date>2013-12-15</date> <homepage src="res:page/login.xhtml"/> <faultconfig src=""/> <vendor url="" email=""></vendor> <access orientation="port" land="false" network="true" gps="true" camera="true" certificate="true"/> <icon selectedlogo="res:image/selectedlogo.png" main="res:image/main.png" logo="res:image/logo.png"/> </config> |
重新打开login.xhtml页面就会看到默认效果已经变为:
这样就完成了一个控件的默认样式的开发,其他控件以此类推。
13.2.3 使用默认主题
开发好的主题可以用于任意的ExMobi应用中。
值需要把主题的整个目录拷贝到相应的应用中,然后在config.xml中使用该主题即可。
下面继续在usetheme应用的基础上改成使用testtheme的主题。
首先需要把maketheme应用的testtheme的整个目录原样拷贝到usetheme应用的主题目录下,目录结构一样,如下所示:
然后编辑config.xml设置theme的值为testtheme,如下所示:
这样就完成新主题的使用。
14 Native插件的开发和使用
14.1 概述
为了满足更多的应用场景,有时候需要扩展ExMobi的能力。
Native插件提供了这样一种扩展方式:熟悉原生开发的人员根据ExMobi的接口要求开发插件,嵌入到ExMobi应用中,提供给应用开发人员使用。
ExMobi支持合作伙伴开发Android、IOS Native插件,通过EDN打包可方便集成Native插件。烽火星空会封装一些常用插件供选用,合作伙伴也可开发自己的插件上传至EDN,打包时选择需集成插件即可轻松生成集成插件版本的Apk安装包或Ipa安装包。
下面简单描述下插件开发流程:
1: EDN上获取Android/IOS 插件开发示例工程;
2:按照插件开发说明构建Native插件,开发完成生成.jar包/.a包;
3:将.jar包/.a包与工程所需资源压缩为zip包;
4:登录EDN门户,上传插件zip包,选择所需基准版本打包;
5:打包完毕后下载即可得到包含插件功能的Apk/Ipa安装包。
14.2 Android插件开发与使用
14.2.1 开发
14.2.1.1 工程创建
开发Native插件的步骤如下:
1) 从Mbuilder中获取Android工程AppPlugin.zip,以这个工程为基础,参考其中的示例进行开发
可以看到项目内文件结构如下:
每构建一个native插件,均需创建以com.appplugin +.插件名的package包,如上图所示的
com.appplugin.SmsComponent, 该package包内需要包含子stub包,stub包包含Component.java,
ComponentContext.java, ResManager.java三个固定桥接类,这三个类切记不要修改。
Component.java:组件基类,自定义插件类需继承于该类,切勿修改;
ComponentContext.java:组件桥接类,切勿修改;
ResManager.java:资源处理类,包含getResourcesIdentifier,getResource两个函数
int getResourcesIdentifier(Context context, String tag):用于获取工程内资源,包括图片,布局,字符串定义文件等,使用工程内资源时用于代替系统R.layout.xml, R.drawable.icon, R.string.stringname等。
Drawable getResource( String filename):用于读取ExMobi应用内图片文件,返回drawable对象。
2) 修改ComponentFactory.java文件,在其中注册开发的Native插件:
3)开发继承于Component的插件XXXComponent,实现下列方法
各方法说明如下:
方法 | 说明 |
init | 初始化相关资源 |
release | 释放相关资源 |
getView | 返回用于显示的Android View(可以是LinearLayout,RelativeLayout等容器View) 对于不可见插件,返回null |
set | 设置插件属性值,此方法会在2处被调用: 1) 页面中插件xml节点中定义的属性,会通过此方法设置给插件 2) 页面中Javascript调用插件的set方法 (Javascript中int会被自动转换为String类型传递给插件) |
addChildElement | 增加子节点数据,此方法会在1处被调用: 1)页面中插件xml节点定义了子节点,页面会调用addChildElement传递节点数据给插件,根据子节点数量调用多次 |
get | 获取插件的属性值,此方法会在1处被调用: 1)页面中Javascript调用插件的get方法 |
call | 调用插件的方法,此方法会在1处被调用: 1)页面中Javascript调用插件的call方法 |
下图以CityComponent描述了插件的初始化过程:
4)编译工程,编译通过后导出工程jar包,导出工程包时需要注意,只需选中src文件即可,其他目录如assets目录,res目录,libs目录导出时切勿勾选,即保证导出的jar包只有插件代码文件。
14.2.1.2 示例1:SmsComponent的实现
SmsComponent的代码如下:
public class SmsComponent extends Component { private String phonenumber = ""; private boolean attachSignature = false; private String signature = ""; private long sentMsgs = 0; private String onSent = "";
Handler handler;
@Override public void init() { this.phonenumber = "19909900001"; this.handler = new Handler() { @Override public void handleMessage(Message msg) {
} }; }
@Override public void release() {
}
@Override public View getView() { return null; } @Override public void setViewSize(int w, int h){
}
@Override public void set(String name, String value) { if ("attachSignature".equals(name)){ this.attachSignature = "true".equals(value); }else if ("signature".equals(name)){ this.signature = value; }else if ("onSent".equals(name)){ this.onSent = value; }
}
@Override public String get(String name) { if ("phonenumber".equals(name)){ return this.phonenumber; }else if ("attachSignature".equals(name)){ return "" + this.attachSignature; }else if ("signature".equals(name)){ return this.signature; } return null;
}
@Override public String call(String functionName, String param1, String param2, String param3, String param4, String param5, String param6, String param7) { if ("sendSms".equals(functionName)){ // comp.call("sendSms", "今天下午点开会", 10); String content = param1; int delaySeconds = Integer.parseInt(param2); this.sendSms(content, delaySeconds);
return null; }else if ("getSent".equals(functionName)){ return "" + this.sentMsgs; }else{ Log.i("SmsComponent", "ERROR: unsupported function call : " + functionName); return null; }
}
private void sendSms(String content, int delaySeconds){ String smsText = content; if (this.attachSignature){ smsText += this.signature; } smsText += " [发送自" + this.phonenumber + "]";
final String msg = smsText; this.handler.postDelayed(new Runnable(){ public void run(){ Log.i("SmsComponent", msg); Toast.makeText(helper_getAndroidContext(), msg, 5000).show(); sentMsgs++;
if (onSent != null && onSent.length() > 0){ String script = String.format("%s( '%s' )" , onSent, msg); SmsComponent.this.helper_callJsScript(script); }
} }, 1000*delaySeconds);
}
}
|
其中几个重要的点解释如下:
>> SmsComponent.this.helper_callJsScript(script);
可以通过调用Component的helper_callJsScript来调用页面中的脚本函数。
>> else if ("onSent".equals(name)){ this.onSent = value; }
onSent作为事件属性,和普通的属性没有区别。
插件事件触发时,通过onSent保存的函数名称,拼装js脚本,进行调用。
14.2.1.3 示例2:CityComponent的实现
CityComponent的代码如下:
public class CityComponent extends Component {
static class City{ String name; String code; City(String name1, String code1){ this.name = name1; this.code = code1; } }
String icon; String onSelected; List<City> cities = new ArrayList<City>(); IndexedTable mIndexedTable;
@Override public void init() { ResManager.getInstance().init(false, super.helper_getAndroidContext(), super.helper_getAppId()); }
@Override public void release() { this.cities = null; }
@Override public View getView() { if (this.mIndexedTable == null){ initView(); } return mIndexedTable; }
private void initView(){ IndexedTable table = new IndexedTable(super.helper_getAndroidContext()); if (this.icon != null && this.icon.length() > 0){ table.icon = this.icon; }
table.initView();
Collections.sort(cities, new java.util.Comparator<City>() {
public int compare(City o1, City o2) { return o1.code.compareTo(o2.code); }
});
List<Content> list = new ArrayList<Content>(); for (City city : this.cities){ list.add((new Content(city.code, city.name))); } table.setContent(list);
this.mIndexedTable = table; this.mIndexedTable.listener = new OnItemSelectedListener(){
@Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { selectCity(position); }
@Override public void onNothingSelected(AdapterView<?> parent) {
}
}; }
private void selectCity(int index){ if (onSelected != null && onSelected.length() > 0){ City city = this.cities.get(index); String script = String.format("%s( '%s', '%s'); ", onSelected, city.code, city.name); super.helper_callJsScript(script); } }
@Override public void setViewSize(int w, int h) {
}
@Override public void set(String name, String value) { if ("icon".equals(name)){ this.icon = value; }else if ("onSelected".equals(name)){ this.onSelected = value; } }
@Override public String get(String name) { if ("icon".equals(name)){ return this.icon; }else if ("onSelected".equals(name)){ return this.onSelected; } return null; }
@Override public void addChildElement(String tag, Object/*Map<String, String>*/ attributes){ Map<String, String> attrs = (Map<String, String>)attributes; String name = attrs.get("name"); String code = attrs.get("code"); this.cities.add(new City(name, code)); }
@Override public String call(String functionName, String param1, String param2, String param3, String param4, String param5, String param6, String param7) {
return null; }
}
|
其最复杂的函数是initView,在这个函数中,构造Android的View插件,进行初始化。
其它关键代码点:
>> ResManager.getInstance().init(false, super.helper_getAndroidContext(), super.helper_getAppId());
>> imageView.setImageDrawable(ResManager.getInstance().getResource(icon));
初始化图片文件管理。此处通过ResManager.getInstance().getResource(icon)方法使用ExMobi应用目录image/下的图片。
>> public void setViewSize(int w, int h) { }
ExMobi客户端会自动设置好getView返回的Android View的尺寸。所以一般不需要在setViewSize函数中做什么操作。只有特殊情况,需要根据View的大小进行相关处理时,可以从这个函数得到View的大小。
14.2.1.4 注意事项和FAQ
l 能否在插件工程中引入第三方jar包
可以,同普通Android工程一样导入即可,注意导入jar包需要放置到工程lib目录里面。
l 工程目录结构如何组织
同普通Android工程类似,包含assets,res,libs目录。
assets:放置打包在apk中的资源,如较小的图片,较小的音频文件等,打包时assets目录下的文件将不做任何处理被打包,不会被编译为R.java;
res:放置应用图片,应用布局文件,应用文本xml等,放置于res的文件打包时候参与编译,被编译为R.java格式数据。
libs:放置插件工程引用的jar包。
l 插件工程package包结构如何组织?
以com.appplugin(固定) +.插件名(建议采用唯一标识防止与其他插件重复)的package包,如:
com.appplugin.SmsComponent, 该package包内需要包含子stub包,stub包包含Component.java,
ComponentContext.java, ResManager.java三个固定桥接类。用户其他自定义类及package需要放置于com.appplugin(固定) +.插件名包内。如下图结构:
l 能否使用插件工程中assert目录下的图片
可以,采用android标准读取方式即可。
如:AssetManager a = getAssets() ;
//fileName为assets目录下需要访问的文件的名称
InputStream is = a.open(fileName) ;
l 能否使用插件工程中res目录下的图片
可以,但注意资源id不能直接用R.drawable.xxid 引用,需要使用ResManager.java类方法 public int getResourcesIdentifier(String tag)
如:Bitmap originalImage = BitmapFactory.decodeResource(mContext.getResources(), ResManager.getInstance().getResourcesIdentifier("R.id.fiberhomeplugin_gallery_flow"));
l 能否使用插件工程中res/layout目录下的布局
可以,但注意资源id不能直接用R.layout.xxid 引用,需要使用ResManager.java类方法 public int getResourcesIdentifier(String tag)
如:LayoutInflater mInflater = LayoutInflater.from(mContext_);
View currView = mInflater.inflate(
ResManager.getInstance().getResourcesIdentifier("R.layout.fiberhomeplugin_galleryflow_main"), null) ;
l 插件工程assets, res目录下文件命名有什么规则
所有文件建议增加特殊前缀避免打包时资源冲突,如fiberhomeplugin_galleryflow_main.xml, fiberhomeplugin_gallery_flow.png。
l 如何获取ExMobi应用下的图片
参考CityComponent中ResManager的实现,通过ResManager.getInstance().getResource(“res:/image/XXX.png”)来得到图片,注意仅支持res:前缀的应用内图片。
如:Drawable iconDraw = ResManager.getInstance().getResource("res:/image/mm_contact_title");
l 如何得到当前界面的Android Context
通过super.helper_getAndroidContext()方法获取。
l 如何调用ExMobi页面中的脚本
通过super.helper_callJsScript(script)方法来调用。
l 一个插件工程能否编写多个插件类
可以,建立多个package包,按照工程创建说明构建多个插件类即可,如下图所示,该插件工程包含了
MyHttpComponent(自定义http插件),CityComponent(列表插件),GalleryComponent(滑动容器插件)等多个插件,只需导出一个jar包即可。
l 如何测试
由于插件需要在EDN打包后才能实际使用,为了更好的开发测试,提供了native-common.jar测试包,将测试jar导入工程中,可以很容易针对自己的组件编写测试用例并进行测试,详见自测章节描述。
14.2.2 自测
14.2.2.1 测试类构建
自测Native插件步骤如下:
1)AppPlugin工程libs文件夹下有个native-common.jar,拷贝jar到插件的libs下,文件结构如下:
2)在插件工程中创建一个测试窗口TestActivity,通过这个窗口测试插件view的展示和具体操作,比如插件数据注入,JS方法调用,获取应用的图片等
具体结构如下:
3)测试获取应用图片:
在手机的SD卡中创建应用资源目录(Exmobi/apps/newclientapp/image),如图:
TestActivity中调用插件时设置appID为newclientapp,设置工程目录名称为Exmobi,图片路径为:res:/image/xxx
14.2.2.2 示例:CityComponent的测试用例
TestActivity的代码如下:
public class TestActivity extends Activity { com.exmobi.applguin.Component com; String comType = "CityComponent"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.requestWindowFeature(Window.FEATURE_NO_TITLE); //创建插件管理 PluginManager com.exmobi.applguin.PluginManager plugin = new com.exmobi.applguin.PluginManager(); /** * 通过插件TYPE加载插件 * @param appId 应用ID 测试用例中可以写具体的应用ID,也可以为空 * @param type 插件type 插件的type,比如CityComponent */ plugin.loadNativeComponents("newclientapp", comType); /** * 通过插件TYPE创建插件 * * @param type * 插件类型 ,比如CityComponent * @param view * 插件对应EXMOBI VIEW,这里可以为null * @param activity * EXMOBI 窗口 */ com = plugin.createComponentByType(comType, null, this); /** * 设置插件属性 * @param type 插件类型 ,比如CityComponent */ setNativeComAttributes(comType); //设置工程创建的目录名称 com.setProjectName("Exmobi"); //设置测试数据 HashMap<String, String> map = new HashMap<String, String>(); map.put("name", "北京"); map.put("code", "BEIJING"); com.addChildElement("city", map); //调用插件中的init()方法 com.init(); //获取插件view View nativeCompVew = com.getView(); //将插件view通过TestActivity显示 this.setContentView(nativeCompVew); }
private void setNativeComAttributes(String type) { com.set("type", type); com.set("id", "city"); com.set("onSelected", "onCitySelected"); com.set("icon", "res:/image/big1.jpg"); } }
|
14.2.2.3 注意事项和FAQ
l 如何模拟控件xml初始载入
调用com.exmobi.applguin.Component类的loadxml()方法,可以模拟ExMobi页面控件的加载,如:
l 控件的属性测试
调用com.exmobi.applguin.Component类的set()和get()方法,可以测试插件属性设置是否成功
如:
exmobi调用是先解析XML得到控件的属性值,这里的测试用例省略了解析过程,直接设置属性值
l 控件回调的JS方法测试
调用helper_callJsScript方法测试控件回调的JS方法名和参数设置是否正确
如:
l Exmobi调插件方法测试
调用com.exmobi.applguin.Component类的call方法,具体调用如下:
/** * 插件中的call方法调用 * * @param params * 参数数组 * @return */ public String call() { button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { String[] params = { "addChildElement", "上海", "SHANGHAI" }; com.call(params); } }); return ""; } |
在插件中接收参数,验证参数是否正确,如下:
@Override public String call(String functionName, String param1, String param2, String param3, String param4, String param5,String param6, String param7) { android.util.Log.d("call", "functionName: " + functionName + ",param1:" + param1 + ",param2:" + param2); if ("addChildElement".equals(functionName)) { HashMap<String, String> map = new HashMap<String, String>(); map.put("name", param1); map.put("code", param2); addChildElement("city", map); return null; } else { return null; } } @Override public void addChildElement(String tag, Object/* Map<String, String> */attributes) { Map<String, String> attrs = (Map<String, String>) attributes; String name = attrs.get("name"); String code = attrs.get("code"); this.cities.add(new City(name, code)); if(mIndexedTable != null) { List<Content> list = new ArrayList<Content>(); for (City city : this.cities) { list.add((new Content(city.code, city.name))); } mIndexedTable.setContent(list); } } |
l 打包的时候需要将测试用到的native-common.jar和TestActivity删除避免类冲突
14.2.3 打包
14.2.3.1 打包步骤
1)切换到插件工程文件夹目录,选中assert文件夹,libs文件夹,res文件夹,生成的plugin.jar文件,添加到zip压缩文件中,生成插件zip压缩文件。
2)打开EDN打包页面,上传插件zip包,选择所需基准版本打包,打包完毕后下载即可得到包含插件功能的Apk安装包。
14.2.3.2 注意事项和FAQ
l 上传包包含哪些数据?
包含插件工程中的assert文件夹,libs文件夹,res文件夹,以及插件jar包,如下图所示。
l 上传包格式可以为rar吗
不能,必须为zip格式。
l 安装包集成时能否加载多个插件zip包
可以,建多个插件工程,构建native插件完毕后,导出插件jar包,同资源一起压缩为zip包提交EDN打包即可。
l 常见打包错误说明
14.2.4 使用
Native插件分为不可见插件、可见插件两种,两种插件的XML表示方法、Javascript函数是一致的。
不可见插件一般用于支持某种算法、硬件能力;
可见插件用于提供UI插件,嵌入到应用页面中。
Native插件用<nativecomponent type=”XxxComponent” …./>来嵌入到页面中。
插件具有属性、方法、事件,一个插件具体可以调用哪些方法和属性,是Native插件的开发人员来提供说明的。
下面用两个例子,分别进行讲解:
14.2.4.1 示例:SmsComponent 短信能力插件
SmsComponent插件提供发送短信的能力。
SmsComponent为不可见插件,通过<nativecomponent …/>元素将其嵌入到页面中,其中type=”SmsComponent”属性指明了其插件类型。
一个页面中可以嵌入任意数目的不可见插件。
插件可以有数据属性和事件属性;SmsComponent具有下列属性:
属性 | 意义 |
attachSignature | 短信内容是否附加签名(数据属性) |
signature | 签名内容(数据属性) |
onSent | 短信发送成功时,回调的javascript函数。(这是一个事件属性,一般是事件触发时,插件回调的Javascript函数名称) |
插件的属性,可以通过javascript的get、set方法来获取和修改。
SmsComponent的能力是通过javascript来调用的,点击“test”按钮后,触发test1函数,在test1函数中,调用SmsComponent的 call(‘sendSms’, content, delay)来发送内容。
需要注意的是:调用插件的方法名称,例如sendSms,是作为首参数传递的。
如果需要更加友好的调用方式,可以用Javascript进行二次封装。下面是一个参考的封装方式:
在短信发送成功后,插件会触发onSent事件:
下面是界面截图:
14.2.4.2 示例:CityComponent 城市选择插件
CityComponent提供了选择某个城市的功能,可以根据拼音进行排序快速选择:
CityComponent插件是可见插件,一个页面中只能有一个。
从上面XML可以看到,插件还可以有子节点(根据需求可选)。
CityComponent的属性如下:
属性 | 意义 |
icon | 列表左侧的png图标,图片位置和格式和exmobi应用一致。 |
onSelected | 选择了一个城市后,触发的javascript函数 |
选择城市后,插件回调Javascript函数:
14.2.4.3 注意事项和FAQ
l 如何实现ExMobi页面至插件的属性设置?
两种方式:
1:插件控件构建时通过属性设置;
如:<nativecomponent id="mysms" type="SmsComponent" attachSignature="true" signature="Fiberhome" />
2:JS获取插件控件对象设置;
如:
//获取插件对象
var mysms = document.getElementById("mysms");
//设置signature属性
mysms.set("signature", "StarrySky");
l 如何实现ExMobi页面至插件的函数调用?
页面中JS获取Native控件对象,通过控件对象call方法调用插件提供函数方法名即可,支持字符串类型多参数传递,即call方法第一个参数为需调用插件方法,后面参数为方法入参。
如:
ExMobi页面:
<nativecomponent id="myhttp" type="MyHttpComponent" onSent="onHttpSent"/>
//获取插件对应的控件对象
var myHttp = document.getElementById("myhttp");
//调用插件sendHttp方法,参数为"send http request"
myHttp.call("sendHttp","send http request");
Native插件代码:
@Override
public String call(String functionName, String param1, String param2,
String param3, String param4, String param5, String param6,
String param7) {
if ("sendHttp".equals(functionName)){
String content = param1;
this.sendHttp(content);
return null;
}else{
return null;
}
}
l 如何实现插件至ExMobi页面的函数调用?
ExMobi页面通过插件控件属性设置回调函数名,JS中设置回调函数处理。Native插件类通过MyHttpComponent.this.helper_callJsScript方法调用定义控件属性定义回调函数名即可,支持字符串类型多参数传递。
如:
ExMobi页面:
<nativecomponent id="myhttp" type="MyHttpComponent" onSent="onHttpSent"/>
function onHttpSent(rsp){
var myHttpRsp = document.getElementById("rspMessage");
myHttpRsp.innerHTML = rsp;
}
Native插件代码:
//发送完毕需回调ExMobi页面
if (onSent_ != null && onSent_.length() > 0){
String script = String.format("%s( 'http 请求发送成功,回应数据:%s' )" , onSent_, strRsp);
MyHttpComponent.this.helper_callJsScript(script);
}
l 如何获取插件控件表单提交值
通过Component 基类的public void helper_getValue (String value)方法来调用。
如: public void getNativeValue(String value)
{
String value = super.helper_getValue();
}
l 如何设置插件控件表单提交值
通过Component 基类的public void helper_setValue(String value)方法来调用。
如: public void setNativeValue (String value){
super.helper_setValue(value);
}
14.3 IOS插件开发与使用
14.3.1 开发
14.3.1.1 工程创建
开发Native插件的步骤如下:
1) 从烽火星空获取iOS 工程AppPlugin.zip,以这个工程为基础,参考其中的示例进行开发
可以看到项目内文件结构如下图所示:
其中,XKPlugin_Component.h,XKPlugin_ComponentFactory.h为默认类,请勿修改。XKPlugin_Test2_ComponentFactory.h继承于XKPlugin_ComponentFactory.h,命名规则建议为XKPlugin_+插件工厂类名(唯一标识)+_ComponentFactory,该类对外仅需提供createComponent方法,如下图所示:
2)修改XKPlugin_Test2_ComponentFactory.mm文件createComponent方法,在其中注册开发的Native组件,如下图所示,示例插件工程注册了ABC_RefreshListComponent(下拉刷新列表插件),ABC_AnimationComponent(滑动容器插件),ABC_SliderMenuComponent(3D菜单插件),SmsComponent(模拟短信发送插件),CityComponent(类通讯录列表插件),MyHttpComponent(第三方http直连插件)。
3)开发继承于XKPlugin_Component的组件XXXComponent,实现下列方法
各方法说明如下:
方法 | 说明 |
initComponent | 初始化相关资源 |
releaseComponent | 释放相关资源 |
getView | 返回用于显示的UIView 对于不可见组件,返回nil |
setViewSize | 告诉组件占用的空间大小,一般不用处理 对于不可见组件,此方法不会被调用 |
set | 设置组件属性值,此方法会在2处被调用: 2) 页面中组件xml节点中定义的属性,会通过此方法设置给组件 3) 页面中Javascript调用组件的set方法 (Javascript中int会被自动转换为String类型传递给组件) |
addChildElement | 增加子节点数据,此方法会在1处被调用: 1)页面中组件xml节点定义了子节点,页面会调用addChildElement传递节点数据给组件,根据子节点数量调用多次 |
get | 获取组件的属性值,此方法会在1处被调用: 1)页面中Javascript调用组件的get方法 |
call | 调用组件的方法,此方法会在1处被调用: 1)页面中Javascript调用组件的call方法 |
下图以CityComponent描述了组件的初始化过程:
4)在iOS Device模式下编译工程,会生成的libAppPlugin.a静态库文件,如下图所示。
需选择IOS Device
Xcode 顶部工具栏,Product->Archive,点击开始build生成.a包。
Build成功后,可以看到.a库已生成,xcode 4.x在Organizer视图中可以看到生成的 .a库,xcode 5.x有些时候会无法跳转到Organizer视图,我们可以从下图所示位置看到生成 .a库路径
输入路径,在文件管理器可看到生成的.a包
14.3.1.2 示例1:SmsComponent的实现
#import "ABC_SmsComponent.h"
@implementation ABC_SmsComponent{ NSString* phonenumber; BOOL attachSignature; NSString* signature; long sentMsgs; NSString* onSent; }
-(void)dealloc{ NSLog(@"ABC_SmsComponent dealloc"); }
-(void) initComponent{ phonenumber = @"135000000001"; }
-(void) releaseComponent{
}
-(UIView*) getView{ return nil; }
-(void) setViewSize:(int)width height:(int) h{
}
-(void) set:(NSString*) name value:(NSString*)value{ if ([@"attachSignature" isEqualToString:name]){ attachSignature = [@"true" isEqualToString:value];
}else if ([@"signature" isEqualToString:name]){ signature = [NSString stringWithString:value];
}else if ([@"onSent" isEqualToString:name]){ onSent = [NSString stringWithString:value]; } }
-(NSString*) get:(NSString*) name{ if ([@"phonenumber" isEqualToString:name]){ return phonenumber;
}else if ([@"attachSignature" isEqualToString:name]){ return [NSString stringWithUTF8String: attachSignature ? "true" : "false"];
}else if ([@"signature" isEqualToString:name]){ return signature; } return nil; }
-(void) addChildElement: (NSString*)tag attributes:(NSDictionary*)attributes{
}
-(NSString*) call:(NSString*)functionName par1:(NSString*)param1 par2:(NSString*)param2 par3:(NSString*)param3 par4:(NSString*)param4 par5:(NSString*)param5 par6:(NSString*)param6 par7:(NSString*)param7{ if ([@"sendSms" isEqualToString: functionName]){ // comp.call("sendSms", "今天下午10点开会", 10); NSString* content = param1; NSString* par2 = param2; int delaySeconds = [par2 intValue]; [self sendSms:content delay:delaySeconds]; return nil;
}else if ([@"getSent" isEqualToString:functionName]){ return [NSString stringWithFormat:@"%ld", sentMsgs];
}else{ NSLog(@"SmsComponent ERROR: unsupported function call : %@" , functionName); return nil; } }
-(void) sendSms:(NSString*)content delay:(int)delaySeconds{ NSString* smsText = content; if (attachSignature){ smsText = [smsText stringByAppendingString:signature]; } smsText = [smsText stringByAppendingFormat:@" [发送自%@]", phonenumber];
NSLog(@"SmsComponent WILLL send msg %@ after %d seconds", smsText, delaySeconds); [NSTimer scheduledTimerWithTimeInterval: delaySeconds target: self selector: @selector(onTimer:) userInfo: smsText repeats: NO];
}
-(void)onTimer:(NSTimer*)timer{ NSString* smsText = timer.userInfo; NSLog(@"SmsComponent send msg %@", smsText); UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"Sms" message:smsText delegate:nil cancelButtonTitle:@"Close" otherButtonTitles: nil]; [alert show];
sentMsgs++;
if (onSent !=nil && onSent.length > 0){ NSString* script = [NSString stringWithFormat:@"%@( '%@' )" , onSent, smsText ]; [super helper_callJsScript:script]; }
}
@end
|
其中几个重要的点解释如下:
>> [super helper_callJsScript:script]
可以通过调用Component的helper_callJsScript来调用页面中的脚本函数。
>> else if ([@"onSent" isEqualToString:name]){ onSent = [NSString stringWithString:value]; }
onSent作为事件属性,和普通的属性没有区别。
组件事件触发时,通过onSent保存的函数名称,拼装js脚本,进行调用。
:
14.3.1.3 示例2:CityComponent的实现
#import "ABC_CityComponent.h" #import "ABC_IndexedTableviewController.h" #import "ABC_Content.h"
@implementation ABC_CityComponent{ NSString* icon; NSMutableArray* cities; /* NSArray<ABC_Content> */ NSString* onSelected; ABC_IndexedTableviewController* indexedTable; }
-(void)dealloc{ NSLog(@"ABC_CityComponent dealloc"); }
-(void) initComponent{
}
-(void) releaseComponent{
}
-(void) set:(NSString*) name value:(NSString*)value{ if ([@"icon" isEqualToString: name]){ icon = [NSString stringWithString:value];
}else if ([@"onSelected" isEqualToString: name]){ onSelected = [NSString stringWithString:value];
} }
-(NSString*) get:(NSString*) name{ if ([@"icon" isEqualToString: name]){ return icon; } return nil; }
-(UIView*) getView{ if (indexedTable == nil){ ABC_IndexedTableviewController* controller = [[ABC_IndexedTableviewController alloc] initWithStyle:UITableViewStylePlain]; [controller setContents:cities]; controller.icon = [super helper_getImage:icon]; controller.delegate = self; indexedTable = controller; }
return indexedTable.view; }
-(void) setViewSize:(int)width height:(int) h{
}
-(void) addChildElement: (NSString*)tag attributes:(NSDictionary*)attributes{ if (cities == nil){ cities = [[NSMutableArray alloc] init]; } ABC_Content* content = [[ABC_Content alloc] init]; content.title = [attributes objectForKey:@"name"]; content.code = [attributes objectForKey:@"code"]; content.indexCode = [content.code uppercaseString]; //NSLog(@"add City %@, %@", content.code, content.title); [cities addObject:content];
}
-(void)onRowSelected:(ABC_Content*)row{ if (onSelected != nil){ NSString* script = [NSString stringWithFormat:@"%@ ('%@', '%@');", onSelected, row.code, row.title]; [super helper_callJsScript:script]; } }
-(NSString*) call:(NSString*)functionName par1:(NSString*)param1 par2:(NSString*)param2 par3:(NSString*)param3 par4:(NSString*)param4 par5:(NSString*)param5 par6:(NSString*)param6 par7:(NSString*)param7{ return nil; }
@end
|
其中需要注意的代码:
>> controller.icon = [super helper_getImage:icon];
可以通过调用helper_getImage得到图像内容,图像必须使用ExMobi应用中的图像文件,格式为res:/image/XXX.png
14.3.1.4 注意事项和FAQ
l 插件工程设置采用ARC or MRC
都可以,示例工程提供ARC版 和 MRC版,若设备采用IOS 5.0,建议采用ARC方式。
l 类名的冲突
为了避免在打包编译时,插件和ExMobi以及其它lib库的名字冲突,插件中的所有类名(包括插件类及其他辅助类),均应该使用特有的前缀,如:
l 能否在工程中引入静态Lib库
可以,若引入第三方.a库或者第三方framework库,提交时需要一起提交打包,还需要注意的是,插件使用的第三方库若与ExMobi使用的第三方库一样,则无须提交,打包后插件仍可正常使用。
l 能否使用插件工程中的图片文件
可以,插件工程中如需图片,按照普通UIImage读取方式使用即可,需要注意图片文件命名必须使用特殊前缀以保证图片名称唯一,如:
if(icon == nil){
//直接使用工程中图片
controller.icon = [UIImage imageNamed:@"ABC_city.png"];
}
l 插件构建时能否使用xib文件
可以,提交时需要一起提交打包,需要注意xib文件命名必须使用特殊前缀以保证图片名称唯一。
l 插件基类提供了哪些辅助方法?
l 如何获取ExMobi应用下的图片
通过XKPlugin_Component 基类的-(UIImage*) helper_getImage:(NSString*) uri方法来获取图片,注意仅支持res:前缀的应用内图片。
如:UIImage* arrow = [super helper_getImage:@"res:/image/arrow.png"];
controller.arrowImage = arrow;
l 如何调用ExMobi页面中的脚本
通过XKPlugin_Component 基类的- (BOOL) helper_callJsScript:(NSString*) script 方法来调用。
如: NSString* script = [NSString stringWithFormat:@"%@('%d','%@','%@');",onSelected_,index,
row.code,row.name];
[super helper_callJsScript:script];
l 如何获取插件控件表单提交值
通过XKPlugin_Component 基类的-(void) setNativeValue:(NSString *)value方法来调用。
如:-(void) setNativeValue:(NSString *)value
{
[super helper_setValue:value];
}
l 如何设置插件控件表单提交值
通过XKPlugin_Component 基类的-(NSString*) helper_getValue; 方法来调用。
如: NSString* script = NSString* script = [super helper_getValue];
l 一个插件工程能否编写多个插件类
可以,按照工程创建说明构建多个插件类即可,如下图所示,该插件工程包含了
RefreshListComponent(下拉刷新插件),SliderMenuComponent(3D动态菜单),AnimationComponent(滑动容器插件)等多个插件,只需导出一个.a包即可。
l 生成静态库plugin.a的类型
plugin.a必须是iOS Device版本,而不是iOS Simulator版本。
执行lipo –info plugin.a可以看到architecture为arm类型,而不是i386类型。
l 能否通过直接build方式生成.a包,如下图所示
可以,但不推荐。建议用Product->Archive生成,这样生成的 .a包体积更小。
l 如何测试
由于插件需要在EDN打包后才能实际使用,为了更好的开发测试,AppPlugin工程内自带了一个测试工程TestAppPlugin,通过参考SmsComponent、CityComponent插件的测试代码,可以很容易针对自己的组件编写测试用例并进行测试,详见自测章节描述。
14.3.2 自测
14.3.2.1 测试类构建
自测Native插件步骤如下:
1) TestAppPlugin工程,文件结构如下,common目录下包含XKPlugin_Component.h, XKPlugin_Component.mm, XKPlugin_ComponentDelegate.h, XKPlugin_ComponentFactory.h, XKPlugin_ComponentFactory.mm,XKPlugin_XMLReader.h, XKPlugin_XMLReader.mm, XKPluginManager_ComponentFactory.h, XKPluginManager_ComponentFactory.mm 九个文件,分别说明如下:
XKPlugin_Component.h:插件基类头文件,无须修改
XKPlugin_Component.mm:插件基类实现文件,无须修改
XKPlugin_ComponentDelegate.h:回调函数代理类,无须修改
XKPlugin_ComponentFactory.h:插件工厂基类头文件,无须修改
XKPlugin_ComponentFactory.mm:插件工厂基类实现文件,无须修改
XKPlugin_XMLReader.h:XML读取辅助类头文件,无须修改
XKPlugin_XMLReader.mm:XML读取辅助类实现文件,无须修改
XKPluginManager_ComponentFactory.h:插件工厂管理类头文件,无须修改
XKPluginManager_ComponentFactory.mm:插件工厂管理类实现文件,该类需要修改createFactory方法,增加对测试插件工厂类的创建,如下图所示:
2) 拷贝插件工程中创建的插件工厂类XKPlugin_Test2_ComponentFactory.h头文件到测试工程中。
3) 拷贝插件工程所需图片至测试工程中。
4) 在插件工程中创建测试窗口类ABCViewController,ABC_TestSmsComponentViewController, ABC_TestCityComponentController通过这几个窗口类测试插件view的展示和具体操作,比如插件数据注入,JS方法调用,获取应用的图片,加载工程图片等
具体结构如下:
3)测试获取应用图片:
在IOS设备的TestAppPlugin程序document目录中创建应用资源目录(Exmobi/apps/epower@fiberhome/image),其中Exmobi/apps名称固定,epower@fiberhome为设置的应用id,如图:
调用插件时设置appID为epower@fiberhome,设置工程目录名称为Exmobi,图片路径为:res:/image/xxx
14.3.2.2 示例:CityComponent的测试用例
ABC_TestCityComponentController.mm的代码如下:
// // ABC_TestCityComponentController.m // TestAppPlugin // // Created by fiberhome on 13-8-9. // Copyright (c) 2013年 fiberhome. All rights reserved. //
#import "ABC_TestCityComponentController.h" #import "XKPluginManager_ComponentFactory.h" #import "XKPlugin_ComponentFactory.h" #import "XKPlugin_XMLReader.h"
@interface ABC_TestCityComponentController (){ XKPlugin_Component* component; }
@end
@implementation ABC_TestCityComponentController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { // Custom initialization } return self; }
- (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@"CityComponent"]; //构建factoryManager管理类 XKPluginManager_ComponentFactory* factoryManager = [[XKPluginManager_ComponentFactory alloc]init]; //根据factoryname创建插件工厂类 XKPlugin_ComponentFactory* factory = [factoryManager createFactory:@"XKPlugin_Test2_ComponentFactory"]; //根据插件名创建插件 component = [factory createComponent:@"CityComponent"]; /* <nativecomponent type="CityComponent" id="city" icon="res:/image/native/city.png" onSelected="onCitySelected"> <city name="北京" code="BEIJING" /> <city name="上海" code="SHANGHAI" /> ... </nativecomponent>
*/ //插件为空则提示并返回 if (component == nil) { UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"Test" message:@"Load SmsComponent fail" delegate:nilcancelButtonTitle:@"close" otherButtonTitles:nil]; [alert show]; return; } //传递self到插件中用于接收js回调事件 void* container = (__bridge void*)self; //初始化插件 传入appid [component _sys_initComponent:@"epowernew@fiberhome" container:container];
//加载子节点 { //模拟文件中xml定义数据,加载子字段 NSString* filePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"city.xml"];
NSData* cityData = [NSData dataWithContentsOfFile:filePath]; [component loadXml:[[NSString alloc] initWithData:cityData encoding:NSUTF8StringEncoding]]; }
//设置插件普通属性及事件属性 可用于测试js属性设置 [component set:@"icon" value:@"res:/image/city.png"]; [component set:@"onSelected" value:@"onCitySelected"];
//初始化插件 [component initComponent];
//获取插件view对象 UIView* compView = [component getView];
//设置控件显示区域 if (compView != nil){ CGRect bound = self.container_.bounds;
[self.container_ addSubview:compView]; compView.frame = bound; compView.hidden = NO; //设置插件显示宽,高 [component setViewSize:bound.size.width height:bound.size.height]; }
}
- (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning];
}
- (BOOL) callJsScript:(NSString*) script{ //插件回调函数 将被传递至此 用于测试回调方法 NSLog(@"SCRIPT called: %@", script); UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"Script called From CityComponent" message:script delegate:nil cancelButtonTitle:@"close" otherButtonTitles: nil]; [alert show]; return YES; }
- (void)viewDidUnload { [self setContainer_:nil]; [super viewDidUnload]; } - (IBAction)onTestAdd:(id)sender { //通过插件call 方法调用插件方法 首参数为调用方法名,后续参数为方法入参 [component call:@"addCity" par1:@"阿拉姆" par2:@"alamu" par3:nil par4:nil par5:nil par6:nil par7:nil]; }
- (IBAction)onTestGet:(id)sender{ //获取插件普通属性及事件属性设置值 可用于测试js属性获取 NSString* icon = [component get:@"icon"]; NSString* onSelected = [component get:@"onSelected"]; UIAlertView* alert = [[UIAlertView alloc] initWithTitle: @"Script get From CityComponent" message:[NSString stringWithFormat:@"icon = %@ onSelected = %@" , icon, onSelected] delegate:nil cancelButtonTitle:@"close" otherButtonTitles: nil]; [alert show]; } @end
|
14.3.2.3 注意事项和FAQ
l 如何模拟控件xml初始载入
调用XKPlugin_Component类的loadxml()方法,可以模拟ExMobi页面控件的加载,如:
l 控件的属性测试
调用XKPlugin_Component类的set()和get()方法,可以测试插件属性设置是否成功
如:
l 控件回调的JS方法测试
测试插件的ViewControler需要实现XKPlugin_ComponentDelegate协议,同时需要设置_sys_initComponent函数关联js回调代理
如:
l Exmobi调插件方法测试
调用XKPlugin_Component类的call()方法
如:
14.3.3 打包
14.3.3.1 打包步骤
1)切换到文件管理器,获取该插件工程调用的插件工厂类 ,如XKPlugin_Test2_ComponentFactory.h; 获取生成的.a库,如plugin.a; 建立image文件夹,将插件工程所需的所有图片均拷贝至image文件夹中;建立framework文件夹,将插件工程所需的第三方.a库/framework库拷贝至该文件夹;建立xib文件夹,将插件工程所需的xib文件拷贝至改文件夹;建立other目录,将其他类型文件(如xml,plist等)拷贝至该目录,这六项选中生成插件zip压缩文件。
2)打开EDN打包页面,上传插件zip包,选择所需基准版本打包,打包完毕后下载即可得到包含插件功能的Ipa安装包。
14.3.3.2 注意事项和FAQ
l 上传包包含哪些数据?
包含:
1:插件工程中的工厂类头文件(必选);
2:插件.a包(必选);
3:插件工程图片的image文件夹(可选);
4:包含第三方静态库的framework文件夹(可选);
5:包含其他文件的other文件夹(可选);
l 上传包格式可以为rar吗
不能,必须为zip格式。
l 安装包集成时能否加载多个插件zip包
可以,建多个插件工程,构建native插件完毕后,导出插件.a包,压缩为zip包提交EDN打包即可。
l 常见打包错误说明
14.3.4 使用
参见1.2.4 Android插件开发与使用--->使用章节。
Ios插件使用需要注意,由于系统差异导致插件实现机制不同,nativecomponent使用时属性需要增加插件库factoryname设置,如对来源于XKPlugin_Test2_ComponentFactory控件工厂类的SmsComponent插件调用。
Android
<nativecomponent id="mysms" type="SmsComponent" attachSignature="true" signature="Fiberhome" />
Ios
<nativecomponent id="mysms" type="SmsComponent" factoryname="SmsComponent" attachSignature="true" signature="Fiberhome" />
若Android,Ios同时实现SmsComponent插件,factoryname 属性ExMobi Android客户端加载时会忽略,不会影响插件使用。
15 Native插件开发实战
15.1 Android插件实例开发与使用
15.1.1 开发
15.1.1.1 工程创建
前面我们大概了解android开发的整个过程和一些插件类的说明,下面我们来完整的开发一个插件,从而来理解上面说介绍的内容。
首先我们需要有一个已经开发完毕的android小程序,比如:日历、时间日期控件、画廊等一些功能比较单一的小程序,注意这些android程序最好只有一个Activity, 这样可以方便把这个Activity套装插件的view下面,因为最后打包是不需要AndroidManifest.xml 文件的,如果里面定义了多个Activity,那么就修改成插件就非常困难。
这里,我先从网上随便下载一个android控件小程序,下载完后导入到我们的工程目录,我们先运行下它,证明这个代码本身是没有问题的,可以看出下图是一个日期时间的一个android小控件,可以通过这个控件,进行快速的选择日期和时间。
15.1.1.2 设计插件
现在,我们已经准备好了一个android小程序,接下来我们要开始设计这个插件。现在我为了保留原来的程序做对比,我把这个工程项目copy一份出来,我们在这个DatePickerCopy 这个项目上来进行修改。
首先我们要设计Native插件,其实就是一个xml文件,把这个文件放入assets目录中(这里只是为了方便后面做测试用,在打包的时候,其实是没有这个文件的,这段代码应该在客户端的xxx.xhtml页面中)。
用<nativecomponent id="deskTop" type=”datePicker” …./>来嵌入到页面中。
对与这个控件标签前面的章节已经介绍过,这里不再做详细的介绍,接下来我们来对这个控件定义方法,我们希望点击确定,然后把当前选中的时间返还给exmobi客户端页面里面的某一个控件上面并且显示,那么我们需要定义一个“点击”的方法,于是我们可以这样定义:
<nativecomponent
id="deskTop" style=””
type="datePicker" onClick="getDateTime" >
</nativecomponent>
定义一个onClick名称,方法名为getDataTime ,这里的定义的getDataTime是告诉android程序exmobi客户端有个getDataTime的方法需要调用,这个getDataTime会在xhtml中的js进行定义的,目前我们没有exmobi工程,在测试的时候如果在android里面把方法名称和参数值打印出来,就说明测试成功了。
需要注意的是,这里定义的方法,都是从android插件里面调用exmobi中的js里面的写法,如果从exmobi中的js调android插件里面的方法,我们需要用到call,这个我们在后面会讲到。
因为我们这个插件很简单,所以我这里就定义一个方法,除了id,type,type属性是固定的,后面的方法属性根据自己的实际需求随便定义,这里定义一个onClick=” getDateTime” 那么传到android里面以后key 就是onClick,value就是getDateTime, 在android里面通过获取onClick属性来得到其value值,然后这个value就是客户端js的方法名,来调用客户端的js。
15.1.1.3 给资源名和资源文件添加前缀,并且修改包名
因为我们的插件最终会和exmobi项目一起打包或者不止一个插件打包,最终形成一个项目工程,那么工程里面的资源文件可能就会出现同名的情况,为了确保我这个插件和其他插件的资源不会有同名的情况,那么我需要给我的资源文件加上唯一的标示。其中需要修改的资源包括drawable、layout里面所有图片的名字和所有xml的名字,还有string.xml里面的<string 的name名都要改掉。(如果资源比较多,这可能是个繁琐的过程,后面我们可能会提供脚本来修改)
修改前的工程文件 修改后的工程文件
然后我们还需要修改包名,还记我们前面说的吗,要把所有包名加上com.appplugin,形式为com.appplugin +.插件名的package包,这里我们定义包名为com.appplugin.datePicker,注意:后面我会建一个datePicker类来继承插件的Component抽象类的方法,所以我自己多加一个datePicker的包名(注意:这里的datePicker 一定要和之前定义插件的type是一样的)。
我们修改完文件名和包名后,这个时候程序可能会出现红色的错误,我需要逐一的去修改错误,因为资源变了,包名也变了,程序里面引用资源的地方可能会出错。修改完后,我们来运行下程序,看看是否修改成功。
然后所有的布局文件里面的id也都要修改掉
15.1.1.4 导入插件需要的类文件
下面我们把stub包 包含Component.java,ComponentContext.java, ResManager.java的三个文件全拷过来,注意不要修改里面的任何内容。
接下来,我们在把示例中的ComponentFactory.java 文件拷过来考到com.applugin.datePicker包中,不过这里需要里面的内容。修改这个type的地方,这个值就是我们自己定义的插件名称,如果有多个插件,这里就需要用if来判断,下面我们就需要new 一个插件,这里我还没有创建这个插件类,所以目前报错。
15.1.1.5 创建一个插件类,并且继承Component类
在插件类里面,我们要对插件做一系列的初始化,并且我们和exmobi客户端之间的交互方法也都在这里定义。
在dataPicker这个类里面会实现Component类里面所有的抽象方法,每个方法具体什么意思,前面都用说明,这里不再详细讲解。
接下来,我们要来定义一些插件传过来的一些信息,我们之前给插件定义了一个点击事件的方法,这里我们要在插件里面通过set和get方法实现它,通过get方法我们就可以得到插件控件上面定义的getDateTime 方法名了,到时我们在android里面就可以调用客户端js里面的getDateTime方法了,并且可以传参数给客户端。
private String onClick;
public void set(String name, String value) {
// TODO Auto-generated method stub
if (name.equals("onClick"))
{
onClick = value;
}
}
public String get(String name) {
// TODO Auto-generated method stub
if (name.equals("onClick"))
{
return onClick;
}
return "";
}
然后,就是比较重要的一块了,要实现getView() 这个方法,这个方法是用来得到view,并且把原来的view放在插件工程提供的activity里面。要想得到这个view, 我们需要把原来程序里面创建view的代码修改下(原来创建activity的页面是MainActivity.java)。这里为了方便处理,我在新建一个类用来修改以前的代码,MainActivity.java这个文件,到时候留给我们测试用。这里我就新建一个名字为datePickerRelayout.java 的类,这个类用来继承一个相对布局RelativeLayout。
public class dataPickerRelayout extends RelativeLayout {
public Context context_;
private datePicker mCom;
public dataPickerRelayout(Context context) {
super(context);
context_ = context;
}
}
这里我们定义了两个属性一个Context 和 datePicker 类的对象,其中Context主要是用来传递Activity的上下文,datePicker 对象,方便我们可以调用datePicker 类中的一些方法。
接着我们在定义一个方法,用来加载原来的view并执行一些操作,那么我们就定义一个名字为initDatePickerRelayout的方法。然后,我们把MainActivity.java里面的除了创建view以外的其他代码全部复制到这方法里面来,修改后代码如下:
原来MainActivity.java中的代码
复制后并修改后的代码
我们可以看到,上图红框中的代码,其实就是用来得到view的,其他的代码目前先保存这样,后面我们还要继续修改。
15.1.1.6 完善datePicker类中的getView方法
现在我们在回到datePicker.java 里面来,我们要在getView方法中把插件的context上下文传给datePickerRelayout类里面,并且只需里面的方法,让整个程序可以完整的运行起来。
到目前为止我们的插件在不和客户端交互的基础上基本上修改完成了,但是还不能测试运行,接下来我们需要修改MainActivity.java文件,把这个文件当成测试文件,在修改之前我们需要额外导入插件的一个jar包。我们从插件示例里面把这个native-common.jar包复制到我这个工程的libs目录下,然后把示例中的测试页面中带代码拷贝到我们这个项目的MainActivity.java里面来。
这里需要注意修改comType的值,修改成我们定义的插件名称。
接下来我们来运行下这个程序,看看我们有没有修改成功。
耶,成功了!!!
接下来,我们就需要根据我们最开始定义的插件来修改里面的方法了,还记得我们想让这个插件干什么吗,我们想点击确定,把时间传给exmobi客户端,我们还希望在exmobi里面设置一个时间来改变这个插件显示的时间。
下一节我们来讲这个要怎么做。
15.1.2 修改插件方法并自测
15.1.2.1 调客户端定义的js,并传值
我们在开始定义插件的时候定义了一个getDateTime ,这个是要告诉android插件,exmobi页面中有个方法名为getDateTime的方法需要调用。
我们希望点击“确定”然后调用exmobi里面的js,所以我们来看下android代码这个“确定”按钮到底在什么地方,通过查找发现PowerHallStrArrayDialog.java文件里面
下面我们需要在这个地方来调用客户端的方法,那么首先我们在datePicker.java里面定义一个getDateTime方法,为什么要这这个类里面定义呢,因为这个类是专门用来和exmobi交互信息用的。
其实这段代码最关键的地方就是super.helper_callJsScript(formScript); 通过这个方法可以调用客户端的js。
好了,既然我们在这里定义了getDateTime, 那么接下来我们需要在点击“确定”的地方来调用它,那么我们需要把这个datePicker类的对象,传给PowerHallStrArrayDialog.getDateDialog()方法,那么我们来修改下这个方法的参数。
原来的代码PowerHallStrArrayDialog.getDateDialog(context_).show();
修改后的代码
这样我们就把datePicker类的对象mCom传进去了。
我们在“确定”的点击事件里面,通过mCom来调getDateTime()方法。
写好后,我们来运行下,因为先还没有和exmobi一起打包,所以我们只能通过日志的方式来调试。
这个时候我们可以在日志里面看到,我们这个插件要准备调用exmobi页面中js的方法了。
15.1.2.2 从客户端页面的js向插件传值
我们插件类里面提供了一个方法call,这个方法可以在客户端的js里面来调用插件里面的方法。
由于我们现在还是没有exmobi工程,所以还是只能通过测试文件来测试。首先我们在datePicker.java里面来完成call方法,这个方法一共有7个参数,一般情况我们不会用那么多,这里我只用两个参数就可以了,一个方法名,一个时间值。
在定义调用call方法之前,我们先定义一个可以修改时间的方法。我们直接在PowerHallStrArrayDialog.java里面建一个changeDate方法。
然后我们在datePickerRelayout.java里面定义一个方法来调用changeDate.
接着,我们在datePicker.java里面完善call方法,来调用setDate()
这里我们需要在客户端的js里面传一个方法名过来,通过判断方法名来调用不同android里面不同的方法,这里我们只有一个方法,其实写不写都可以。
好了,接下来我们开始调用call方法了,由于目前还是没有exmobi工程,所以我们还是一样在测试文件里面模拟调用call, 下面我们打开MainActivity.java。
最后,我们来运行下,看下效果
到这里,我们的插件基本上算是全部开放完毕了。后面我就可以开始打包上传了,不过还有个地方我们忘记修改了,就是在我们android里面所有调用资源文件的id的代码都需呀修改,主要是为了避免打包后受到exmobi的R.java文件的影响,这个过程可能也是比较繁琐的过程,以后可能会提供脚本替换。
比如页面中有R.drawable.datepicker_wheel_bg 的地方全部改成 ResManager.getInstance().getResourcesIdentifier("R.drawable.datepicker_wheel_bg")
15.1.3 插件打包
前面我们已经基本开发完插件,下面我们需要将插件打包,并且和exmobi集成到一起。
首先我们把插件用到的代码文件导出一个jar包。
导出jar包的时候需要注意,这里只要src里面的文件,其他的都不要勾选,而且src里面也不要勾选那个测试用的文件了。
导出jar包后,接下来我们需要把这个jar包和我们用的的一些资源一起打成zip压缩包,前面对应这部分已经介绍了,不清楚的在回到1.2.3打包章节。
最后我们把这个zip包,在EDN门户上进行上传和exmobi集成到一起,然后我们把这个集成到一起的这个exmobi下载下来,进行安装,这个时候整个插件开发打包就算完成了。下面我来介绍下如何在EDN上上传插件。
首先登陆EDN门户http://www.exmobi.cn/, 然后在导航栏上可以看到有一个“插件管理”的模块。
点进去,出现一个插件列表,一开始里面肯定是空的,我们点击右上角的“申请发布插件”进入一个表单填写的页面。
红色区域是必填信息。
插件名称: 这里自己定义,主要能看出插件的功能就可以了
系统平台:目前暂时只支持android和ios两个系统的插件集成
重力感应:主要是选择横竖屏查看模式
插件: 这里就是我们之前打包好的插件zip格式的文件
封面图片:上传一张可以提现插件样子的图片当做商城展示封面,最好是Png格式。
效果图:这里给出了3张效果图,开发人员可以把插件界面和功能通过图片方式展现出来,最好是Png格式。
标签:随便填写
摘要:关于该插件的相关摘要。
所有信息填写完成后,我们点击提交,然后我们在列表里面就可以看到刚才上传的插件了,不过这个时候,插件还没有审核,只有审核通过的插件,才可以进行下一步的集成,那么现在我们唯一能做的就等待,等管理员进行审核。
好了,我们的插件终于审核通过了。
下面我们开始集成打包了,我们进入“项目打包管理”页面,如果是新项目就重新建立项目,如果是老项目,我们在之前的项目上进行打包,关于如何打包这里就不在介绍了,不清楚的同学可以在论坛上搜索“打包”。
这里我为了方便,我就直接选择一个老项目进行打包。
这里需要注意的是,exmobi版本只有从5.0.0开始才支持集成插件,选中后,下面就会出现之前我们发布的插件,让我们选择。我们勾选上,接下来的就和普通打包一样了,上传我们的应用资源进行打包
接下来,等待打包结果
15.1.4 使用插件
打包完成后,接下来我们在我们的应用里面新建一个页面来测试下这个插件,页面的内容如下:
下面我们点击下 这个按钮,把我们的插件显示出来
我们之前给插件里面的“确定”已经定义好了方法,点击“确定”应该会调用我们客户端页面的js里面的getDateTime方法,然后给页面的一个text进行赋值,我们点击“确定”看下效果是不是这样。
效果和我们想象的一样,证明插件掉客户端js方法成功,下面我们在来测试下客户端js调用插件里面的方法,客户端js是通过call方法进行调用的,下面我们设置一个值,然后让插件显示我们设置的值。
OK了,到这里,我们已经成功的为exmobi增加了一个新的控件,让其他同样使用exmobi的小伙们一起惊呆吧。
15.2 iOS插件实例开发与使用
15.2.1 定义xml标签
比如实现如图slider插件如下:
可以根据插件实现功能的定义一些必要的属性和事件,如下:
<nativecomponent id="native" style="width:200;height:300" type="ABC_SliderComponent" factoryname=" XKPlugin_Test2_ComponentFactory" οnchange="changetodo" minvalue =”1” maxvalue=”10”></nativecomponent>
插件名是ABC_SliderComponent
插件工厂为:XKPlugin_Test2_ComponentFactory
回调,在用户拖动控件的slider控件的时候,想要触发js里changetodo方法,支持传参。
注意事项:ios开发,工程内文件都不能同名,所以在命名上要注意唯一性。
15.2.2 制作插件
1) 从EDN门户获取工程
把xml标签文件上传到EDN门户,生成插件工程,解压缩,用xcode工具打开,如下图所示。
可以从EDN门户获取实例工程,参考实例工程进行开发。
2) 新建ABC_SliderViewController,继承UIViewController
在ABC_SliderViewController.h头文件中,申明setRangeMinValue方法(设置slider控件的起始值和最大值)
定义ABC_SliderComponent属性。
ABC_ SliderViewController.h
#import <UIKit/UIKit.h> #import "ABC_SliderComponent.h" @interface ABC_SliderViewController : UIViewController @property int width; @property int height; @property (strong,nonatomic) UISlider* mySlider; @property (assign,nonatomic) ABC_SliderComponent* delegate; -(void)setRangeMinValue:(float)minValue maxValue:(float)maxValue; @end |
ABC_ SliderViewController.m
#import "ABC_SliderViewController.h" @interface ABC_SliderViewController () @end @implementation ABC_SliderViewController -(void) dealloc{ NSLog(@"ABC_SliderViewController dealloc"); } - (void)viewDidLoad { [super viewDidLoad]; //初始化滑片 _mySlider = [[UISlider alloc] initWithFrame:CGRectMake(0, 0, 200, 50)]; //设置拖动滑块图 [_mySlider setThumbImage:[UIImage imageNamed:@"slider_normal.png"] forState:UIControlStateNormal]; [_mySlider setThumbImage:[UIImage imageNamed:@"slider_normal.png"] forState:UIControlStateHighlighted]; //设置valuechange事件是否实时回调 [_mySlider setContinuous:NO]; //结束拖动 [_mySlider addTarget:self action:@selector(endDrag:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchUpOutside]; // 添加 [self.view addSubview:_mySlider]; } //结束拖动 -(void) endDrag:(UISlider*) slider { NSLog(@"slider end drag"); NSString* sliderValue = [NSString stringWithFormat:@"%f",[_mySlider value]]; [_delegate onChange:sliderValue]; }
//设置slider区间 -(void)setRangeMinValue:(float)minValue maxValue:(float)maxValue{ [_mySlider setMinimumValue:minValue]; [_mySlider setMaximumValue:maxValue]; } - (void) viewDidUnload { } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } @end |
Slider的view创建好了,如何在ABC_SliderComponent插件中调用呢?修改ABC_SliderComponent代码
3) 修改ABC_SliderComponent
设置成员变量ABC_SliderViewController;
在getView方法中创建并返回ABC_SliderViewController,注意要设置其delegate为self自身,此处用到了委托模式。
.h头文件中申明onChange方法,外部类ABC_ SliderViewController中才能调用。
ABC_SliderComponent.m
#import "ABC_SliderComponent.h" #import "ABC_SliderViewController.h" @implementation ABC_SliderComponent{ ABC_SliderViewController* sliderView; NSString* minValue; NSString* maxValue; NSString* onChange; } -(void)dealloc{ NSLog(@"ABC_SliderComponent dealloc"); }
-(void) initComponent{ }
-(void) releaseComponent{ } -(void) set:(NSString*) name value:(NSString*)value{ //设置属性值, //在js中可以调用进行赋值 //xml标签中设置的属性,也会执行此函数来赋值。 if ([@"minvalue" isEqualToString:name]) { minValue = [NSString stringWithString:value]; NSLog(@"set: minvalue:%@",minValue); }else if([@"maxvalue" isEqualToString:name]) { maxValue = [NSString stringWithString:value]; NSLog(@"set: maxvalue:%@",maxValue); }else if([@"onchange" isEqualToString:name]) { onChange = [NSString stringWithString:value]; } }
-(NSString*) get:(NSString*) name{ //获得属性值 if ([@"minvalue" isEqualToString: name]){ return minValue; }else if ([@"maxvalue" isEqualToString: name]){ return maxValue; }else if ([@"onchange" isEqualToString: name]){ return onChange; } return nil; }
-(UIView*) getView{ if (sliderView == nil){ //设置slider范围 ABC_SliderViewController* controller = [[ABC_SliderViewController alloc] init]; controller.delegate = self; sliderView = controller;
} return sliderView.view; }
-(void) addChildElement: (NSString*)tag attributes:(NSDictionary*)attributes{ //如果xml标签中含有子节点,则每个子节点都会运行一遍这个方法 }
-(void) onChange:(NSString*) value{ if (onChange != nil){ NSString* script = [NSString stringWithFormat:@"%@('%@');",onChange,value]; [super helper_callJsScript:script]; } }
-(NSString*) call:(NSString*)functionName par1:(NSString*)param1 par2:(NSString*)param2 par3:(NSString*)param3 par4:(NSString*)param4 par5:(NSString*)param5 par6:(NSString*)param6 par7:(NSString*)param7{ //在js中调用给插件传值 if ([@"setRange" isEqualToString: functionName]){ float minV = [param1 floatValue]; float maxV = [param2 floatValue]; [sliderView setRangeMinValue:minV maxValue:maxV]; return nil; }else{ NSLog(@"ABC_SliderComponent ERROR: unsupported function call : %@" , functionName); return nil; } } @end |
15.2.3 测试插件
由于插件需要在EDN打包后才能实际使用,为了更好的开发测试,AppPlugin工程内自带了一个测试工程TestAppPlugin,所以插件制作完,可以先在测试工程中测试,测试通过后,再提交到门户上进行打包。
在ABCViewController中增加一个按钮,给按钮添加事件,点击按钮进入到ABC_TestSliderViewController页面,
新建ABC_TestSliderViewController,继承UIViewController,并且勾选生成.xib文件。
在xib文件中,拖入view控件(x=0,y=60)
选中view控件,右击拖入到ABC_TestSliderViewController.h中定义成属性,命名为controller。
ABC_TestSliderViewController.h需要实现XKPlugin_ComponentDelegate
ABC_TestSliderViewController.h
#import <UIKit/UIKit.h> #import "XKPlugin_ComponentDelegate.h" @interface ABC_TestSliderViewController : UIViewController<XKPlugin_ComponentDelegate> @property (weak, nonatomic) IBOutlet UIView *controller; @end |
ABC_TestSliderViewController.m
#import "ABC_TestSliderViewController.h" #import "XKPlugin_Component.h" #import "XKPluginManager_ComponentFactory.h" @interface ABC_TestSliderViewController () { XKPlugin_Component* component; } @end
@implementation ABC_TestSliderViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { // Custom initialization } return self; }
- (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@"Test ABC_SliderComponent"]; //构建factoryManager管理类 XKPluginManager_ComponentFactory* factoryManager = [[XKPluginManager_ComponentFactory alloc]init]; //根据factoryname创建插件工厂类 XKPlugin_ComponentFactory* factory = [factoryManager createFactory:@"XKPlugin_Test2_ComponentFactory"]; //根据插件名创建插件 component = [factory createComponent:@"ABC_SliderComponent"]; //插件为空则提示并返回 if (component == nil) { UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"Test" message:@"Load ABC_SliderComponent fail" delegate:nil cancelButtonTitle:@"close" otherButtonTitles:nil]; [alert show]; return; } //传递self到插件中用于接收js回调事件 void* container = (__bridge void*)self; [component _sys_initComponent:@"TestApp" container:container];
//设置插件普通属性及事件属性 可用于测试js属性设置 // [component set:@"onchange" value:@"changetodo"]; //初始化插件 [component initComponent];
//获取插件view对象 UIView* compView = [component getView];
//设置控件显示区域 if (compView != nil){ CGRect bound = self.container_.bounds;
[self.container_ addSubview:compView]; compView.frame = bound; compView.hidden = NO; //设置插件显示宽,高 [component setViewSize:bound.size.width height:bound.size.height]; } }
- (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } @end |
最好用真机测试。
15.2.4 打包插件
1) 生成静态库plugin.a文件
选中AppPlugin工程,不是TestAppPlugin工程。
plugin.a必须是iOS Device版本,而不是iOS Simulator版本。
执行lipo –info plugin.a可以看到architecture为arm类型,而不是i386类型。
可以通过直接build方式生成.a包,如下图所示
但不推荐。建议用Product->Archive生成,这样生成的 .a包体积更小。
2) 压缩成zip包
切换到文件管理器,获取该插件工程调用的插件工厂类 XKPlugin_Test2_ComponentFactory.h; 获取生成的.a库plugin.a; (如果插件中需要用到图片,需要建立image文件夹,将插件工程所需的所有图片均拷贝至image文件夹中),压缩成zip格式文件。
打开EDN打包页面,上传插件zip包,选择所需基准版本打包,打包完毕后下载即可得到包含插件功能的Ipa安装包。
第6篇ExMobi综合实例
16 开发模式介绍
开发模式按照数据传输的实时性分为同步模式和异步模式。这两种模式不是对立的,同样的功能可以采用同步模式也可以采用异步模式来实现。
16.1 同步模式
同步模式是指界面展现和数据处理都是在JSP中进行的,也就是说JSP最终下行给客户端的内容即为XHTML。
比如6的示例开发模式就是同步模式。
16.2 异步模式
异步模式是指界面采用静态页面(即XHTML)进行展现,数据内容通过JS进行动态注入。这时候JSP返回的内容一般为JSON、XML或者是HTML片段。
AJAX就是最常见的异步模式。
16.3 异步模式的优势
对于移动应用的开发更推荐的是异步模式,因为这种模式对于移动应用开发有特殊的优势:
1) 通过异步模式,可以达到数据的逐个加载,页面的逐步展现,提升用户体验度。
2) 优化了客户端和服务端之间的传输,减少不必要的数据往返,减少了带宽占用,这一点对于移动终端用户特别重要。
3) 承担了一部分本来由服务端承担的工作,从而减少了大用户量下的服务器负载。
17 ExMobiJS框架介绍
ExMobiJS框架是基于ExMobi支持的JS类库开发出来的一个类似于JQuery的前端开发框架。可以到ExMobi官网下载。
ExMobiJS主要包含几个类库:base.js、utility.js、app.js、tree.js、validate.js、db.js和weibo.js。
使用前需要在页面中导入这些JS,如下所示:
<script src="res:script/exmobijs/base.js"/> <script src="res:script/exmobijs/utility.js"/> <script src="res:script/exmobijs/app.js"/> <script src="res:script/exmobijs/tree.js"/> <script src="res:script/exmobijs/validate.js"/> <script src="res:script/exmobijs/db.js"/> <script src="res:script/exmobijs/weibo.js"/> |
本章将对这些类库的常用方法做简要说明。
17.1 基础类base
base基础类主要提供对DOM对象的封装、String对象的扩展和Array对象的扩展。
17.1.1 DOM对象的封装
ExMobiJS对DOM对象的封装是通过$(id Or name)函数来实现的,该函数只接受一个参数,参数值为某个DOM对象的id或者name。如果存在多个同id(原则上不会有)或者name的控件,返回值为第一个出现的控件对应的DOM对象,如果不存在对应的控件则返回null。
假设页面的body中有个div控件的id为out,如下:
<div id="out">这里是内容</div> |
则可以在JS中通过下面的方法获取div控件的DOM对象,如下:
var divObj = $("out"); |
$对象可以进行操作的方法主要有:
方法 | 说明 | 示例 |
show() | 显示控件 | $("out").show() |
hide() | 隐藏控件 | $("out").hidden() |
toogle() | 控件显隐状态反转 | $("out").toogle() |
html() | 获取innerHTML | $("out").html() |
html(str) | 设置innerHTML | $("out").html("这是新的内容") |
addHtml(str) | 追加innerHTML | $("out").addHtml("追加的内容") |
val() | 获取value属性值 | $("ipt").val() |
val(str) | 设置value属性值 | $("ipt").val("这是新的值") |
addVal() | 追加value属性值 | $("ipt").addVal("追加的值") |
setCache() | 把控件当前value储存到缓存 | $("ipt").setCache() |
getCache() | 获取储存的缓存到控件的value中 | $("ipt").getCache() |
17.1.2 String对象的扩展
String对象的扩展是为了对String对象进行更方便的操作而提供的一些扩展函数。
方法 | 说明 | 示例 |
encode() | 把字符串进行urlencode | var str = "你好"; var strEndoce = str.encode(); |
decode() | 还原被urlencode的字符串 | var str = "%e4%bd%a0%e5%a5%bd"; var strDecode = str.decode(); |
trim() | 去掉字符串前后空格 | var str = " 你好 "; var strTrim = str.trim(); |
htmlEncode () | 将字符串中的XML关键字转义 | var html = "<div>你好</div>"; var htmlEncode = html.htmlEncode(); |
htmlDecode() | 把被转义的XML关键字还原 | var html = "<div>你好</div>"; var htmlDecode = html.htmlDecode(); |
replaceAll(pattern replacement) | 将字符串中符合pattern规则的内容替换成replacement | var str = "1234567"; str = str.replaceAll("2", "0"); |
session (str) | 给字符串增加session记忆功能 | var str = "username"; str.session("admin"); |
session() | 获取字符串记忆的session值 | var str = "username"; var username = str.session(); |
toInt() | 把字符串转成int型 | var str = "123"; var num = str.toInt(); |
toJSON() | 把字符串转成JSON对象 | var str = '{"username", "admin"}'; var json = str.toJSON(); var username = json.username; |
toXML () | 把字符串转成XML的XMLElement对象 | var str = "<catalog><id>1</id><name>目录视图</name></catalog>"; var xml = str.toXML(); var name = xml.getElementsByName("name")[0].text; |
toAscii() | 把字符串转成Ascii码 | var str = "test"; alert(str.toAscii());//返回:\u0074\u0065\u0073\u0074 |
unAscii() | 把Ascii码字符串还原 | var str = "\u0074\u0065\u0073\u0074"; alert(str.unAscii());//返回:test |
17.1.3 Array对象的扩展
Array对象的扩展是为了对Array对象进行更方便的操作而提供的一些扩展函数。
方法 | 说明 | 示例 |
append (obj[,obj1,obj2…]) | 把一个或多个对象加入数组 | var arr = ["1", "2"]; arr.append("3", "4"); |
clear () | 把数组元素清空,及length为0 | var arr = ["1", "2"]; arr.clear(); |
toString() | 把数组元素转成String对象,各元素之间无连接符 | var arr = ["1", "2", "3"]; var str = arr.toString();//返回123 |
toString (str) | 把数组元素转成String对象,各元素之间用str连接 | var arr = ["1", "2", "3"]; var str = arr.toString(",");//返回1,2,3 |
17.2 应用场景类app
app.js是为了满足某种应用场景而封装的一些方法。
17.2.1 ajax请求函数
ajax封装函数$a.go()函数是最长用的函数,它是对AJAX对象的封装,并且对AJAX请求的异常做了统一处理。
该函数支持的参数有:
$a.go(url, method, data, successFuntion, failFunction, requestHeader, isShowProgress, isCache, callbackParams),其中
url
ajax要发送请求的地址
method
ajax提交方式,post|get
data
ajax要发送的数据
successFunction
ajax成功执行后回调函数
failFunction
ajax执行失败后回调函数
requestHeader
ajax发送请求时的请求头信息
isShowProgress
发送ajax请求时是否显示进度条,true:显示;false:不显示
isCache
是否使用缓存,true为使用,false为不使用。如果使用了缓存,会对优先显示该请求的历史响应,并且新的响应存到缓存中,每次显示的内容都是上次的结果,以提高页面响应速度。
callbackParams
在回调函数中可以获取的参数,该参数的格式为json对象,比如{“username”, “admin”}实际上是对ajax.setStringData(key,value),封装,所以可以在回调函数中通过data.getStringData(“username”)获取。
下面是AJAX请求的一个例子:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>ajax封装</title> <script src="res:script/exmobijs/base.js"/> <script src="res:script/exmobijs/utility.js"/> <script src="res:script/exmobijs/app.js"/> <script src="res:script/exmobijs/validate.js"/>
<script> <![CDATA[ //不用传参示例 function doAjax(){ var data = "username="+$("username").val()+"&password="+$("password").val(); $a.go($("form").action+"?ajax", "post", data, onSuccess, null, '{"Content-Type": "application/x-www-form-urlencoded"}', true); }
function onSuccess(data){ var rs = data.responseText.trim().toJSON(); alert(rs.status);//登陆信息 } /
//需要传参示例 function doAjax1(){ var data = "username="+$("username").val()+"&password="+$("password").val(); //注意,要想ajax可以传参,可以增加第9个参数 $a.go(document.getElementById("form").action+"?ajax", "post", data, "onSuccess1", null, '{"Content-Type": "application/x-www-form-urlencoded"}', true, false, {"name": $("username").val()}); }
function onSuccess1(data){ var rs = data.responseText; alert(data.getStringData("name"));//返回用户名信息 } ]]> </script> </head> <body> 通过$a.go封装了ajax对象。其中包含了超时操作,如果请求超时会自动执行超时操作,而不会进入回调函数。并且简化了写法,无需new也不需要send。直接调用go即可,其它参数一致,如doAjax()。 <br/> 需要注意的是如果需要在回调函数里面获取参数,那就需要定义个变量等于go对象,其实go就是一个ajax对象。可以给这个对象传参,如doAjax1()里的示例。 <form id="form" action="http://domain/app/services/webServiceTest" method="post"> <input type="text" name="username" value="admin" prompt="请输入用户名"/> <br/> <input type="password" name="password" value="111" prompt="请输入密码"/> <br/> <input type="button" value="ajax不传参" onclick="doAjax()" style="width:50%"/> <input type="button" value="ajax传参" onclick="doAjax1()" style="width:50%"/> </form> </body> </html> |
17.2.2 其他函数
方法 | 说明 | 示例 |
$a.tel(num) | 提示是否拨打num电话号码 | $a.tel("02566777333"); |
$a.tel(num, name) | 提示是否拨打name的电话num | $a.tel("02566777333", "ExMobi支撑"); |
$a.toast(msg) | 以toast方式提示msg信息 | $a.toast("发送成功!"); |
$a.isNetConnected () | 返回设备的网络连接状态 | if($a.isNetConnected ()){ //网络已连接 }else{ //网络未连接 } |
17.3 工具类utility
工具类提供了一些数据转换的操作。
方法 | 说明 | 示例 |
$u.transObj(obj) | 该方法是把json对象转成json字符串 | var str = '{"username":"admin"}'; var json = str.toJSON(); str = $u.transObj(json); |
$u.cache.set(key, value); | 设置cache缓存,以键值对存储 | $u.cache.set('username','admin'); |
$u.cache.get(key); | 获取某个key对应的cache | $u.cache.get('username'); |
$u.cache.clear(key); | 清除某个key下的cache | $u.cache.clear('username'); |
17.4 动态树类tree
动态树类提供了一种高效的动态构建树结构的方法。
使用动态树类,比如使用$tree(id),其中id为树控件的id。所有的操作都是基于$tree的,不能通过document.getElementById(id)来获取tree对象使用对动态树类tree的所有方法的使用。
17.4.1 树的构建方法load
load方式构建树,是通过一种无序的方式来构建。每一个树节点都是一个json对象,由于是无序的,所以每个json对象都必须包含一个id和pId,指明当前节点的id和父节点的id,以建立树之间的层级结构。而且,树节点的其他属性可以作为json对象的key:value键值对,使得树的构建更灵活。比如:treeitem的checkbox属性、collapsehref属性等都可以作为json对象的键值对。
var tree; function doLoad(){ tree = $tree("tree");//tree控件的id //json格式为对象数组,如:[{"id":"当前节点id","pId":"父节点id","text":"显示的文本","value":"有checkbox时提交的值","href":"点击文本触发的事件","collapsehref":"点击加减号触发的事件"}] //但是对象的属性可以是任意的treeitem的属性,如checkbox、target等 var json = [ {"id":"1","pId":"tree","text":"node1","value":"","href":"","collapsehref":"", "iscollapse":true}, {"id":"3","pId":"2","text":"node3","value":"","href":"alert('i am node3')","collapsehref":""}, {"id":"2","pId":"tree","text":"node2","value":"","href":"","collapsehref":"", "checkbox":"false"}, ]; //可以直接load以json生成树节点,返回的是一个树节点信息的json数组 tree.load(json); } |
17.4.2 获取树节点对象get
可以通过树节点的id获取到当前树节点的对象:
var treeitem = tree.get(id)//返回该树节点的json对象 |
获取到的treeitem对象是一个json格式数据,通过该对象可以获取树节点的id、pId以及其他属性。
比如:
treeitem. checkbox;//返回该树节点的选中状态 |
17.4.3 判断子节点是否存在hasChild
获取到某个树节点对象,可以判断其是否有子节点:
tree.get(id).hasChild()//true为有,false为无 |
17.4.4 清除所有子节点clear
获取到某个树节点对象,可以清除其所有子节点:
tree.get(id). clear()//把某id的树节点的所有子节点移除掉 |
17.4.5 清除某个树节点remove
获取到某个树节点对象,可以将该节点从其父节点中移除:
tree.get(id). remove()//把某id的树节点从其父节点中移除掉 |
17.5 表单校验类validate
表单校验类通过统一的方法对表单进行统一校验,并返回校验结果用于逻辑判断操作。
17.5.1 初始化表单验证类
表单验证类的初始化方法为:
var v = $v.validate(mode) |
其中,默认mode为0时alert提醒,1时toast提示。不填则默认为0。
17.5.2 增加验证项
要增加验证项,首先需要了解一下,目前支持的验证类型,主要包括:
//正则判断 Require:/.+/, //必填项,对radio、checkbox、tree等数组型不生效,可通过可选个数达到必填要求 Email:/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/, //Email地址格式 Phone:/^((\(\d{2,3}\))|(\d{3}\-))?(\(0\d{2,3}\)|0\d{2,3}-)?[1-9]\d{6,7}(\-\d{1,4})?$/, //电话号码格式 Mobile:/^((\(\d{2,3}\))|(\d{3}\-))?1\d{10}$/, //手机号码格式 Url:/^http:\/\/[A-Za-z0-9]+\.[A-Za-z0-9]+[\/=\?%\-&_~`@[\]\':+!]*([^<>\"\"])*$/, //基于HTTP协议的网址格式 Currency:/^\d+(\.\d+)?$/, //货币格式 Number:/^\d+$/, //数字 Zip:/^[1-9]\d{5}$/, //邮政编码 Integer:/^[-\+]?\d+$/, //整数 Double:/^[-\+]?\d+(\.\d+)?$/, //实数 English:/^[A-Za-z]+$/, //英文 Chinese: /^[\u0391-\uFFE5]+$/, //中文 Username:/^[a-z]\w{3,}$/i, //用户名 UnSafe:/^(([A-Z]*|[a-z]*|\d*|[-_\~!@#\$%\^&\*\.\(\)\[\]\{\}<>\?\\\/\'\"]*)|.{0,5})$|\s/, //安全密码 Date:/^(\d{1,4})(-|\/)(\d{1,2})\2(\d{1,2})/, //日期 //函数判断 MaxLength:"$v.isLength(obj.val(), item.ps)", //最大长度 Ip:"$v.isIp(obj.val())", //IP Accept:"$v.isAccept(obj.val(), item.ps)", //设置过滤,用于限制文件上传 Limit:"$v.isLimit(obj.val().length,item.ps)", //限制输入长度 LimitByte:"$v.isLimit($v.lenByte(obj.val()), item.ps)", //限制输入的字节长度 Custom:"$v.exec(obj.val(), item.ps)", //自定义正则表达式验证,选择此项则表单元素的正则表达式在regexp属性里定义 Checked:"$v.isChecked(obj.name, item.ps)", //验证单/多选按钮组 |
增加验证的方法为
add(objId,alertMsg,validateType[,otherParams]) |
其中,objId为被校验控件的id或者name;alertMsg为当校验不通过时的提示语;validateType为验证类型;otherParams为当验证类型为函数型的时候,比如Limit类型的item.ps需要一个参数,就是要限制的长度,则需要添加额外的参数作为添加项。
比如:
var v = $v.validate()//默认alert提示 .add("title","请输入标题","Require") //.add("title","标题至少要大于等于4个字","Limit","4") .add("type","类型必须选择一项","Require") .add("read","可传阅部门至少勾选一项,最多选两项","Checked","1|2") .add("leval","紧急程度必须勾选一项","Checked") .add("file","请选择文件","Require") .add("file","请选择doc或者txt文件","Accept","doc|txt") .add("area","请填写所属区域","Require") .add("public","请填写正确时间","Require") .add("public","请填写发布时间","Date") .add("isPublic","请请选择发布","Checked") .start(); |
17.5.3 验证触发
上一节的示例中用到的start()的执行实际就是验证的触发,该函数触发后会返回校验的结果,true则为校验通过,false则为校验失败。
17.6 SQLite数据库操作类db
由于该类的封装已经作为标准对象统一到ExMobi的DB类(ExMobi中旧的SQLite数据库类为DataBase类)。使用方法基本一致,建议SQLite的操作使用ExMobi的DB类。
其介绍可以参考第4.7.2章节。
17.7 微博调用类weibo
微博调用类支持新浪和腾讯的微博。发送新浪/腾讯微博的前提是必须到新浪/腾讯开发者中心申请开发者账号,申请通过后会分配key、secret和redirecturl几个参数,需要配置到config.xml中,如下:
<tencentweibo key="801292142" secret="f5a0d823227e9561839b37144b481361" redirecturl="http://app.tongbu.com/10000403_exmobihd.html"/> <sinaweibo key="3050909412" secret="cc03a010ca4773c4fbb5d9af01406537" redirecturl="http://weibo.com/u/2159849513"/> |
如果不配置的话默认使用ExMobi内置的key。
17.7.1 初始化weibo
要使用weibo对象需要先进行初始化,方法如下:
$wb.init(type) |
其中type指明微博的类型,当为sina时初始化新浪微博,当为tencent时初始化腾讯微博。
17.7.2 发送微博信息
发送微博信息实际是把微博账号绑定和发送微博信息集成为一个功能。即当调用发送微博信息的时候会先判断是否已经绑定某个微博账号,如果已经绑定则信息直接发送;额如果尚未绑定则先提示要求绑定,绑定成功则继续发送微博,绑定失败则不继续发送。
发送的方法为:
$wb.init(type).go(content, path); |
其中content为发送的微博的文本信息内容,path为发送的图片地址。content是必填项,path为选填项。
比如:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <html> <head> <meta charset="UTF-8"/> <title>发送微博</title> <script src="res:script/exmobijs/base.js"/> <script src="res:script/exmobijs/utility.js"/> <script src="res:script/exmobijs/app.js"/> <script src="res:script/exmobijs/weibo.js"/> <script> <![CDATA[ function loadPhoto(){
var photo = document.getElementById("photo");
photo.openCameraUpload(true);
}
function doSend(type){ var content = document.getElementById("content").value; var photo = document.getElementById("photo"); var path = ""; if(photo.getFilePaths().length>0){ path = photo.getFilePath(photo.getFilePaths().length-1); } if(!Util.getConnectState()){ $a.toast("抱歉,网络无法连接,请稍后分享!"); return; }
$wb.init(type).go(content, path); }
]]> </script> </head> <body> <textarea prompt="请输入微博内容" id="content"></textarea> <photoupload id="photo" name="photo" style="display:none"/> <input type="button" value="拍照" onclick="loadPhoto()"style="width:30%;"></input> <input type="button" value="发送新浪" onclick="doSend('sina')" style="width:35%;"></input> <input type="button" value="发送腾讯" onclick="doSend('tencent')" style="width:35%;"></input> </body> </html> |
17.7.3 解绑微博账号
微博账号可以进行解绑,以释放给其他微博账户使用,解绑的方法为clear(),如下:
$wb.init(type).clear(); |
18 应用实例开发
应用开发环节主要是根据需求确认形成的文档进行移动应用开发,形成对应的应用包。
这里以页面抓取形式适配烽火Eoffice为例进行介绍,开发流程借助请求与响应的模式逐步模拟页面里的各个请求从而形成移动应用,模拟每一个请求时分为“分析页面请求à上行请求àMAPP路由àJSP处理”四步。
18.1 登录页
18.1.1 当前目标
| 浏览器显示效果 | 客户端显示效果 |
登录页 |
制作登录页,实现页面基本元素包括用户名、密码和登录按钮,可以触发应用登录请求。
18.1.2 抓包分析
1. 打开抓包工具
打开HTTPAnalyzer,点击启动开启抓包模式。
2. 打开浏览器
打开浏览器,进入Eoffice登录页http://miap.cc:8181/login.php。
3. 查看抓包
4. 分析结果
通过查看抓包可见登录页即get请求1(http://miap.cc:8181/login.php)获取响应html页面,其中登录账号是名为USERNAME的文本框,登录账号是名为PASSWORD的密码框,登录按钮触发的是名为loginState()的Javascript方法,阅读响应html可知上述元素均可固定,因此可采用异步模式完成登录页制作。
注:ExMobi®4.0建议采用静态页面加AJAX获取数据的形式开发应用,此种异步模式可通过xxxx链接地址下载套用提供的UI模板,有助于缩短开发周期,大大提升开发效率。本应用均采取异步模式开发。
18.1.3 发起上行
1. 新建应用
新建应用时config.xml中配置homepage为本地静态页面res:page/login.xhtml。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <config clientversion="4" scope="client"> <appid>eoffice@test</appid> <appname>Eoffice</appname> <description></description> <version>1.0.1</version> <date>2012-06-01</date> <homepage src="res:page/login.xhtml"/> <!-- <homepage src="http://EOFFICEURL/login.php"/> --> <vendor url="" email=""></vendor> <access network="true" gps="false" camera="false" certificate="false"/> <icon selectedlogo="res:image/logo.png" main="res:image/main.png" logo="res:image/logo.png"/> </config> |
注:开发准备时基础环境要求MBuilder此时已经打开,并客户端模拟器可以成功连接服务端。
18.1.4 制作静态页面
1. 新建静态页面
在MBuilder里右键选中eoffice@test/client/page/phone/default,新建login.xhtml。
1. 还原目标元素
编辑login.xhtml,参照12.2.1还原目标元素,用户名、密码和登录按钮。
<html style="background-image:url(res:image/bg/bg_login.jpg);"> <head> <title show="false"/> <link rel="stylesheet" type="text/css" href="res:css/global.css" /> <link rel="stylesheet" type="text/css" href="res:css/control.css" /> </head> <header> <titlebar caption="返回" title="用户登录" iconhref="script:close" riconhref="doSubmit()" rcaption="登录"/> </header> <body class="bg-transparent" onload="doLoad()"> <form id="form" action="http://EOFFICEURL/logincheck.php" method="post"> <img src="res:image/login/logo_login.png" class="center" style="margin:16dp 0 16dp 0;"/> <!-- LOGO可更改--> <div class="input-wrap"><font class="login-left">用户名</font><input type="text" id="USERNAME" name="USERNAME" class="input-hiding input-txt"/></div> <div class="input-wrap"><font class="login-left">密码</font><input type="password" id="PASSWORD" name="PASSWORD" class="input-hiding input-txt"/></div> <input type="hidden" name="PASSWORD2" value=""/> <input type="hidden" name="USER_LANG" value="cn"/> <div class="input-wrap inner-middle"> <font>自动登录</font> <switch ontext="开" offtext="关" id="switch_autologin" class="right" checked="true" style="margin:0 4dp 0 0;"/> </div> </form> </body> </html> |
注:当前步骤只完成界面制作,此处静态页面中登录按钮模拟浏览器中请求触发checklogin()方法,方法内容在中12.3完善。
18.1.5 查看效果
1. 下载应用
注:开发准备时基础环境要求客户端模拟器此时已经打开,并客户端模拟器可以成功连接服务端。
2. 点击进入应用
18.2 应用登录
18.2.1 当前目标
| 浏览器显示效果 | 客户端显示效果 |
应用登录成功进主页 | ||
应用登录失败登录提示 |
模拟页面登录请求,登录成功跳转至主页,登录失败提示框提示。
18.2.2 抓包分析
1. 浏览器操作
分别进行成功与失败登录,予以抓包。
2. 查看抓包
当输入正确用户名/密码成功登录时,post请求1(http://miap.cc:8181/checkdata.php)AJAX校验账号密码响应文本“1”,post请求2(http://miap.cc:8181/logincheck.php)表单提交登录,get请求3(http://miap.cc:8181/general/index.php)跳转至主页。
当输入错误用户名/密码失败登录时,post请求1(http://miap.cc:8181/checkdata.php)AJAX校验账号密码响应错误提示信息。
3. 分析结果
通过查看抓包可见浏览器中登录请求存在AJAX校验(http://miap.cc:8181/checkdata.php),开发移动应用并不是全盘照搬浏览器,为了简化手机端操作提高处理速度,此处结合静态登录页跳过AJAX校验,直接模拟真正的登录请求(http://miap.cc:8181/logincheck.php),通过JSP进行数据抽取,动态的判断登录成功或失败,结合12.3.1目标错误时使用alert提示框提示错误信息,成功时显示主页。
18.2.3 发起上行
1. 编辑login.xhtml
点击登陆按钮,触发js进行非空验证并进行登陆提交。
注:由于Eoffice为产品,不同Eoffice部署地址会有不一,为了方便应用部署此处配置了域名EOFFICEURL,如静态页面中代码“//设置url var url = "http://EOFFICEURL/logincheck.php";”。
<html style="background-image:url(res:image/bg/bg_login.jpg);"> <head> <title show="false"/> <link rel="stylesheet" type="text/css" href="res:css/global.css" /> <link rel="stylesheet" type="text/css" href="res:css/control.css" /> <script type="text/javascript" src="res:script/exmobijs/base.js"></script> <script type="text/javascript" src="res:script/exmobijs/utility.js"></script> <script type="text/javascript" src="res:script/exmobijs/validate.js"></script> <script type="text/javascript" src="res:script/exmobijs/config.js"></script> <script type="text/javascript" src="res:script/exmobijs/app.js"/> <script type="text/javascript"> <![CDATA[ function doSubmit() { //登录前表单验证 var v = $v.validate().add("USERNAME", "用户名不能为空!", "Require").start(); if(v) {
if($("switch_autologin").checked) { $u.cache.set("USERNAME","PASSWORD","switch_autologin"); } else { $u.cache.clear("USERNAME","PASSWORD","switch_autologin"); } window.setStringSession("username",$("USERNAME").value); window.setStringSession("password",$("PASSWORD").value); $("form").submit();
} } //初始化操作,主要是设置cahce function doLoad(){ $u.cache.get("USERNAME","PASSWORD","switch_autologin"); } function changeSwitch() { document.getElementById('switch_autologin').checked=!document.getElementById('switch_autologin').checked; }
]]>
</script> </head> <header> <titlebar caption="返回" title="用户登录" iconhref="script:close" riconhref="doSubmit()" rcaption="登录"/> </header> <body class="bg-transparent" onload="doLoad()"> <form id="form" action="http://EOFFICEURL/logincheck.php" method="post"> <img src="res:image/login/logo_login.png" class="center" style="margin:16dp 0 16dp 0;"/> <!-- LOGO可更改--> <div class="input-wrap"><font class="login-left">用户名</font><input type="text" id="USERNAME" name="USERNAME" class="input-hiding input-txt"/></div> <div class="input-wrap"><font class="login-left">密码</font><input type="password" id="PASSWORD" name="PASSWORD" class="input-hiding input-txt"/></div> <input type="hidden" name="PASSWORD2" value=""/> <input type="hidden" name="USER_LANG" value="cn"/> <div class="input-wrap inner-middle"> <font>自动登录</font> <switch ontext="开" offtext="关" id="switch_autologin" class="right" checked="true" style="margin:0 4dp 0 0;"/> </div> </form> </body> </html> |
注:此处需要新建主页静态页面以后成功后调用,此时尚且不定具体模块请求的链接地址,所以只有基本页面元素,并无链接。代码如下:
<html> <head> <title show="false"/> <link rel="stylesheet" type="text/css" href="res:css/global.css" /> <link rel="stylesheet" type="text/css" href="res:css/control.css" /> <style type="text/css">
</style> </head> <header> <titlebar caption="返回" title="首页" iconhref="script:close" rcaption="设置"/> </header> <body > <grid col="4" class="grid-four"> <cell icon="res:image/grid/grid_icon_user.png" text="通知公告" href="" /> <cell icon="res:image/grid/grid_icon_user.png" text="加班申请" href="" /> <cell icon="res:image/grid/grid_icon_user.png" text="功能模板" href="" /> <cell icon="res:image/grid/grid_icon_user.png" text="推送设置" href="res:page/pushSure.xhtml" /> </grid> <hr/> <div class="inner-middle list_box"> <img src="res:image/icon/icon_user.png" style="margin:0 0 0 4dp"/> <font>加班申请</font> <div class="inner-center inner-middle right tip" >33</div> </div> <div class="inner-middle list_box"> <img src="res:image/icon/icon_user.png" style="margin:0 0 0 4dp"/> <font>公共文档</font> <div class="inner-center inner-middle right tip">13</div> </div> </body> <footer class="inner-center inner-middle " style="height:50;background-color:#222222;"> <menubar> <menu icon="res:image/menubar/menubar_icon.png" text="功能模板"/> <menu icon="res:image/menubar/menubar_icon.png" text="功能模板"/> <menu icon="res:image/menubar/menubar_icon.png" text="功能模板"/> <menu icon="res:image/menubar/menubar_icon.png" text="功能模板"/> <menu icon="res:image/menubar/menubar_icon.png" text="功能模板" class="inner-center menubar white bg-black"/> </menubar> </footer> </html> |
18.2.4 MAPP路由
1. 配置域名
在mapp.xml中给应用配置域名EOFFICEURL,代码如下
<?xml version="1.0" encoding="UTF-8" ?> <maxml version="2.0" xmlns="http://www.nj.fiberhome.com.cn/map" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.nj.fiberhome.com.cn/map maxml-2.0.xsd"> <config> <!-- 域名配置 --> <domain name="EOFFICEURL" address="miap.cc:8181"/> </config> </maxml> |
2. 配置路由
在mapp.xml中给请求地址http://EOFFICEURL/login.php配置相关路由,使其进入对应的JSP文件进行数据转换,配置方法如下
<?xml version="1.0" encoding="UTF-8" ?> <maxml version="2.0" xmlns="http://www.nj.fiberhome.com.cn/map" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.nj.fiberhome.com.cn/map maxml-2.0.xsd"> <config> <!-- 域名配置 --> <domain name="EOFFICEURL" address="miap.cc:8181"/> </config> <!-- 配置路由 --> <route baseaddr="http://EOFFICEURL"> <!-- 登录判断 --> <forward pattern="/logincheck.php" path="logincheck.jsp"/> </route> </maxml> |
3. 新建JSP
在MBuilder里右键选中eoffice@test/server/jsp,选择“New→JSP File”,输入File name为logincheck.jsp后,点击“Next”选择“New JSP File(exmobi)”,完成新建。
18.2.5 JSP处理
1. 发起请求获取响应
在logincheck.jsp中,使用<aa:http>标签发起请求获取响应
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%> <!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <aa:http></aa:http> |
注1: <!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd">。此申明用于在开发过程中进行客户端控件标签提示,譬如在body下输入jiugong,但输入“<j”时就有提示。此申请新建时自动引入,可视情况自行删减。
注2:JSP头部include的JSP“<%@ include file="/client/adapt.jsp"%>”是用于提示服务端标签,譬如<aa:http/>等,新建时自动添加。
服务端标签提示只在根元素下可以提示,在根元素外侧无法提示,如
可以提示 | <%@ page language="java" import="java.util.*" pageEncoding="UTF-8" contentType="application/uixml+xml; charset=UTF-8"%> <%@ include file="/client/adapt.jsp"%> <!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <%--根元素外侧--%>
<html><%--此处响应html即为根元素--%> <%--根元素内侧--%> <aa:http></aa:http> </html> |
不可提示 | <%@ page language="java" import="java.util.*" pageEncoding="UTF-8" contentType="application/uixml+xml; charset=UTF-8"%> <%@ include file="/client/adapt.jsp"%> <!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <%--根元素外侧--%> <aa:http></aa:http> <html><%--此处响应html即为根元素--%> <%--根元素内侧--%> </html> |
2. 查看临时文件
进入临时文件所在目录,打开*_dom.html的临时文件,通过判断登录成功/失败响应数据的差异做不同的JSP响应处理。
此处的文件名是由请求设置的id决定的,如果aa:http没有设置id,那么生成的临时文件就是随机的uuid字符串命名。如果设置为id=”a1”,那么文件名就变成a1.html和a1_dom.html。
1) 失败时响应数据
2) 成功时响应数据
很明显成功和失败的差异有很多,选取其中一种“失败时有name为login_error的input,成功时没有”为判断条件。
3. 数据拣选处理响应
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%> <!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <aa:http></aa:http> <aa:choose> <aa:when testxpath="//input[@name='login_error']"> <html type="alert"> <body> <alert title="提示" icontype="info"> <msg><aa:value-of xpath="//input[@name='login_error']/@value"/></msg> </alert> </body> </html> </aa:when> <aa:otherwise> <html> <head> <title show="false"/> <link rel="stylesheet" type="text/css" href="res:css/global.css" /> <link rel="stylesheet" type="text/css" href="res:css/control.css" /> <style type="text/css">
</style> </head> <header> <titlebar caption="返回" title="首页" iconhref="script:close" rcaption="设置"/> </header> <body > <grid col="4" class="grid-four"> <cell icon="res:image/grid/grid_icon_user.png" text="通知公告" href="" /> <cell icon="res:image/grid/grid_icon_user.png" text="加班申请" href="" /> <cell icon="res:image/grid/grid_icon_user.png" text="功能模板" href="" /> <cell icon="res:image/grid/grid_icon_user.png" text="推送设置" href="res:page/pushSure.xhtml" /> </grid> <hr/> <div class="inner-middle list_box"> <img src="res:image/icon/icon_user.png" style="margin:0 0 0 4dp"/> <font>请假申请</font> <div class="inner-center inner-middle right tip" >33</div> </div> <div class="inner-middle list_box"> <img src="res:image/icon/icon_user.png" style="margin:0 0 0 4dp"/> <font>公共文档</font> <div class="inner-center inner-middle right tip">13</div> </div> </body> <footer class="inner-center inner-middle " style="height:50;background-color:#222222;"> <menubar> <menu icon="res:image/menubar/menubar_icon.png" text="功能模板"/> <menu icon="res:image/menubar/menubar_icon.png" text="功能模板"/> <menu icon="res:image/menubar/menubar_icon.png" text="功能模板"/> <menu icon="res:image/menubar/menubar_icon.png" text="功能模板"/> <menu icon="res:image/menubar/menubar_icon.png" text="功能模板" class="inner-center menubar white bg-black"/> </menubar> </footer> </html> </aa:otherwise> </aa:choose>
|
上面的代码中,请假申请的待办条数是写死的,如何动态的取呢?
在现有的响应文件中,没有加班申请的待办条数,这个待办条数,得请求待办事宜http://EOFFICEURL/general/workflow/work_list.php这个地址,在这个响应中获取到,那如何在jsp中发起两次请求呢?
只需要再写个<aa:http>,具体代码如下:
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%> <!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <aa:http></aa:http> <aa:http id="a1" url="http://EOFFICEURL/general/workflow/work_list.php" method="get"> </aa:http> <aa:choose> <aa:when testxpath="//input[@name='login_error']"> <html type="alert"> <body> <alert title="提示" icontype="info"> <msg><aa:value-of xpath="//input[@name='login_error']/@value"/></msg> </alert> </body> </html> </aa:when> <aa:otherwise> <html> <head> <title show="false"/> <link rel="stylesheet" type="text/css" href="res:css/global.css" /> <link rel="stylesheet" type="text/css" href="res:css/control.css" /> <style type="text/css">
</style> </head> <header> <titlebar caption="返回" title="首页" iconhref="script:close" rcaption="设置"/> </header> <body > <grid col="4" class="grid-four"> <cell icon="res:image/grid/grid_icon_user.png" text="通知公告" href="" /> <cell icon="res:image/grid/grid_icon_user.png" text="加班申请" href="" /> <cell icon="res:image/grid/grid_icon_user.png" text="功能模板" href="" /> <cell icon="res:image/grid/grid_icon_user.png" text="推送设置" href="res:page/pushSure.xhtml" /> </grid> <hr/> <div class="inner-middle list_box" > <img src="res:image/icon/icon_user.png" style="margin:0 0 0 4dp"/> <font>请假申请</font> <div class="inner-center inner-middle right tip" ><aa:value-of xpath="//a[@href='work_list3.php?flow_id_r=22'][last()]/." dsId="a1"/></div> </div> <div class="inner-middle list_box"> <img src="res:image/icon/icon_user.png" style="margin:0 0 0 4dp"/> <font>公共文档</font> <div class="inner-center inner-middle right tip">13</div> </div> </body> <footer class="inner-center inner-middle " style="height:50;background-color:#222222;"> <menubar> <menu icon="res:image/menubar/menubar_icon.png" text="功能模板"/> <menu icon="res:image/menubar/menubar_icon.png" text="功能模板"/> <menu icon="res:image/menubar/menubar_icon.png" text="功能模板"/> <menu icon="res:image/menubar/menubar_icon.png" text="功能模板"/> <menu icon="res:image/menubar/menubar_icon.png" text="功能模板" class="inner-center menubar white bg-black"/> </menubar> </footer> </html> </aa:otherwise> </aa:choose>
|
18.2.6 查看效果
18.3 请假申请·列表
18.3.1 当前目标
| 浏览器显示效果 | 客户端显示效果 |
公共文档列表 |
模拟浏览器中“ 待办事宜—〉请假申请”模块,显示待办列表,其中涵盖事宜标题、创建人、当前步骤,可以触发链接查看事宜详情页面。
18.3.2 抓包分析
1. 浏览器操作
点击查看“待办事宜—〉请假申请”模块,予以抓包。
2. 查看抓包
3. 分析结果
通过查看抓包可见待办列表页即get请求(http://miap.cc:8181/general/workflow/work_list3.php?flow_id_r=22),获得列表信息。
18.3.3 发起上行
1. 在主页面中加入加班申请的链接
<div class="inner-middle list_box" href="http://EOFFICEURL/general/workflow/work_list3.php?flow_id_r=22"> <img src="res:image/icon/icon_user.png" style="margin:0 0 0 4dp"/> <font>请假申请</font> <div class="inner-center inner-middle right tip" ><aa:value-of xpath="//a[@href='work_list3.php?flow_id_r=22'][last()]/." dsId="a1"/></div> </div> |
18.3.4 MAPP路由
1. 配置路由
在mapp.xml中给请求地址http://EOFFICEURL/general/workflow/work_list3.php\?flow_id_r=22配置相关路由,使其进入对应的JSP文件进行数据转换,配置方法如下
注:在forward标签的pattern属性中写的是正则表达式,“&”需要转换成实体形式“&”表示,如果不转日志会报错提示mapp.xml文件格式有误;而“?”在正则表达式中有实在意义,表示匹配零次或一次,所以此处需要将其前面加斜杠转义“\?”。
<?xml version="1.0" encoding="UTF-8" ?> <maxml version="2.0" xmlns="http://www.nj.fiberhome.com.cn/map" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.nj.fiberhome.com.cn/map maxml-2.0.xsd"> <config> <!-- 域名配置 --> <domain name="EOFFICEURL" address="miap.cc:8181"/> </config> <!-- 配置路由 --> <route baseaddr="http://EOFFICEURL"> <!-- 登录判断 --> <forward pattern="/logincheck.php" path="logincheck.jsp"/> <!—请假申请 --> <forward pattern="/general/workflow/work_list3.php\?flow_id_r=22" path="worklist.jsp"/> </route> </maxml> |
2. 新建JSP
在MBuilder里右键选中eoffice@test/server/jsp,选择“New→JSP File”,输入File name为worklist.jsp后,点击“Next”选择“New JSP File(exmobi)”,完成新建。
18.3.5 JSP处理
1. 发起请求获取响应
在worklist.jsp中,使用<aa:http>标签发起请求获取响应
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%> <!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <aa:http></aa:http>
|
2. 查看临时文件
进入临时文件所在目录,打开*_dom.html的临时文件,定位列表信息所在位置。
阅读临时文件,定位列表信息即为响应数据中class=“pubtable”下面的tr节点,第一个tr为状态,第二个tr为表头,第三个tr开始为列表数据。
3. 数据拣选处理响应
在worklist.jsp中,用listitem控件展现列表数据。
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%> <!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <aa:http></aa:http>
<html> <head> <title show="false"/> <link rel="stylesheet" type="text/css" href="res:css/global.css" /> <link rel="stylesheet" type="text/css" href="res:css/control.css" /> </head> <header> <titlebar caption="返回" title="待办列表" iconhref="script:close" rcaption="查询"/> </header> <body> <aa:for-each var="list" xpath="//table[@class='pubtable']/tr[position() > 2]"> <listitem type="twoline" href="<%=aa.xpath("./td[3]/a/@href", "list")%>" icon="res:image/icon/icon_user.png" caption="<%=aa.xpath("./td[3]/.", "list")%>" sndcaption="<%=aa.xpath("./td[7]/.","list")%>" rcaption="<%=aa.xpath("./td[6]/.", "list")%>"/> </aa:for-each> </body> </html> |
注:此处item的href为触发详情的链接地址,12.5.3章节中会有描述。
18.3.6 查看效果
18.4 请假申请·详情
18.4.1 当前目标
| 浏览器显示效果 | 客户端显示效果 |
详情 |
模拟浏览器中“请假申请—》详情”模块,点击请假申请列表显示详情,其中涵盖详情和选择步骤提交。
18.4.2 抓包分析
1. 浏览器操作
打开详情页面,予以抓包。
2. 查看抓包
3. 分析结果
通过查看抓包可见详情页即get请求(http://miap.cc:8181/general/workflow/input_form/?OP_FLAG=1...)即可获取响应的详情页面数据。
18.4.3 发起上行
之前在worklist列表页面中,详情地址已经配好,详见6.4节。
18.4.4 MAPP路由
1. 配置路由
在mapp.xml中给请求地址http://EOFFICEURL/general/workflow/input_form/?OP_FLAG=1...配置相关路由,使其进入对应的JSP文件进行数据转换,配置方法如下
注:在forward标签的pattern属性中写的是正则表达式,“&”需要转换成实体形式“&”表示,如果不转日志会报错提示mapp.xml文件格式有误;而“?”在正则表达式中有实在意义,表示匹配零次或一次,所以此处需要将其前面加斜杠转义“\?”。
<?xml version="1.0" encoding="UTF-8" ?> <maxml version="2.0" xmlns="http://www.nj.fiberhome.com.cn/map" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.nj.fiberhome.com.cn/map maxml-2.0.xsd"> <config> <!-- 域名配置 --> <domain name="EOFFICEURL" address="miap.cc:8181"/> </config> <!-- 配置路由 --> <route baseaddr="http://EOFFICEURL"> <!-- 登录判断 --> <forward pattern="/logincheck.php" path="logincheck.jsp"/> <!-- 待办公文列表 --> <forward pattern="/general/workflow/work_list3.php\?flow_id_r=22" path="worklist.jsp"/> <!-- 办理流程 --> <forward pattern="/general/workflow/input_form/\?.*" path="workdetail.jsp"/> </route> </maxml> |
2. 新建JSP
在MBuilder里右键选中eoffice@test/server/jsp,选择“New→JSP File”,输入File name为workdetail.jsp后,点击“Next”选择“New JSP File(exmobi)”,完成新建。
18.4.5 JSP处理
1. 发起请求获取响应
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%> <!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <aa:http id="workdetail"> </aa:http> |
2. 查看临时文件
进入临时文件所在目录,打开workdetail _dom.html的临时文件,定位列表信息所在位置。
阅读临时文件,可以看到表单信息是由两个form表单里面的数据组成。那详情只需要把这两个表单中的数据取出即可,因为表单的字段很少,所以我们可以把需要的数据一一取出。
3. 数据拣选处理响应
在workdetail.jsp中,从响应数据中,进行挑选数据进行展现。
<%@ page language="java" import="java.util.*,com.fiberhome.bcs.appprocess.common.util.*" contentType="application/uixml+xml; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="/client/adapt.jsp"%> <!DOCTYPE html SYSTEM "http://www.nj.fiberhome.com.cn/exmobi.dtd"> <aa:http id="workdetail"> </aa:http> <html id="workdetail"> <head> <title show="false"/> <link rel="stylesheet" type="text/css" href="res:css/global.css" /> <link rel="stylesheet" type="text/css" href="res:css/control.css" /> </head> <header> <titlebar caption="返回" title="<%=aa.xpath("//td[@class = 'pagehead1']/.","workdetail")%>" iconhref="script:close" rcaption="查询"/> </header> <body> <form action="http://EOFFICEURL/general/workflow/input_form/do.php" method="post" id="form1"> <br size="15"/> <%--流程表单tnotenum--%> <div id="tnotenum" style="border-size:1;border-radius:4;"> <br/> <font>●流程表单</font> <hr/> <font style="width:30%;">说明</font> <font style="width:70%;"> <aa:value-of xpath="//form[@name = 'runnameform']/tr/td[2]/." dsId="workdetail"/> </font> <hr/> <font style="width:30%;">流水号</font> <font style="width:70%;"> <aa:value-of xpath="//form[@name = 'runnameform']/tr/td[4]/." dsId="workdetail"/> </font> <hr/> <font style="width:30%;">请假人</font> <input style="width:70%;" type="text" name="DATA_1" value="<%=aa.xpath("//input[@name = 'DATA_1']/@value", "workdetail")%>" readonly="<%= aa.xpathNode("//input[@name = 'DATA_1' and @readonly]","workdetail")==null?"false":"true"%>"/> <hr/> <font style="width:30%;">部门</font> <input style="width:70%;" type="text" name="DATA_2" value="<%=aa.xpath("//input[@name = 'DATA_2']/@value", "workdetail")%>" readonly="<%= aa.xpathNode("//input[@name = 'DATA_2' and @readonly]","workdetail")==null?"false":"true"%>"/> <hr/> <font style="width:30%;">请假类型</font> <input style="width:70%;" type="text" name="DATA_3" value="<%=aa.xpath("//input[@name = 'DATA_3']/@value", "workdetail")%>" readonly="<%= aa.xpathNode("//input[@name = 'DATA_3' and @readonly]","workdetail")==null?"false":"true"%>"/> <hr/> <font style="width:30%;">请假事由</font> <textarea rows="2" type="text" name="DATA_4" readonly="<%=aa.xpathNode("//textarea[@name = 'DATA_4' and @readonly]","workdetail")==null?"false":"true"%>"> <aa:value-of xpath="//textarea[@name = 'DATA_4']/text()" dsId="workdetail"/> </textarea> <hr/> <font style="width:30%;">请假时间</font>共<input style="width:30%;" type="text" name="DATA_7" value="<%=aa.xpath("//input[@name = 'DATA_7']/@value", "workdetail")%>" readonly="<%= aa.xpathNode("//input[@name = 'DATA_7' and @readonly]","workdetail")==null?"false":"true"%>"/>天 <br/> <object type="date" class="width-half" name="DATA_5" value="<%=aa.xpath("//input[@name = 'DATA_5']/@value", "workdetail")%>" prompt="开始日期" readonly="<%= aa.xpathNode("//input[@name = 'DATA_5' and @readonly]","workdetail")==null?"false":"true"%>"></object> <object type="date" class="width-half" name="DATA_6" value="<%=aa.xpath("//input[@name = 'DATA_6']/@value", "workdetail")%>" prompt="开始日期" readonly="<%= aa.xpathNode("//input[@name = 'DATA_6' and @readonly]","workdetail")==null?"false":"true"%>"></object> <hr/>
<font style="width:50%;">公司领导审批</font><br/> <textarea rows="2" type="text" name="DATA_8" readonly="<%=aa.xpathNode("//textarea[@name = 'DATA_8' and @readonly]","workdetail")==null?"false":"true"%>"> <aa:value-of xpath="//textarea[@name = 'DATA_8']/text()" dsId="workdetail"/> </textarea>
<%--隐藏参数--%> <aa:for-each var="hid" dsId="workdetail" xpath="//input[@type = 'hidden']"> <input type="hidden" id="<%=aa.xpath("./@name","hid")%>" name="<%=aa.xpath("./@name","hid")%>" value="<%=aa.xpath("./@value","hid")%>" /> </aa:for-each> </div> </form> </body> </html> |
18.4.6 查看效果
19 附录
19.1 DPI计算方式说明
DPI主要分为四类,所以一个应用一般最多做4套资源:
ldpi——小于160DPI屏幕用120DPI算。
mdpi——160DPI至239DPI屏幕用160DPI算。
hdpi——240DPI至319DPI屏幕用240DPI算。
xhdpi——大于320DPI屏幕用320DPI算。
下面是DPI速查表,如果不在速查表中的手机,请按公式计算得出相应的DPI
手机分辨率 | 屏幕尺寸 | 实际DPI | 分屏 | DPI值 |
480*800 | 4英寸 | 233 | 高分屏 [188,300] | 240 |
480*854 | 3.7英寸 | 265 | 高分屏 [188,300] | 240 |
1024*768 | 9.7英寸 | 132 | 低分屏 [0,135] | 120 |
320*480 | 3.5英寸 | 165 | 中分屏 [136,187] | 160 |
640*960 | 3.5英寸 | 330 | 超高分屏 [300,+错误!未找到引用源。错误!未找到引用源。] | 320 |
计算手机DPI公式如下:
示例:
三星i9000,屏幕尺寸4英寸,分辨率480*800。套入公式
计算结果取近似值233,即三星i9000的dpi为233。介于高分屏区间[188,300],所以取240
19.2 客户端头信息说明
客户端头信息主要有(头信息均为小写):
头信息 | 说明 |
imsi | 手机卡唯一标识 |
esn | 移动设备唯一标识 |
os | 移动设备操作系统类型,取值为pc、ios、android等 |
platformid | 移动设备的型号 |
clientversion | 客户端的版本号 |
osversion | 移动设备操作系统的版本号 |
clientid | 客户端唯一标识 |
screenwidth | 设备屏幕的宽度 |
screenheight | 设备屏幕的高度 |
appid | 当前访问应用的appid |
url | 客户端请求的URL地址 |
method | 客户端请求的方式,1为get,2为post |
charset | 请求的字符编码 |