Facelets 非常适合 JSF

最后,专为 JSF 设计的视图技术!

试图把 JSF 和 JSP 结合起来就像试图要把脚硬塞进手套一样:可能做得到,但是只是更好的解决办法出现之前的一个权宜之计。在这篇文章中,JSF 的热心支持者 Rick Hightower 介绍了关于 Facelets 他最喜欢的内容:容易的 HTML 样式的模板化和可重用的复合组件。

Richard Hightower (rhightower@arc-mind.com), 开发人员, ArcMind Inc.

Rick HightowerArcMind Inc 的首席技术官,这是家培训公司,专攻 JSF、Spring 和 Hibernate。他是流行的 Java Tools for Extreme Programming 一书的合著者,该书介绍的是把极限编程应用到 J2EE 开发,他还是 Professional Struts 的合著者。



2006 年 2 月 21 日

由于最近在 Java™ 服务器外观(JSF)项目上工作,我很有幸第一次使用了 Facelets。关于 Facelets,我最喜欢的是它让我可以创建可重用的复合组件。能够拿出一个页面(例如 JSP)并把它变成组件,对于我的 JSF 开发来说真是莫大的好处。我的结论是什么?如果不用 Facelets,那么就无法得到能从 JSF 获得的最大收获。

JSF 和 Java 服务器页面技术之间的不匹配,是 JSF 开发中的一个严重问题。问题是如何把 JSP 的动态内容集成到 JSF 基于组件的模型中。JSP 非常重视生成动态内容输出,而 JSF 需要 JSP 来协调组件模型的构建。因为这个任务超出了 JSP 原来的目的,所以产生了距离。

大多数 JSF 开发人员只是学会了一事一议地解决这类问题,但是这就像在锤子上放一个枕头,最终还会掉下来打伤脑袋。Facelets 是更加全面的解决方案:专为 JSF 组件模型度身定制的模板化语言。

Facelets 有以下吸引人的特性:

  • 模板化(像 Tiles)
  • 复合组件
  • 定制的逻辑标记
  • 表达式语言
  • 对设计师友好的页面开发
  • 创建组件库

这些特性比我想像的要更相关和统一。在这篇文章中,我讨论前两个:模板化和复合组件。我使用的 Web 应用程序基于为我的针对怀疑者的 JSF 系列开发的一个应用程序,我把它更新成使用 Facelets 视图而不是 Tiles。在进一步阅读之前,应当 下载示例代码。如果要随着讨论一起操作,还需要 安装 Facelets

Facelets 概述

对于 Facelets 可能会做的最大一个错误假设,就是它只是 Tiles 的替代品。Facelets 远不止如此:它是思考 JSF 的新方式。

选择标记

虽然多数开发人员把 Facelets 用于 XHTML,实际上这个框架并不在意使用什么标记:它与 XUL (XULFaces)兼容, Kito Mann 已经用它为 JSF 中心提供 RSS。

JSP 是种生成 servlet 的模板化语言。JSP 的主体与 servlet 的 doGet()doPost() 方法等价(也就是说,成为 jspService() 方法)。JSF 定制标记(例如 f:viewh:form)只是调用 JSF 组件来呈现它们自己的当前状态。JSF 组件模型的生命周期独立于 JSP 生成的 servlet 的生命周期。这种独立性就是混淆的来源。

与 JSP 不同,Facelets 这个模板化语言,从构建之初,就考虑了 JSF 的组件生命周期。使用 Facelets,生成的模板会构建组件树,而不是 servlet。这就允许更好的重用,因为可以把组件组合成另一个组件。

Facelets 减少了编写定制标记才能使用 JSF 的需求。Facelets 本身就可以使用 JSF 定制组件。沟通 JSF 和 Facelets 只需要很少的特殊编码:要做的全部工作就是在 Facelet 标记库文件中声明 JSF 组件。在 Facelets 模板化语言中可以直接使用 JSF 组件,不用任何额外的开发。

Facelets 模板框架

在提供针对组件构建设计的模板框架方面,Facelets 与 Tapestry (请参阅 参考资料)类似。但是,对于具有 JSP 背景的我们来说,Facelets 看起来比 Tapestry 友好得多。它允许使用熟悉的 JSTL 样式的标记和 JSTL/JSF/JSP 样式的表达式语言。大大降低的学习曲线意味着可以更加迅速地开始开发。

