1、互联网产品特点与OperaMasks的特性
OperaMasks作为一款Web应用开发框架已经在许多项目的开发实践中体现了其应用于IT系统的高效和敏捷。众所周知,“Web应用”或称“B/S系统”是一个相对广泛的代名词,小至一个公司内部的CRM、MIS等系统,大至一个互联网的大型网站都属于这个范畴。以CRM、MIS等为代表的系统可以归类为企业内部IT系统,这类系统通常包含以下特点:
- 有地域范围限制(通常建立在一个公司内部的Intranet);
- 用户的数量通常比较固定和有限;
- 对满足业务需求,系统稳定性和数据安全性方面要求比较高;
- 界面风格相对统一,变化不会太大,更多的关注于实用而不是人机交互。
有别于这类系统,互联网产品更为普通人所熟悉,主要包括了门户,搜索引擎,社区,电子商务等和我们日常生活息息相关的地方。它们的特点也恰恰和传统的IT系统相反,因此,互联网产品作为在Internet上同时被大量用户访问的系统需要遵循的最重要的原则就是“快速,简洁,用户体验好”。
一直以来,OperaMasks 在开发企业应用的领域具备很大的优势,但并不代表在开发互联网产品的时候就不具备。事实上,自从OperaMasks3.0以后,其作为适合互联网产品开发的特性已经崭露头角。接下来通过OperaMasks在电子商务网站中的应用进行介绍。
2、构件资源依赖的修改--性能的初步优化
OperaMasks自带了一套官方的构件库,这套构件库使用了第三方的客户端JS框架-EXTJS。
EXTJS是一套功能强大的JS控件库,里面包含的控件功能全面并且使用简单。有利必有弊,这么强大的功能同时带来了让人困扰的缺点:
首先,依赖的资源过多,其次,页面繁杂的JS操作也会对老式的浏览器客户端(比如IE6)的页面渲染造成不小的挑战。
这两点都会直接导致页面的渲染时间过长,这是与“快速,简洁”的原则不相符的。再加上EXTJS控件的封装粒度大,如果想在原来的基础上进行大幅度的样式修改也是不太可行的。
总而言之,首先需要把构件对资源的依赖去除掉,在OperaMasks中修改构件的资源依赖是再简单不过的事。以w:page为例子,首先从
《Apusic OperaMasks 3.0参考手册》中找到w:page对应的构件渲染类为org.operamasks.faces.render.widget.ajax.AjaxPageRenderer。然后新建一个自己的渲染类com.apusic.nsec.renderer.NSECPageRenderer继承自AjaxPageRenderer,接着覆盖getDependedJSPackages和getDependedCSSPackages方法返回null。
AjaxPageRenderer.java:
@Override
public String[] getDependedJSPackages(FacesContext context, UIComponent component) {
return new String[] {"Ext.Base"};
}
@Override
public String[] getDependedCSSPackages(FacesContext context,
UIComponent component) {
return new String[] {"All"};
}
NSECPageRenderer.java:
@Override
public String[] getDependedJSPackages(FacesContext context, UIComponent component) {
return null;
}
@Override
public String[] getDependedCSSPackages(FacesContext context, UIComponent component) {
return null;
}
最后在工程的faces-config.xml中加上这段配置即可:
<render-kit>
<render-kit-id>AJAX</render-kit-id>
<renderer>
<component-family>org.operamasks.faces.HtmlDocument</component-family>
<renderer-type>org.operamasks.faces.HtmlPage</renderer-type>
<renderer-class>com.apusic.nsec.renderer.NSECPageRenderer</renderer-class>
</renderer>
</render-kit>
这些完成之后编写一个只包含w:page构件的faces页面,然后在客户端查看这个页面的源代码时就再也看不到EXTJS的影子了。
3、主页面模板+复合构件--页面的最大复用
电子商务网站和传统的IT系统有很大的不同,网站在页面之间的跳转和链接比较频繁,因此需要编写大量的静态(html)或者动态(faces)页面,这个时候页面复用很关键,比如网站中几乎每个页面都需要一个页头,左侧菜单和页脚,如下图:
这个时候模版正好派上用场。模板里面包含两大部分内容,一部分是模板的固定内容(如上图中被红色边框包围的地方),另一部分是待插入的动态内容。使用模板的页面自动“继承”了模板中的固定内容,然后在动态内容的区域填上本页面实际的内容就完成了。在模板里我们还同时引入了工程依赖的一些静态资源包括Jquery JS和网站的基础CSS样式文件template.css.。
如果你认为光这样就够了,那就错了。主角才刚刚登场:复合构件。
对于许多不同的页面中总能找到一些类似的可以重用的页面代码,比如在网站的很多地方中需要用小图标的方式表示某个卖家的信誉积分,显示这些图标的逻辑是在页面进行很多的if/else判断,根据卖家的信誉分数值显示不同的图标。
这个时候可以把那些重复的代码都删掉,只留一份封装成一个复合构件creditIcon就行了,使用这个复合构件只需要传入一个具体的分值作为构件的属性值,然后剩下的事情都由creditIcon来完成。
正式使用这个复合构件之前还需要先新建一个配置文件composition_component.taglib.xml,在文件中加上这段配置:
<facelet-taglib>
<namespace>http://www.nsec.com</namespace>
<tag>
<tag-name>creditIcon</tag-name>
<source>composition/creditIcon.xhtml</source>
</tag>
</facelet-taglib>
接下来就可以在页面中使用了,注意要先在f:view里面声明nsec的命名空间:
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:nsec="http://www.nsec.com" xmlns:ui="http://java.sun.com/jsf/facelets"
renderKitId="AJAX">
<ui:decorate template="template.xhtml">
<ui:param name="message" value="会员级别图标控件演示:会员级别图标控件"/>
<ui:define name="content">
<nsec:creditIcon credit="1"></nsec:creditIcon>
<nsec:creditIcon credit="2"></nsec:creditIcon>
<nsec:creditIcon credit="4"></nsec:creditIcon>
<nsec:creditIcon credit="8"></nsec:creditIcon>
</ui:define>
</ui:decorate>
</f:view>
这样就够了?等等,还没结束,在项目里面发现仅仅显示分数还是不够,还需要根据分数查询对应的级别然后再显示级别对应的图标,这个需要在显示之前先执行一次数据库查询的动作自然也放在复合构件里面。首先给复合构件绑定一个ManagedBean:
<om:useBean value="composition.gradeIconBean"></om:useBean>
@ManagedBean(name = "composition.gradeIconBean", scope = ManagedBeanScope.REQUEST)
public class GradeIconBean {
WebApplicationContext wac = WebApplicationContextUtils
.getWebApplicationContext((ServletContext) FacesContext
.getCurrentInstance().getExternalContext().getContext());
private CreditPointLevelService creditPointSvc;
private CreditPointsLevelEnum creditPointLevel;
public void setCreditPointLevel(CreditPointsLevelEnum creditPointLevel) {
this.creditPointLevel = creditPointLevel;
}
public CreditPointsLevelEnum getCreditPointLevel() {
return creditPointLevel;
}
public CreditPointLevelService getCreditPointSvc() {
return (CreditPointLevelService) wac.getBean("creditPointLevelService");
}
}
从这个Bean中可以获取用来根据分数值查询数据库返回具体级别的service方法,还包含了一个给页面访问的级别属性(creditPointLevel)。万事俱备,只欠Elite,接下来在页面访问creditPointLevel属性之前先用Elite执行service的查询:
<om:elite>
<![CDATA[
composition.gradeIconBean.setCreditPointLevel(composition.gradeIconBean.creditPointSvc.getCreditPointLevel(gradePoint));
]]>
</om:elite>
这样每个复合构件在渲染之前都会执行这段Elite把查询结果赋予creditPointLevel属性。
使用效果:
4、submitAction+updater--页面局部刷新的黄金组合
在OperaMasks中,页面的局部刷新通常和具体的构件或构件的属性联系在一起,在上面的修改资源依赖过程中我们已经把EXTJS从工程中移除,这就意味着一些基于EXTJS的UI构件我们无法使用,因此我们需要一个能实现页面局部刷新的简便方案。
ajax:updater是一个能使页面局部刷新的容器类控件,你所需要做的是用updater把需要刷新的区域包含进去,然后在后台调用控件类的repaint()方法就可以了。当然还需要一个客户端事件去触发后台的刷新方法,以前的做法是在页面放置一个隐藏的UIButton,现在完全可以用submitAction代替了,使用submitAction一样可以通过JS调用触发后台事件,一样可以把页面的请求参数通过HTTPRequest传到后台。
比如,在电子商务网站中常常有很多页面需要用表格的形式展示数据,表格的下面通常放置了一个分页条。比如卖家查看仓库资源的页面,买家查看已购买商品的页面等等。当用户点击“下一页”或者点击“搜索”的时候需要刷新表格里面的数据,在不使用w:dataGrid(基于EXTJS实现的以表格形式展现数据的强大控件)的情况下我们采用了另一种替代方案,用c:forEach遍历后台数据并以table的形式展现在前台。由于c:forEach在后台不能绑定构件类,无法调用自身的reload或repaint去刷新数据。因此在c:forEach的table外面再放置一个updater容器,并同时在里面放置一个submitAction。
这样就可以在用户点击的时候执行submitAction的submit方法触发后台更新数据源并刷新updater。这种用法在项目的页面中已经大量使用,基本上,submitAction+updater是在不使用其他具有AJAX特性的构件的情况下实现页面局部刷新的黄金搭档,能满足99%的功能需求。
5、自定义构件--这里还能用JQUERY
由于OperaMasks默认提供了一套完整的基于EXTJS的构件库,以至于许多人可能一开始会觉得OperaMasks==Ext JS。其实不然,OperaMasks同时是一个能开发自定义构件的框架,EXT JS只是默认提供的一套构件而已,你完全可以用现有的任何浏览器客户端框架(例如DOJO,HTML5等等)或者自己编写原生的JavaScript去实现自定义的构件。
由于我们在页面的主模版中已经引入了Jquery JS。因此我们基于Jquery UI编写了一个自动下拉提示的输入构件autocomplete。
首先,编写autocomplete的构件类UIAutocomplete,这个构件类是基于普通输入控件的扩展,只需要在继承HtmlInputText的基础上添加一个数据源去获取自动提示的内容。这个自动提示的数据源可以通过EL从后台获取,也可以在页面写静态的JSON。因此定义了autocomplete构件类的两个属性为:
public class UIAutocomplete extends HtmlInputText{
/** 静态数据源 **/
protected String jsonSource;
/** 动态数据源 **/
protected MethodExpression listSource;
}
接着,编写AjaxAutocompleteHandler在构件创建的时候对listSource进行初始化:
public class AjaxAutocompleteHandler extends ComponentHandler {
private TagAttribute listSource;
public AjaxAutocompleteHandler(ComponentConfig config) {
super(config);
this.listSource=this.getAttribute("listSource");
}
@Override
protected void onComponentCreated(FaceletContext ctx, UIComponent c, UIComponent parent) {
if(this.listSource != null){
((UIAutocomplete)c).setListSource(this.listSource.getMethodExpression(ctx, List.class, null));
}
}
}
这个Handler的onComponentCreated()方法会在facelets构建构件树的时候被自动调用。
然后,开始编写渲染类AjaxAutocompleteRenderer,AjaxAutocompleteRenderer继承AjaxInputRenderer,渲染HTML代码的工作由父类完成,autocomplete渲染类只需要返回正确的资源依赖字符串和渲染一段用来初始化并绑定数据到输入框的JS脚本:
public String[] getDependedCSSPackages(FacesContext context, UIComponent component) {
return new String[] {"jquery-ui-autocomplete-css"};
}
public void encodeInitScriptEnd(FacesContext context, ResourceManager rm, UIComponent component) throws IOException {
… … // 此次省略初始化代码
fmt.format("%s={};", jsvar);
fmt.format("%s.data=%s;", jsvar, jsonSource);
fmt.format("$(function() {");
fmt.format("$( '#%s' ).autocomplete({ source: %s.data, minLength:0 });", clientId, jsvar);
fmt.format("$('#%s').bind('focus',function(){$(this).autocomplete('search','');});", clientId);
fmt.format("%s.source=function(data){$('#%s').autocomplete( 'option' , 'source' , data );%s.data=data;};",
jsvar,
clientId,
jsvar);
fmt.format("});");
ComponentResource resource = ComponentResource.getResourceInstance(rm);
resource.addInitScript(fmt.toString());
}
最后配置一下taglib和faces-config文件就可以运行了,值得注意的是配置taglib的时候需要配置<handler-class>为AjaxAutocompleteHandler:
composition_component.taglib.xml
<tag>
<tag-name>autocomplete</tag-name>
<component>
<component-type>com.apusic.nsec.component.autocomplete.autocomplete</component-type>
<renderer-type>com.apusic.nsec.component.autocomplete.autocomplete</renderer-type>
<handler-class>com.apusic.nsec.component.autocomplete.AjaxAutocompleteHandler</handler-class>
</component>
</tag>
faces-config.xml
<component>
<component-type>com.apusic.nsec.component.autocomplete.autocomplete</component-type>
<component-class>com.apusic.nsec.component.autocomplete.UIAutocomplete</component-class>
</component>
<renderer>
<component-family>com.apusic.nsec.component.autocomplete.autocomplete</component-family>
<renderer-type>com.apusic.nsec.component.autocomplete.autocomplete</renderer-type>
<renderer-class>com.apusic.nsec.component.autocomplete.AjaxAutocompleteRenderer</renderer-class>
</renderer>
运行效果:
更多示例请参看:http://www.dt1688.com
http://demo.operamasks.org/bpdemos/index.faces