事实上,本教程主要介绍的内容是 Facelets,坦白说,这并不能够体现 AOM 的魔力,但本文是后续一些章节的基础,因此,权且把本文也放在《神奇魔力》这个系列中。尽管笔者认为:在 AOM 中, Facelets 的某些特性能够发挥的更加的淋漓尽致,但必须要事先予以澄清的是:Facelets 毕竟是人家的东西,AOM 是拿来主义,并迎合了其中的某些特性而已,倘若各位看官觉得这样不妥,请各位见谅则个。
同样,在阅读本文之前,笔者建议你首先阅读前几篇文章:
AOM遵循JSF规范,因此,首先要谈一谈JSP和JSF的关系。很多同学一看到 JSF,首先想到的可能是诸如<f:view>此类的 一堆tag-lib,确实,JSF是从 JSP/Servlet 技术上发展过来的,并且,JSF从规范制定之初就试图用JSP作为JSF的底层支撑技术,但事实证明: 这是完全不同的两种编程风格,试图将这两者结合起来, 就像是把手塞进袜子一样别扭。
JSP是基于I/O流的网页开发技术,说的直白点就是:JSP所做的所有的一切,无论是“<% %>”之间的代码片断,还是 tag-lib,其最终目的就是一个:动态的拼凑 HTML或JavaScript。
而JSF则是基于组件的网页开发技术,它最终所产生的HTML也好,JavaScript也罢,甚至是如WML、SVG等其它的UI描述语言, 都是通过组件渲染得到的。JSF页面本质上就是一棵组件树,其根节点就是<f:view>。试想, Swing/SWT又何尝不是这样的呢?只不过 Swing 组件树的根节点可能是JFrame罢了。 因此,JSF就需要一种“组件树的描述技术”。在 JSF 规范制定最初,确实是希望把 JSP 的 tag-lib 作为“组件树描述技术”的,但最终发觉,这两者之间的编程思想差异太大,以致于,用JSP作为JSF的“组件树描述技术”,会给用户造成很大的误解, 譬如,很多朋友都问过我这个问题: 我能够在JSF页面中嵌入“<%%>”的代码片断吗?这些代码片断能够正常执行吗?每次遇到这种问题,我总是哭笑不得; 但如果各位读者看完上面的文字后依然想问我这个问题,那我只有痛哭流涕的份了:(
于是,Facelets出现了。这是由 Sun 公司在 dev.java.net 上的一个开源项目,其主页为:facelets.dev.java.net。为什么说 Facelets 更适合JSF?笔者认为,主要是基于以下特性:
-
Facelets基于xml,它是组件树更自然的一种描述方式(xml天生就是一种树形结构描述语言)。
-
Facelets的模版技术,使它更适合网页开发
-
Facelets支持复合组件,并且,组件的定义方式更简单
-
Facelets的 jsfc 技术对 html 设计器更友好
-
与JSP相比,Facelets无需运行前编译,并且,Facelets 还适合对生成的组件树做cache,从而使运行期更轻量,效率更高
以下对Facelets的模版及复合组件技术做一个简单介绍。
首先,我们举一个场景:假设我们现在要做一个向导,该向导包含两个页面,分别是录入用户的姓名和其它信息,如下所示:
Dialog1:

Dialog2:

暂且不考虑 Facelets,这两个页面的代码分别如下:
dialog1.xhtml:
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
renderKitId="AJAX" xmlns:h="http://java.sun.com/jsf/html">
<w:page title="Dialog" style="font-family: Verdana; font-size: 10pt; margin: 0px; padding: 0px">
<w:form>
<layout:panelGrid columns="2" width="100%" style="padding-left: 20px">
<layout:cell colspan="1" rowspan="1" width="99%">
<div style="font-size: 11pt; font-weight: bold;">Input Name</div>
</layout:cell>
<layout:cell colspan="1" rowspan="2" style="align: right">
<img src="/articles/magic-7/html_single/images/wizard.gif" />
</layout:cell>
<layout:cell colspan="1" rowspan="2">
<div style="font-size: 10pt; font-weight: normal;">Please input your name</div>
</layout:cell>
</layout:panelGrid>
<w:separator />
<layout:panelGrid columns="2" style="height: 250px">
<layout:cell style="vertical-align: top">
<h:outputLabel value="Name:"/>
</layout:cell>
<layout:cell style="vertical-align: top">
<w:textField id="name" width="250"/>
</layout:cell>
</layout:panelGrid>
<w:separator />
<layout:panelGrid columns="5" style="align: right" width="100%">
<layout:cell style="width: 90%"> </layout:cell>
<w:button value="Back"/>
<w:button value="Next"/>
<w:button value="Finished"/>
<w:button value="Cancel"/>
</layout:panelGrid>
</w:form>
</w:page>
</f:view>
dialog2.xhtml
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
renderKitId="AJAX" xmlns:h="http://java.sun.com/jsf/html">
<w:page title="Dialog" style="font-family: Verdana; font-size: 10pt; margin: 0px; padding: 0px">
<w:form>
<layout:panelGrid columns="2" width="100%" style="padding-left: 20px">
<layout:cell colspan="1" rowspan="1" width="99%">
<div style="font-size: 11pt; font-weight: bold;">Input Others</div>
</layout:cell>
<layout:cell colspan="1" rowspan="2" style="align: right">
<img src="/articles/magic-7/html_single/images/wizard.gif" />
</layout:cell>
<layout:cell colspan="1" rowspan="2">
<div style="font-size: 10pt; font-weight: normal;">Please input your others information</div>
</layout:cell>
</layout:panelGrid>
<w:separator />
<layout:panelGrid columns="2" style="height: 250px">
<layout:cell style="vertical-align: top">
<h:outputLabel value="Others:"/>
</layout:cell>
<layout:cell style="vertical-align: top">
<w:combo id="others" width="250"/>
</layout:cell>
</layout:panelGrid>
<w:separator />
<layout:panelGrid columns="5" style="align: right" width="100%">
<layout:cell style="width: 90%"> </layout:cell>
<w:button value="Back"/>
<w:button value="Next"/>
<w:button value="Finished"/>
<w:button value="Cancel"/>
</layout:panelGrid>
</w:form>
</w:page>
</f:view>
首先非常抱歉的是,笔者对HTML/CSS并不是很懂,因此,上述的代码示例有许多可以改进的地方。但无庸置疑的是: 上述两个页面有太多的重复性代码。如果我们用纯粹的JSP/Servlet技术来开发此应用,可以用 Tiles 来消除这种重复;如果用 AOM 来开发,则可以通过 Facelets 来达到同样的目的。
针对上述示例,我们首先定义一个模版文件dialog_template.xhtml。
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
renderKitId="AJAX" xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://java.sun.com/jsf/facelets">
<w:page title="Dialog" style="font-family: Verdana; font-size: 10pt; margin: 0px; padding: 0px">
<layout:panelGrid columns="2" width="100%" style="padding-left: 20px">
<layout:cell colspan="1" rowspan="1" width="99%">
<div style="font-size: 11pt; font-weight: bold;">
<ui:insert name="title">default title</ui:insert>
</div>
</layout:cell>
<layout:cell colspan="1" rowspan="2" style="align: right">
<img src="/articles/magic-7/html_single/images/wizard.gif" />
</layout:cell>
<layout:cell colspan="1" rowspan="2">
<div style="font-size: 10pt; font-weight: normal;">
<ui:insert name="description">default description</ui:insert>
</div>
</layout:cell>
</layout:panelGrid>
<w:separator />
<ui:insert name="content">empty content</ui:insert>
<w:separator />
<layout:panelGrid columns="5" style="align: right" width="100%">
<layout:cell style="width: 90%"></layout:cell>
<w:button value="Back" />
<w:button value="Next" />
<w:button value="Finished" />
<w:button value="Cancel" />
</layout:panelGrid>
</w:page>
</f:view>
请注意,我们在上述模版文件中,分别插入三个<ui:insert/>标记,每个标记都有一个name属性,其值分别是:title、description、content。 这三个标记,本质上就相当于定义了三个“锚点”,至于具体的内容,随时可以替换。
现在,我们再来看看 dialog1 应该怎么使用这个模版。
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://java.sun.com/jsf/facelets">
This text should be skiped.
<ui:composition template="/dialog_template.xhtml">
This text should be skiped too.
<ui:define name="title">Input Name</ui:define>
<ui:define name="description">Please input your name</ui:define>
<ui:define name="content">
<layout:panelGrid columns="2" style="height: 250px">
<layout:cell style="vertical-align: top">
<h:outputLabel value="Name:" />
</layout:cell>
<layout:cell style="vertical-align: top">
<w:textField id="name" width="250" />
</layout:cell>
</layout:panelGrid>
</ui:define>
</ui:composition>
</html>
我们可以观察:首先,<ui:composition>指定使用哪个模版文件,然后通过<ui:define>对模版文件中每个可供插入的“<ui:insert>锚点”进行定义。在 上述的dialog1.xhtml文件中,我们针对模版文件(dialog_template.xhtml)的三个锚点,分别定义了相应的具体内容。在运行期,这些具体的内容, 将会被插入到模版文件中定义的锚点位置。 这里需要提醒注意的是:<ui:composition>标记和<ui:define>标记以外的所有内容都会被忽略,并且,最外面的<html>标记, 只是为了定义一个根元素,以便在这个根元素中声明命名空间,至于你是用<html>还是<f:view>,甚至是其它的乱七八糟的 标记,如<abcde>等等,其实都无所谓。笔者建议各位同学使用<html>作为根元素,一是因为<html>所属的命名空间已经被声明过了(xmlns="http://www.w3.org/1999/xhtml"), 二是因为它对设计器更友好(哪个网页设计器不认识<html/>标记呢?)。
Facelets的相关知识毕竟不是本文的重点,读者可以参考其它更加专业的文档,但为了方便对模版技术的理解,最后再把修订后的 dialog2.xhtml 页面源码附上:
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:composition template="/dialog_template.xhtml">
<ui:define name="title">Input Others</ui:define>
<ui:define name="description">Please input your others information</ui:define>
<ui:define name="content">
<layout:panelGrid columns="2" style="height: 250px">
<layout:cell style="vertical-align: top">
<h:outputLabel value="Others:" />
</layout:cell>
<layout:cell style="vertical-align: top">
<w:combo id="others" width="250" />
</layout:cell>
</layout:panelGrid>
</ui:define>
</ui:composition>
</html>
事实上,笔者认为:复合组件才是 Facelets 的精华所在,而复合组件加上AOM的模型事件,则更是完美的搭配。
回顾Dialog的例子,我们发觉:back、next、finish、cancel这四个按钮,其实经常组合在一起,在许多的场景下都可以重复使用。 那么,我们希望把它组装成一个自定义的“复合组件”,以达到代码重用的目的。
首先,我们准备一个buttons.xhtml作为该复合组件的模版:
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://java.sun.com/jsf/facelets">
<w:page>
This text above will not be displayed.
It's purpose is for design tools.
<ui:composition>
<layout:panelGrid columns="5" style="align: right" width="100%">
<layout:cell style="width: 90%" />
<w:button value="Back"action="#{onBack}" />
<w:button value="Next"action="#{onNext}" />
<w:button value="Finish"action="#{onFinish}" />
<w:button value="Cancel"action="#{onCancel}" />
</layout:panelGrid>
</ui:composition>
</w:page>
</f:view>
同样需要注意的是:上述文件中真正有用的东西是<ui:composition>标记之内的内容,至于外面的<f:view>、<w:page>等标记, 纯粹是为了让设计器更好的工作而准备的。
在buttons.xhtml中,我们定义了四个按钮,并且,分别指定它们的 action为 #{onBack}、#{onNext}、#{onFinished}、#{onCancel}。然后我们再为这个组件准备一个描述文件:buttons.taglib.xml。
<!DOCTYPE facelet-taglib PUBLIC
"-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN"
"http://java.sun.com/dtd/web-facesconfig_1_0.dtd">
<facelet-taglib>
<namespace>http://www.mycompany.com/aom</namespace>
<tag>
<tag-name>buttons</tag-name>
<source>buttons.xhtml</source>
</tag>
</facelet-taglib>
在上面这个文件中,我们声明了一个新的tag,其名称为"buttons",它所属于的命名空间是:"http://www.mycompany.com/aom",并且,这个自定义组件的内容来源自 source属性指定的模版文件buttons.xhtml。
怎么告诉Facelets我们新做了一个复合组件呢?在 web.xml 中增加以下配置:
<context-param>
<param-name>facelets.LIBRARIES</param-name>
<param-value>/buttons.taglib.xml</param-value>
</context-param>
如果你有多个复合组件的描述文件,就以分号隔开。至此,一个复合组件就准备完毕了,我们来看如何使用这个复合组件。我们准备一个 test_buttons.xhtml来调用新做的这个复合组件,如下所示:
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
xmlns:my="http://www.mycompany.com/aom" renderKitId="AJAX"
xmlns:ui="http://java.sun.com/jsf/facelets">
<w:page title="Test buttons component">
<w:form>
<h2>Test the buttons component.</h2>
<p/>
<my:buttons onBack="#{TestButtonsBean.onBack}" onNext="#{TestButtonsBean.onNext}"
onFinish="#{TestButtonsBean.onFinish}" onCancel="#{TestButtonsBean.onCancel}"/>
</w:form>
</w:page>
</f:view>
其中,TestButtonsBean如下所示:
@ManagedBean(name="TestButtonsBean", scope=ManagedBeanScope.SESSION)
public class TestButtonsBean {
public String onBack() {
System.out.println("onBack");
return null;
}
public String onNext() {
System.out.println("onNext");
return null;
}
public String onFinish() {
System.out.println("onFinish");
return null;
}
public String onCancel() {
System.out.println("onCancel");
return null;
}
}
程序运行效果如下,并且,点击不同的按钮,后台的 TestButtonsBean也可以正常响应。

通过上例可以看出:在 Facelets 下完成一个复合组件是比较简单的一件事情,
但有的同学要问了:这种在 <my:buttons onBack="#{testButtonsBean.onBack}"> 中直接引入 EL 表达式的风格,好像不是那么 IoVC ,有没有更好的解决方案?
答案是:“有。通过模型事件,则是一个更加优雅的解决方案。”
我们将模版文件buttons.xhtml修订如下:
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout" renderKitId="AJAX"
xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://java.sun.com/jsf/facelets">
<w:page>
This text above will not be displayed.
It's purpose is for design tools.
<ui:composition>
<layout:panelGrid columns="5" style="align: right" width="100%">
<layout:cell style="width: 90%" />
<w:button value="Back" action="#{buttonsBean.onBack}" />
<w:button value="Next" action="#{buttonsBean.onNext}" />
<w:button value="Finish" action="#{buttonsBean.onFinish}" />
<w:button value="Cancel" action="#{buttonsBean.onCancel}" />
</layout:panelGrid>
</ui:composition>
</w:page>
</f:view>
请注意,我们在模版文件中,直接将上述 Button 的 action 绑定在了一个 “buttonsBean” 上,由于这个 “buttonsBean” 是无状态的,所以,它的生命周期声明为none,以下是示例代码:
@ManagedBean(name = "buttonsBean", scope = ManagedBeanScope.NONE)
public class ButtonsBean {
private EventBroadcaster event = EventBroadcaster.getInstance();
public Object onBack() {
event.broadcast(this, "com.mycompany.buttons.onBack");
return null;
}
public Object onNext() {
event.broadcast(this, "com.mycompany.buttons.onNext");
return null;
}
public Object onFinish() {
event.broadcast(this, "com.mycompany.buttons.onFinish");
return null;
}
public Object onCancel() {
event.broadcast(this, "com.mycompany.buttons.onCancel");
return null;
}
}
换言之,当点击不同的按钮时,buttonsBean 会发送不同的模型事件,事件类型以不同的字符串区分。
然后,我们再修订一下test_buttons.xhtml,看上述这个复合组件,是如何使用的:
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
xmlns:my="http://www.mycompany.com/aom" renderKitId="AJAX"
xmlns:ui="http://java.sun.com/jsf/facelets">
<w:page title="Test buttons component">
<w:form>
<h2>Test the buttons component.</h2>
<p/>
<my:buttons/>
</w:form>
</w:page>
</f:view>
可以看到,此处,我们只是声明一个<my:buttons/>,并没有将其绑定任何EL表达式。那么,如果我要响应 onBack 按钮点击事件该怎么办呢?很简单,只要有一个 EventListener,监听 "com.mycompany.buttons.onBack"事件即可。以下是 TestButtonsBean 的示例代码:
@ManagedBean(name="TestButtonsBean", scope=ManagedBeanScope.SESSION)
public class TestButtons {
@EventListener("com.mycompany.buttons.onBack")
public void onBack() {
System.out.println("onBack event");
}
}
通过上例可以看出:通过模型事件,我们能够设计一个更加优雅的复合组件。
在我们的Web应用开发过程中,会出现多种角色,但无疑,有两种角色是最常见的:网页设计人员及开发工程师,或者换更通俗的说法就是美工和程序员。 但在美工与程序员之间的协作过程中,经常会出现的一种场景是:美工用如Dreamweaver之类的工具画出来的页面,程序员看不懂; 而程序员如果对页面进行了修订,加上诸如<%%>之类的代码片断,美工又无法理解。如何解决这种矛盾?
Facelets中提出了 jsfc 的技术,能够部分解决此问题。所谓 jsfc ,简而言之就是:在Facelets构建组件树时,把一个 tag 根据 jsfc 属性所指定的值,转换成另一个tag。 举个简单的例子:<input type="text" jsfc="h:inputText" value="#{hello.world}" /> 能够在构建组件树时被转换成:<h:inputText value="#{hello.world}"/>,甚至于,你放一个 <img src="/articles/magic-7/html_single/images/button.jpg" jsfc="w:textField" value="hello"/>,Facelets 依然能够将其转换成:<w:textField value="hello"/>。咦,那个 src 属性呢?src属性<w:textField/>不认识,会自动将其忽略。
好了,我们不妨开动一下思路:在做一个Web应用的项目时,程序员事先给美工准备一系列图片,什么 TextField、Button、Combo、Tree、DataGird等等,把项目中能够用到的控件全部以图片形式准备好,并事先给美工进行培训,日后美工在设置页面时, 如果需要要在页面中放置控件,只需插入替代的图片,再设置一下正确的 jsfc 属性即可。
听上去很美,但是,等等:光放控件不行啊,还要对控件进行一系列的属性设置,什么 value="#{a.b}",action="#{b.c}"之类的,美工哪懂这些啊?
是,你说的对,jsfc 只能解决部分问题,但咱们不是还有IoVC吗!美工在设置完 jsfc属性时,只需要再设置一个唯一的id即可。
所以说:jsfc + IoVC,能够使美工与程序员完全解耦。美工做完的页面,程序员可以拿过来直接用,基本无需再做大的更改。
需要培训
你告诉美工:如果要用这个组件,就加上jsfc=xxx的属性。这种学习成本是很低的,总比要教美工写<%%>代码片断要容易许多吧。
如何在一个应用中的不同地方使用文中的<my:buttons/>
在复合组件
<ui:insert name="title">default title</ui:insert>
<ui:insert name="description">default description</ui:insert>
<ui:insert name="content">empty content</ui:insert>
<ui:define name="content">
美工好像也不懂jsfc呀?
美工好像也不懂jsfc呀?他们哪知道jsfc="w:textField"之类的东西呀!