Facelets 和 Tapestry

Facelets 与 Tapestry 很相似,可以相互比较。实际上,Tapestry 刚出现的时候,大大领先于它的时代,而 Facelets 确实借鉴了它的一些想法。但是,如果只把 Facelets 当成 JSF 版本的 Tapestry,那就错了。这两项技术是不同的。要了解关于 Tapestry 的更多内容,请参阅 Brett McLaughlin 两部分的系列 “In tune with Tapestry”。

Facelets 允许定义能够直接包含进页面或者容易地添加到 Facelet 标记库的组件集。实际上让人高兴的是在 Facelets 中定义定制标记(复合组件和类似 JSP 定制标记的标记)的迅速。使用这些组件集,Facelets 还允许定义站点模板(和更小的模板)。这与使用 Tiles 很相似,但是少了定义文件。也可以在定制 JSF 组件内部使用 Facelets,因为 Facelets API 提供了可以容易地与 JSF 组件集成的接口。

从 Tiles 到 Facelets

如前所述,在这里使用的示例 Web 应用程序基于为我的 针对怀疑者的 JSF 系列创建的示例。它为一家在线 CD 店管理库存,创建、读取、更新和删除(CRUD)清单。它包含一个表单,让用户向系统输入新 CD,有一个单选按钮列表,允许用户选择音乐分类。当用户选择了一个分类时,就触发某些 JavaScript 立即把表单提交回服务器。应用程序还包含一个 CD 清单,用户可以根据标题或艺术家对清单中的 CD 排序。图 1 是应用程序类的 UML 图表:

图 1. 在线 CD 商店示例的类图
示例应用程序类

图 2 提供了商店的 CD 清单页面:

图 2. 在线 CD 商店的清单页面
CD 清单

原来的应用程序从 Tiles 得到视图支持,现在我将用 Facelets 构建视图。我先从用 Facelets 替换示例中的 Tiles 支持开始,然后编写复合组件。在开始之前,需要已经安装了 Facelets。

安装 Facelets

安装 Facelets 的步骤很容易。请注意,我假设已经下载并安装了 示例应用程序

  1. 下载 Facelets 发行包 并解压缩。
  2. 把 jsf-facelets.jar 拷贝到 WEB-INF/lib 目录(在应用程序部署时,它最终必须放在 WEB-INF/lib 目录中)。
  3. 把 Facelet 初始化参数添加到 web.xml 文件中。
  4. 把 FaceletViewHandler 添加到 faces-config.xml 文件中。

步骤 1 和 2 比较基本。我将详细介绍其他两个步骤。

添加初始化参数

这一步假设已经安装了工作正常的 JSF 应用程序(例如 在线 CD 商店示例),然后编辑现有的 web.xml 页面,添加以下参数:

<context-param>
	<param-name>javax.faces.DEFAULT_SUFFIX</param-name>
	<param-value>.xhtml</param-value>
</context-param>

这告诉 JSF 采用 xhtml 前缀,Facelet 渲染器能够解释这个前缀。

Facelets 有许多参数,请参阅 参考资料 获得完整清单。如果示例有问题,请参考 DEVELOPMENT init 参数,它适合调试。把 REFRESH_PERIOD 参数设置为 low 在开发期间会有帮助。

添加 FaceletViewHandler

要让 Facelets 模板生效,需要把 Facelets 视图处理器告诉 JSF。JSF ViewHandler 是个插件,为不同的响应生成技术(包括 Facelets)处理 JSF 请求处理生命周期的 “渲染器响应和恢复视图” 阶段。(任何认为 JSF 不能扩展的人都是被误导了!)通过添加以下视图处理器到 faces-config.xml 中,就把 Facelets 插进了 JSF 中:

  <application>
    <locale-config>
      <default-locale>en</default-locale>
    </locale-config>
	<view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
  </application>

用 Facelets 进行模板化

首先介绍 Facelets 模板化框架,因为它相对容易理解。创建和使用 Facelets 模板的步骤如下:

  1. 创建 layout.xhtml 页面。
  2. 定义 Facelet 的命名空间,导入对 Facelets 的使用。
  3. ui:insert 标记定义页面的逻辑区域。
  4. 用纯文本和 ui:include 标记定义合理的默认值。

我要逐步介绍这些步骤,用在线 CD 商店清单页面作为我的布局示例。

步骤 1. 创建 layout.xhtml 页面

layout.xhtml 页面就是一个一般的 XHTML 文本文件,使用了以下文档类型声明:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
</html>

不要求进一步细节!

步骤 2. 定义 Facelets 的命名空间

为了用 Facelets 标记进行模板化,需要用 XML 命名空间像下面这样导入它们:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets">
...

请注意 ui 命名空间的定义。

步骤 3. 用 ui:insert 标记定义页面的逻辑区域

下面,定义布局的逻辑区域,例如页面标题、小标题、导航、内容等等。下面是定义页面标题的示例:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets">
<head>
  <title><ui:insert name="title">Default title</ui:insert></title>
  <link rel="stylesheet" type="text/css" href="./css/main.css"/>
</head>
...

请注意使用 ui:insert 标记定义了标题的逻辑区域。ui:insert 元素内的文本 “Default title” 定义了模板用户不传递标题时显示的文本。也可以像下面这样编写上面的内容:

<title>#{title}</title>

步骤 4. 用纯文本和 ui:includes 定义默认值

可以传递更多的纯文本作为默认值。例如,请研究 layout.xhtml 中的以下代码片段:

<div id="header">
    <ui:insert name="header">
    	<ui:include src="header.xhtml"/>
    </ui:insert>
</div>

在这里,我用了 ui:insert 标记定义逻辑区域,用 ui:include 标记插入默认值。默认情况下,使用布局的页面会采用 header.xhtml 的内容作为标题文本,但是因为标题是 ui:insert 定义的逻辑区域,所以用这个模板也能传递不同的标题。对于拥有前端(例如,带有购物车的目录)和后端管理(例如添加新产品)的应用程序,后端站点在标题或导航上可能不同的链接。ui:include 标记可以容易地用新标题换掉默认标题。

清单 1 显示了示例应用程序的清单页面 list.xhtml 的完整代码:

清单 1. 完整的 list.xhtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets">
<head>
  <title><ui:insert name="title">Default title</ui:insert></title>
  <link rel="stylesheet" type="text/css" href="./css/main.css"/>
</head>
<body>
<div id="header">
    <ui:insert name="header">
    	<ui:include src="header.xhtml"/>
    </ui:insert>
</div>
<div id="left">
  <ui:insert name="navigation" >
    <ui:include src="navigation.xhtml"/>
  </ui:insert>
</div>
<div id="center">
  <br />
  <span class="titleText"> <ui:insert name="title" /> </span>
  <hr />
  <ui:insert name="content">
  	<div>
    <ui:include src="content.xhtml"/>  
    </div>
  </ui:insert>
</div>
<div id="right">
  <ui:insert name="news">
    <ui:include src="news.xhtml"/>
  </ui:insert>
</div>
<div id="footer">
  <ui:insert name="footer">
    <ui:include src="footer.xhtml"/>  
  </ui:insert>
</div>
</body>
</html>

现在已经知道了如何定义布局,我将介绍如何使用布局!


使用 Facelets 模板

为了调用模板,要使用 ui:composition 标记。为了把参数传递给模板,要使用 ui:define 标记,它是 ui:composition 标记的子元素。在清单 2 中,我调用了在线 CD 商店示例的布局页面:

清单 2. 调用布局页面
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
<ui:composition template="/WEB-INF/layout/layout.xhtml">
  <ui:define name="title">CD form</ui:define>
  <ui:define name="content">
	<!-- use the form tag to set up this form -->
	<h:form id="cdForm">
		...
		...
...
	</h:form>
  </ui:define>
</ui:composition>
</html>

请注意在上面的调用中包含了以下命名空间:

  • xmlns:h="http://java.sun.com/jsf/html"
  • xmlns:f="http://java.sun.com/jsf/core"

有了 Facelets,就不必依赖 JSF 标记库,所以要使用核心和 HTML JSF 组件,必须通过以上命名空间导入它们。

html 标记的使用看起来可能有些奇怪。毕竟,清单 2 所示的布局页面要调用的模板已经有了一个 html 标记;所以这是不是意味着会得到两个 html 标记?如果真的这样,那么在 ui:composition 标记之外的内容全部被忽略,所以 html 标记所做的只是让 HTML 编辑器能够看到 HTML 片段。它不会影响运行时行为。

位置是一切

当页面调用布局模板时,只需指定模板的位置,如下所示:

<ui:composition template="/WEB-INF/layout/layout.xhtml">

这个标记调用清单 1 所示的模板,所以我要做的全部工作就是把参数传递给模板。然后,在复合标记内部,可以传递像标题这样的简单文本:

  <ui:define name="title">CD form</ui:define>

或者传递整个组件树:

  <ui:define name="content">
	<!-- use the form tag to setup this form -->
	<h:form id="cdForm">
		...
		...
...
	</h:form>
  </ui:define>

请注意,在我定义和传递的许多逻辑区域中,cdForm.xhtml 只传递两个:内容和标题。

Tiles 与 Facelets

这篇文章带了三个示例:第一个使用 Tiles,第二个使用 Facelets,第三个使用复合组件。之所以包含 Tiles 示例是为了可以比较和对比两个框架中不同的模板化技术。请看看它,并看看自己有什么想法!

复合组件

如果只用 Facelets 定义和使用模板,那么可能会有点失败。虽然 Facelets 的模板化特性完整而且丰富,但是它没有 Tiles 之类的框架那么多的特性,后者还擅长定义默认值、相关模板的层次结构以及类似的东西。

但模板化不是 Facelets 真正出色的地方:Facelets 把它的精华放在复合组件上。(有趣的是,复合组件也给 Facelets 模板化带来了一些好处;例如,在 Facelets 中可以舍弃 f:verbatim 标记和各种 h:outputText 标记,因为所有的东西都被当成组件树中的组件。关于这方面的更多内容稍后介绍。)

对于这篇文章余下的部分,我将重点放在创建和使用复合组件的步骤上。但在开始之前,先要确保能够清楚地理解是什么让这些方便的小代码段这么棒。

破坏 DRY 原则

您是否曾经编写过像清单 3 所示的代码片段?

清单 3. 复合组件之前的生活
<h:dataTable id="items" value="#{CDManagerBean.cds}" var="cd"
	rowClasses="oddRow, evenRow" headerClass="tableHeader">
<!--  Title -->
<h:column>
	<f:facet name="header">
		<h:panelGroup>
			<h:outputText value="Title" />
				
                 <f:verbatim>[</f:verbatim>
			<h:commandLink styleClass="smallLink"
                              action="#{CDManagerBean.sort}">
				<h:outputText id="ascTitle" value="asc" />
				<f:param name="by" value="title"/>
				<f:param name="order" value="asc"/>
			</h:commandLink>
			<h:outputText value="," />
			<!-- Sort descending -->
			<h:commandLink styleClass="smallLink"
                         action="#{CDManagerBean.sort}">
				<h:outputText id="decTitle" value="dec" />
				<f:param name="by" value="title"/>
				<f:param name="order" value="dec"/>
			</h:commandLink>
			     <f:verbatim>]</f:verbatim>
		</h:panelGroup>
	</f:facet>
	<h:outputText value="#{cd.title}" />
</h:column>
<!--  Artist -->
<h:column>
	<f:facet name="header">
		<h:panelGroup>
			<h:outputText value="Artist" />
			   <f:verbatim>[</f:verbatim>
		      <h:commandLink styleClass="smallLink"
                           action="#{CDManagerBean.sort}">
			     <h:outputText id="ascArtist" value="asc" />
				<f:param name="by" value="artist"/>
				<f:param name="order" value="asc"/>
			   </h:commandLink>
			<h:outputText value="," />
					<!-- Sort descending -->
			<h:commandLink styleClass="smallLink"
  				           action="#{CDManagerBean.sort}">
				<h:outputText id="decArtist" value="dec" />
				<f:param name="by" value="artist"/>
				<f:param name="order" value="dec"/>
			</h:commandLink>
			     <f:verbatim>]</f:verbatim>
		</h:panelGroup>
	</f:facet>
	<h:outputText value="#{cd.artist}" />
</h:column>

这段来自 listing.xhtml 的代码为示例应用程序的清单页面生成列标题和升序/降序排列链接。请注意,必须在多个地方重复代码,才能输出多列。(在上面的示例中您还会注意到,我在 ${..}#{..} 之间切换;这可能让人迷惑,但它们做的是同样的事!)

所有这些渲染标题列和艺术家列的重复代码都破坏了 DRY 原则 —— 即,不要重复自己。那么您说,这里错在哪儿呢?假设在清单中平均有 5 列,应用程序中有 20 个不同的清单。使用清单 3 的方法,就不得不重复这 35 行代码 100 次,合计 3,500 行代码!维护所有这些代码会是种痛苦,但是如果决定修改清单的表示或添加一种通用的清单过滤方式,会怎么样?需要更多、更多的工作。

现在拿清单 3 和这个代码比较:

清单 4. 创建字段的新方式
<h:dataTable id="items" value="#{CDManagerBean.cds}" var="cd"
		rowClasses="oddRow, evenRow" headerClass="tableHeader">
			
 <a:column entity="${cd}" fieldName="title" backingBean="${CDManagerBean}"/>
 <a:column entity="${cd}" fieldName="artist" backingBean="${CDManagerBean}"/>

看起来好像我只用 4 行代码就替代了 70 行或更多行代码!可以猜出,a:column 是个复合组件。在 Facelets 中,可以容易地定义这样的组件,如清单 5 所示:

清单 5. column.xhtml 渲染带有排序链接的列
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:z="http://www.qualcomm.com/jsf/core"
      xmlns:c="http://java.sun.com/jstl/core"
      xmlns:fn="http://java.sun.com/jsp/jstl/functions">
THIS TEXT WILL BE REMOVED
<ui:composition>
	<!--  The label attribute is optional. Generate it if it is missing. -->
	<c:if test="${empty label}">
	    <c:set var="label" value="${fieldName}" />
	</c:if>
	<!--  The sort attribute is optional. Set it to true if it is missing. -->
	<c:if test="${empty sort}">
	    <c:set var="sort" value="${true}" />
	</c:if>
	<h:column>
	  <f:facet name="header">
	    <h:panelGroup>
	      ${label} 
	      <c:if test="${sort}">
	          [
	        <h:commandLink styleClass="smallLink"
	      action="#{backingBean.sort}">
	          <h:outputText value="asc" />
	          <f:param name="by" 
              value="${fieldName}"/>
	          <f:param name="order" value="asc"/>
	        </h:commandLink>
					,
	        <!-- Sort descending -->
	        <h:commandLink styleClass="smallLink"
              action="#{backingBean.sort}">	
	          <h:outputText value="asc" />
	          <f:param name="by"                
              value="${fieldName}"/>
	          <f:param name="order" value="dec"/>
	        </h:commandLink>
					]
	      </c:if>			
	    </h:panelGroup>
	  </f:facet>
	  <!--  Display the field name -->
	  <h:outputText value="${entity[fieldName]}"/>
        </h:column>
</ui:composition>
THIS TEXT WILL BE REMOVED AS WELL
</html>

要点

在进入更高级的示例之前,我想把您的注意力引到几件事上。首先,请注意在清单 5 中,我如何用一种通用方式引用值绑定:

<h:outputText value="${entity[fieldName]}"/>

第二,当调用这个复合组件时,我把 entityfieldName 作为属性传递,如下所示:

<a:column entity="${cd}" fieldName="title" backingBean="${CDManagerBean}"/>

Facelets 使用的 EL 规范允许用圆点(.)表示法或较少使用的映射表示法引用字段。例如,如果像上面那样调用,那么 ${entity[fieldName]} 等价于 CDManager.title。还请注意,我不需要 f:verbatim 标记或辅助的 h:outputText。对于您编写的任何 Facelets 页面,都可以这样。Facelets 知道 JSF 组件树,它的唯一目的就是构建这个组件树。这是使用 Facelets 与使用 JSP 和 Tiles 相比的另一个优势。

编写完成之后,就可以在其他许多地方使用 column.xhtml 复合组件。作为一个通用规则:如果正在破坏 DRY 原则,那么请考虑改用复合组件。


创建组件

现在通过 column.xhtml 示例已经快速查看了复合组件。下面一步一步地介绍创建复合组件的过程。以下是创建复合组件的步骤:

  1. 创建 Facelets 标记库。
  2. 在 web.xml 中声明标记库。
  3. 用命名空间导入标记文件。

步骤 1. 创建 Facelets 标记文件

标记文件 是符合 facelet_taglib_1_0.dtd 的文件。在概念上它与 JSP 中的 TLD 文件相似。清单 6 是一个示例标记库文件:

清单 6. 标记库文件 —— arcmind.taglib.xml
<?xml version="1.0"?>
<!DOCTYPE facelet-taglib PUBLIC
  "-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN"
  "facelet-taglib_1_0.dtd">
<facelet-taglib>
      <namespace>http://www.arc-mind.com/jsf</namespace>
	<tag>
		<tag-name>field</tag-name>
		<source>field.xhtml</source>
	</tag>
	<tag>
		<tag-name>column</tag-name>
		<source>column.xhtml</source>
	</tag>
	<tag>
		<tag-name>columnCommand</tag-name>
		<source>columnCommand.xhtml</source>
	</tag>
</facelet-taglib>

arcmind.taglib.xml 文件声明了三个标记:fieldcolumn(已经看过这个!)和 columnCommand。需要做的只是用 tag-name 指定标记的名称和实现文件的位置。实现文件的名称是相对的。可以在示例 Web 应用程序下的 WEB-INF\facelets\tags 文件中找到所有这些代码,包括 DTD。

请一定注意在上面的标记元素之前声明的 namespace 元素:稍后需要通过它在其他 Facelets 页面中使用这个标记库。

步骤 2. 在 web.xml 中声明标记库

有了一个标记库是很好,但是要让它有用,还必须把它的存在告诉 Facelets。在 web.xml 文件中用 facelets.LIBRARIES init 参数做这件事,如下所示:

<context-param>
	<param-name>facelets.LIBRARIES</param-name>
	<param-value>
		/WEB-INF/facelets/tags/arcmind.taglib.xml
	</param-value>
</context-param>

facelets.LIBRARIES 以分号分隔的列表形式传递,就可以想定义多少就定义多少标记文件。

步骤 3. 用命名空间导入标记文件

创建了标记文件并在 Facelets 标记库中定义了它之后,就可以使用它了。标记文件的使用要求把它声明为 XML 命名空间,如下所示:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:a="http://www.arc-mind.com/jsf">
...
...
<a:column entity="${cd}" fieldName="title"  
  backingBean="${CDManagerBean}"/>
<a:column entity="${cd}" fieldName="artist" 
  backingBean="${CDManagerBean}"/>
<a:column entity="${cd}" fieldName="price" 
  backingBean="${CDManagerBean}" sort="${false}"/>
<a:columnCommand label="Edit" action="editCD"
              backingBean="${CDManagerBean}"/>

请注意命名空间的定义如下所示:

xmlns:a="http://www.arc-mind.com/jsf"

命名空间的值与前面步骤 1 中标记库中声明的命名空间元素一样。


高级技巧

上面只介绍了复合组件的基础知识。用我目前为止介绍的内容能够创建可重用组件。在我自己使用 Facelets 时,发现了一些可以让复合组件更有用的小技巧,而且在某些情况下能够解决一些小问题。例如,请考虑来自 cdForm.xhtml 模板的以下代码片段:

清单 7. cdForm.xhtml 的片段
<h:form id="cdForm">
  <h:inputHidden id="cdid" value="#{CDManagerBean.cd.id}" />
  <h:panelGrid id="formGrid" columns="3" rowClasses="row1, row2">
	<!-- Title                                   -->
	<h:outputLabel id="titleLabel" for="title" styleClass="label"
                     value="Title" />
	<h:inputText id="title" value="#{CDManagerBean.cd.title}"
				     required="true" />
	<h:message id="titleMessage" for="title" styleClass="errorText"/>
	<!-- Artist                                   -->
	<h:outputLabel  id="artistLabel" for="artist" styleClass="label" 
                     value="Artist" />
	<h:inputText id="artist" value="#{CDManagerBean.cd.artist}"
				     required="true" />
	<h:message id="titleMessage" for="artist"  
                     styleClass="errorText"/>
	<!-- Price                                   -->
	<h:outputLabel  id="priceLabel" for="price" styleClass="label" value="Price" />
	<h:inputText id="price" value="#{CDManagerBean.cd.price}"
				     required="true">
		<f:validateDoubleRange minimum="15.0" maximum="100.0" />
	</h:inputText>
	<h:message id="priceMessage" for="price" styleClass="errorText"/>

以上页面在概念上与 清单 3 类似,但它为 Facelets 和一个字段复合组件留出了空间,可以避免重复代码。基于这个代码,应当可以容易地创建显示字段的复合组件,但是有一个小麻烦。不知道您看到没有?请看表单的 price 字段:它包含一个验证器。

现在,如何把验证器传递给复合组件?


传递子元素

这里有一个关于 Facelets 的小秘密:复合组件基本上也是一类模板。所以,可以用 ui:define 标记传递模板参数,带上具体的 ui:insert,或者也可以把体作为默认 ui:insert 传递。

清单 8 就是字段组件的这样一种实现(field.xhtml):

清单 8. field.xhtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:z="http://www.qualcomm.com/jsf/core"
      xmlns:c="http://java.sun.com/jstl/core"
      xmlns:fn="http://java.sun.com/jsp/jstl/functions"
      xmlns:t="http://myfaces.apache.org/tomahawk">
THIS TEXT WILL BE REMOVED
<ui:composition>
	      <!--  The label is optional. 
                Generate it if it is missing. -->
		<c:if test="${empty label}">
			<c:set var="label" value="${fieldName}" />
		</c:if>
	 
	 	<!-- The required attribute is optional, 
                 initialize it to true if not found. -->
		<c:if test="${empty required}">
			<c:set var="required" value="true" />
		</c:if>
	
		<h:outputLabel id="${fieldName}Label" 
                      value="${label}" for="#{fieldName}" />			
		<h:inputText id="#{fieldName}" value="#{entity[fieldName]}" 
			             required="${required}">
			  <ui:insert />
		</h:inputText>
		<!--  Display any error message that are found -->
		<h:message id="${fieldName}Message" 
			style="color: red; text-decoration: overline" 
			for="#{fieldName}" />
</ui:composition>
THIS TEXT WILL BE REMOVED AS WELL
</html>

目前为止,清单 8 的工作基本上是不出所料。请注意 h:inputText 内部未命名 ui:insert 标记的使用。理解了它之后,就可以像清单 9 所示那样使用这个复合组件:

清单 9. 字段标记复合组件
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:a="http://www.arc-mind.com/jsf">
...
<h:form id="cdForm">
	<!-- Title, Artist, Price -->
	<a:field fieldName="title" entity="#{CDManagerBean.cd}" />
	<a:field fieldName="artist" entity="#{CDManagerBean.cd}" />
	<a:field fieldName="price" entity="#{CDManagerBean.cd}" >
		<f:validateDoubleRange minimum="15.0" maximum="100.0" />
	</a:field>
...

price 的字段标记被传递给验证器,作为匿名插入。因为其他字段没有定义体,所以匿名插入对于默认值没有影响。


传递动作

在想传递动作绑定来创建像工具栏或导航列表这样的元素时,问题就是使用标准的表达式语言,不能传递,但是有方法可以做到!使用从对象中引用字段的相同方式,可以引用对象中的方法。所以,要创建可以创建动作绑定的组件,可以像下面这样做(来自 columnCommand.xhtml):

<h:commandLink id="#{action}" value="#{label}"  
                              action="#{backingBean[action]}"/>

请研究动作属性的 value。请注意,我使用与前面从实体引用字段相同的方式,访问到了方法。可以用以下语法调用这个组件:

<a:columnCommand label="Edit" action="editCD" 
backingBean="${CDManagerBean}"/>

这个调用把 CDManagerBeaneditCD() 方法绑定到链接。清单 10 显示了 columnCommand.xhtml 的完整清单:

清单 10. columnCommand.xhtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:z="http://www.qualcomm.com/jsf/core"
      xmlns:c="http://java.sun.com/jstl/core">
THIS TEXT WILL BE REMOVED
<ui:composition>
	<!--  The label is optional. Generate it if it is missing. -->
	<c:if test="${empty label}">
			<c:set var="label" value="${action}" />
	</c:if>
	
	<h:column>
		<f:facet name="header">
			<h:panelGroup>
				<h:outputText value="#{label}" />
			</h:panelGroup>
		</f:facet>
		<h:commandLink id="#{action}" value="#{label}" 
                                     action="#{backingBean[action]}"/>
	</h:column>
		
</ui:composition>
THIS TEXT WILL BE REMOVED AS WELL
</html>

Facelets 的不足

我已经清楚地演示了使用 Facelets 的好处:即组件复合和模板框架,它的核心 是组件,而不是 Servlets 输出。但是采用 Facelets 也有些不足。其中之一就是,对 Facelets 的 IDE 支持极少。只有一个 Eclipse IDE 实现完全支持 Facelets(商业版的实现,请参阅 参考资料),而且看起来还不支持代码补足。

而且也没对 Facelets 调试的 IDE 支持(也就是说,设置断点之类的东西)。要想调试,需要阅读 Facelets 手册,打开 JDK 1.4 样式的日志,根据开发情况设置它的 init 参数。

在有利方面来说,我发现使用 Facelets API 非常自然和直观。调试在开始的时候有些怪异,但是后来就适应了。随 Facelets 发行包提供的演示应用程序没有定制标记或功能的示例,但是核心项目代码中有,所以请用核心项目代码作为指导。

如果要使用新的 JSF 组件库,必须有公开这个库的 Facelets 标记库。有些主要的组件库(例如 Oracle 和 Tomahawk)的标记库存在,但是即使这些也需要调整。我必须调整 Tomahawk 标记库才能在应用程序中得到 Tomahawk 的日历组件。虽然编写导出组件的标记库文件比较容易,但是也是件麻烦事。如果想使用新的定制组件库,就必须编写标记库文件。

因为在其他实现中的问题,Facelets 看起来只能用于 MyFaces 1.1.1 和 Sun 1.2 JSF 的参考实现(Sun 的 JSF RI 1.2 还没有正式发布)。不能把 Facelets 用于 1.1 RI。虽然可以把 MyFaces 用于 IBM WebSphere,但不能把 Facelets 用于 IBM 的实现。(如果使用最新版本的 Facelets,必须使用最新构建的 MyFaces 1.1.2,它现在还没推出。)

还要注意的是,MyFaces 1.1 和 JSF RI 1.2 的底层机制不同。尽管如此,Facelets 试图把这两者的实现保持为它们当前的形式(MyFaces 1.1.2 和 JSF RI 1.2),这看起来解释了花在 Facelets 上的大量时间。如果双方更团结协调一点,让 Facelets 在两个环境中做同样的事上少花些时间,就可以把更多时间花在改进 Facelets 上。


结束语

虽然有些缺点,我还是强烈推荐您下载 Facelets 并尽快开始使用它。Facelets 是 JSF 的未来,或者将会是,使用它可以使您在任何 JSF 暴风雨中都干爽(DRY 的双关语,译者注)。如果还不能在当前项目中使用它,请在下一个项目中想着它。

我已经用 Facelets 创建了复合组件、定制 Facelet 标记和内部 CRUD 框架功能的整个库。我发现了许多构建复合组件的技巧和技术(例如,自动生成复选框、日历组件的字段标记,或者根据绑定到组件的值绑定类型的文本字段),超过了在这样一篇介绍性文章中能涉及的内容。相反,我把重点放在让您了解和运行复合组件。通过在这里学到的知识,只用最少的定制功能和定制 Facelets 标记,就可以创建动人的组件。

致谢

特别感谢 Jacob Hookom,他是 Facelets 的创造者,感谢他对本文的审阅和贡献,还要感谢 Athenz 细致入微的编辑。


下载

描述名字大小
Facelets source codej-facelets_code.zip267KB
Facelets source code with jars and warsj-faceletsjarsandwars.zip47MB

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development
ArticleID=107086
ArticleTitle=Facelets 非常适合 JSF
publish-date=02212006