untitled - 文曲经典数字图书馆

500

Upload: khangminh22

Post on 26-Jan-2023

2 views

Category:

Documents


0 download

TRANSCRIPT

企业级 Web 开发实战——

JSF/RichFaces,ExtJS 实战剖析

雨虹 齐天 秋实 王冠宇 编著

北京·BEIJING

内 容 简 介

本书汇集了许多大型企业级 Web 开发的优秀实践经验,内容包含了大量详实的实例:从 Web 开发基础,

到 JSF 组件(RichFaces),再到纯 JavaScript 框架(ExtJS),以及结合两者优点的 ExtFaces,都详细地进行了分

析和总结,其中很多实践解决方案和样例代码稍作修改就可以直接应用于项目开发之中。

结合书中的内容,读者可以根据自己的项目的特点,采用书中的一种或者多种技术灵活地制定实际项目的

解决方案。本书内容广泛,深入浅出,既适合开发人员作为多种 Web UI 技术的入门指导,也适合架构师作为

技术选型的参考手册。

未经许可,不得以任何方式复制或抄袭本书之部分或全部内容。

版权所有,侵权必究。

图书在版编目(CIP)数据

企业级 Web 开发实战:JSF/RichFaces,ExtJS 实战剖析/雨虹等编著.—北京:电子工业出版社,2009.4

ISBN 978-7-121-08476-8

I. 企… Ⅱ.雨… Ⅲ.主页制作-程序设计 Ⅳ.TP393.092

中国版本图书馆 CIP 数据核字(2009)第 033892 号

责任编辑:葛 娜

印 刷:北京东光印刷厂

装 订:三河市皇庄路通装订厂

出版发行:电子工业出版社

北京市海淀区万寿路 173 信箱 邮编 100036

开 本:787×1092 1/16 印张:31 字数:692 千字

印 次:2009 年 4 月第 1 次印刷

印 数:4000 册 定价:59.00 元(含光盘 1 张)

凡所购买电子工业出版社图书有缺损问题,请向购买书店调换。若书店售缺,请与本社发行部联系,

联系及邮购电话:(010)88254888。

质量投诉请发邮件至 [email protected],盗版侵权举报请发邮件至 [email protected]

服务热线:(010)88258888。

·III·

最近几年,Java 和 Web 技术都发生了巨大的变化。对企业应用而言,从外围的门户系统

到核心的业务系统,Web 化已经相当普遍,随之而来的则是计算交互模式的再一次变化。从主

机应用、C/S 应用与 Web 应用,再到 Enterprise Web 2.0 的提出,背后体现的都是计算交互方

式的变化。表现在这几年与用户的交互,成为技术关注的重点。Java Web 表示层的技术多种

多样,变化也比较大。

丰富的用户体验,对企业级应用不只是展现方式的炫和酷,而是通过个性化推送、用户

行为模式分析等,提高应用系统实效的重要手段。根据我这几年的咨询经历,企业的领导者

而不仅仅是过去的 IT 技术人员,都在考虑如何快速应用新的 IT 交互技术,从而增强企业的竞

争力。

大约在 2004 年年初,我的一个同事与好友在做 Rich Client 的项目。闲聊起来,他向我介

绍说有个 Java 设计师不错,基本功比较扎实;而他的肯定通常是相当可靠的。那是我第一次

认识了本书的作者之一雨虹,爱打太极拳的小伙子,朴实的笑容。当时给我的印象是他真心地

喜爱编程,喜欢钻研,不仅仅看做一份工作的技能,而热爱是最好的老师。

这些年雨虹和他的团队出色地完成了许多项目,他们也都成为了有丰富经验的架构师。本

书的作者们结合多年的工作经验,在书中详细地介绍了 Web 表现层编程的现状、发展以及大量

的实践经验。总体来看,这本书以 JSF 为主线,同时又介绍了 Web 2.0 的一些开发技术:Ajax

及一些流行的 Ajax 框架,如 ExtJS,并将两者有机地融合起来,涵盖了 Web 开发的基本方面。

相信本书会为读者提供非常有益的帮助。

新的企业级应用将从交互模式方式上而重新改写。

Sun 中国专业服务部门首席架构师

乔智君

·IV·

前 言

作为一线的技术开发人员,在从业的十年间,我经历过从十几个人的小型项目到数百人的

大型项目。闲暇时我常常细细回味这些经历,感觉技术的演化和发展真是层出不穷,表示层的

技术更是日新月异,从最初的 CGI、Perl、ASP、PHP 到 Servlet、JSP,再到丰富的表示层框架。

现如今,客户端技术主要有 Applet、Swing、SWT/JFace、Flex、AIR、Silverlight、JavaFX 等,

每个都有自己的优缺点,很难真正分出高低。在 Java 的企业级应用开发领域,以 JSP/Servelet

为基础也产生了许多流行一时的 Web 框架,大家耳熟能详的有 Struts、WebWork(后来合并到

Struts 2 项目)、Spring MVC 等。每个框架也都有自己的优缺点,产生了一种百家争鸣的局面,

幸好 Java 社区有 JCP 的存在,虽然有点官僚,但是在瘦客户端开发规范方面推出了统一的 Web

Framework——JSF。随着 Ajax 和 RIA 技术的流行,许多大家以前认为不大可能成为主流的编

程语言又获得了新的生命力,最出彩的是 JavaScript,伴随着“旧瓶新装”Ajax 的流行,涌现出了

一大批的 JavaScript 框架,如 Prototype、script.aculo.us、jQuery、YUI、ExtJS 等。面对林林总

总的开发框架,总是让人难以选择,例如:

我的技术水平不高,面对这些框架,我应该从什么技术开始学起呢?

我想用 ExtJS 的界面,但我不熟悉 JavaScript 语法,有没有办法像使用 JSP 的方法一

样封装 ExtJS?

JSF 真是太好了,但是在上面加一个 Ajax 功能怎么那么困难呢?有没有一个合适的框

架?

有没有一个办法让 Ajax 请求也经过一个 JSF 生命周期?

Struts 统一的异常处理机制真是太棒了,JSF 应用中如何实现这样的机制呢?

JSF 的 DataTable 如何实现翻页时从数据库加载数据?

为什么一次翻页实现在 JSF 生命周期中会被调用两次?如何能避免这样导致性能下降

的问题?

JSF 组件思想是不错,但开发一个组件太难了!写一大堆无用的东西,真是麻烦,有

办法快速定制开发 JSF 组件吗?

…………

本书针对上述问题,结合笔者在项目中的实践给出了行之有效的解决方案,能助你破除重

重障碍;与此同时,也针对 JavaScript 相关的 RIA 开发做了专题描述。书中汇集了许多大型企

·V·

业级 Web 开发的优秀实践经验,内容包含了大量详实的实例:从 Web 开发基础,到 JSF 组件

(RichFaces),再到纯 JavaScript 框架(ExtJS),以及二者的结合 ExtFaces。其中很多解决方案

和样例代码稍作修改就可以直接应用于项目开发之中。结合书中的内容,读者可以根据自己项

目的特点,采用一种或者多种技术灵活地制定实际项目的解决方案。

本书内容广泛,深入浅出,可以作为初学者的入门教材,适合于开发人员作为多种 Web UI

技术的入门指导,也适合架构师作为技术选型的参考手册。

本书结构

本书内容包含 4 大部分共 10 章,所涉及的内容按照学习的层次由浅到深。已经具备 Web

基础知识的读者可以从第Ⅱ部分开始阅读;喜爱 ExtJS 的读者则可以从第Ⅲ部分开始探险。本

书的第Ⅳ部分主要是介绍 ExtFaces,这个框架是笔者实践的总结,是在某大型企业内的成功应

用,其结果远远超出了客户的预期,获得了巨大的成功。

·VI·

缘起

2007 年年末的时候,我们虽然有一些项目采用 Struts,但是大部分项目都还是用 MyFaces

JSF 进行开发。很明显 JSF 的重用组件思想迅速地征服了大部分开发者,虽然刚开始的时候他

们还在抱怨那个所谓的“生命周期”只会提供各种各样的麻烦。在随后的过程中,我们将

RichFaces 引入了项目,获得了巨大的成功。ExtJS 在此时已经逐渐流行,敏锐的雨虹迅速地捕

捉到这一信息,但可惜的是没有一种成熟的方案可以将 ExtJS 和 JSF 完美地结合在一起,充分

发挥两者的长处。在随后的研究过程中,我们找到了一种行之有效的解决方案,并成功地应用

在随后的项目中。此时已经是 2008 年的 6 月了,我们几个人聚在一起萌生出与广大读者分享这

个解决方案的想法,经过 4 个人半年多的努力,这本书终于要和大家见面了。

本书作者

本书作者都是来自于知名企业的一线人员,他们同时也是 Java 社区资深的专家,现在共同

创办了蓝光(BlueLight)社区,主要从事 IT 技术咨询与服务,其范围包括 Java/JEE 活跃的开源框

架、IBM SOA 产品、Sun 主流产品、Oracle 主流产品和 Linux/UNIX 6 等。全书由 4 位作者编写,

其中第 1 章和第 10 章由雨虹编写,第 2~5 章由王冠宇编写,第 6 章和第 7 章由齐天编写,第

8 章和第 9 章由秋实编写。

致谢

本书承 Sun 中国专业服务部门首席架构师、Sun 中国工程研究院软件经理梅其波的审阅,

他们的指点对本书的改进和完善起了很大作用,在此深表感谢。

本书在编写过程中得到了多家 IT 培训机构的热情支持和帮助,他们是国信蓝点的尹德树

(创始人)、程式先峰的黄井洋(创始人)和嘉木华科技的王建中(创始人),在此一并感谢。

本书的出版,绝不仅仅是作者付出辛苦的劳动就能实现的,幕后工作者的名字不应该被埋

没。感谢博文视点的编辑李雨来、葛娜以及为本书顺利出版做过贡献的人。

本书中用到的开源软件,读者可以到 www.broadview.com.cn/08476 下载。

由于作者水平所限,书中肯定存在许多不足之处,敬请读者批评指正,联系邮箱:

[email protected]

·VII·

目 录

第Ⅰ部分 Web UI 开发基础

第 1 章 Web UI 编程综述 ···························································································· 2

1.1 Web 2.0 介绍 ·········································································································· 2

1.1.1 Web 的发展历史 ·············································································································· 2

1.1.2 Web 2.0 相关技术 ·········································································································· 10

1.1.3 SOA 与 Web 2.0 ············································································································· 14

1.2 UI 编程技术 ········································································································· 15

1.2.1 Rich Client Internet 编程 ······························································································· 15

1.2.2 Ajax 编程 ······················································································································ 20

1.2.3 Ajax in JSF 编程 ············································································································· 22

1.3 本章小结 ··············································································································· 23

第 2 章 Ajax 基础知识 ······························································································ 24

2.1 Ajax 的基本知识 ··································································································· 24

2.1.1 Ajax 基本概念 ················································································································ 24

2.1.2 Ajax 的基本特点 ············································································································ 25

2.2 一切从浏览器说起 ································································································ 27

2.2.1 互联网和 HTML 的诞生 ··································································································· 27

2.2.2 HTML 的发展 ················································································································ 29

2.3 DOM 简介 ············································································································ 31

2.3.1 HTML DOM 和 BOM ··································································································· 33

2.3.2 DOM 事件 ······················································································································ 34

2.3.3 DOM 事件流 ·················································································································· 36

2.3.4 事件处理函数 ················································································································ 36

2.4 CSS ························································································································ 37

2.4.1 CSS 简介 ··························································································································· 37

2.4.2 基本的 CSS 语法 ············································································································ 37

2.4.3 CSS 属性 ························································································································ 42

2.4.4 CSS 选择器 ···················································································································· 47

2.4.5 串联(Cascading) ········································································································ 48

2.5 JavaScript 简介 ······································································································ 49

2.5.1 JavaScript 入门示例 ······································································································· 49

2.5.2 JavaScript 基本数据结构 ······························································································· 50

·VIII·

2.5.3 JavaScript 的基本构成 ··································································································· 53

2.5.4 事件驱动及事件处理 ······································································································· 55

2.6 XML 简介 ·············································································································· 55

2.6.1 XML 的产生 ·················································································································· 56

2.6.2 XML 的优点 ·················································································································· 56

2.6.3 一个简单的 XML 文档 ·································································································· 57

2.6.4 XML 文档的整体结构 ··································································································· 58

2.6.5 XML 文档的实质内容——元素 ···················································································· 58

2.6.6 字符数据与实体引用 ····································································································· 59

2.6.7 标记 ································································································································ 59

2.6.8 CDATA ··························································································································· 60

2.6.9 注释 ································································································································ 60

2.7 JSON 简介 ············································································································· 61

2.8 XMLHttpRequest 对象简介 ·················································································· 64

2.8.1 XMLHttpRequest 对象的属性和事件 ············································································ 64

2.8.2 XMLHttpRequest 对象的方法 ························································································ 66

2.8.3 发送请求和处理请求 ····································································································· 67

2.9 本章小结 ··············································································································· 71

第 3 章 Ajax 框架介绍 ······························································································· 72

3.1 Prototype ················································································································ 72

3.1.1 什么是 Prototype ············································································································ 72

3.1.2 Prototype 的下载和引入································································································· 73

3.1.3 Prototype 常用函数介绍································································································· 73

3.1.4 Prototype 的 Ajax 功能 ··································································································· 82

3.2 script.aculo.us········································································································· 90

3.2.1 script.aculo.us 简介 ········································································································ 92

3.2.2 script.aculo.us 的引入和使用 ························································································ 93

3.2.3 script.aculo.us 的功能 ···································································································· 96

3.3 jQuery ·················································································································· 107

3.3.1 jQuery 简介 ················································································································· 108

3.3.2 jQuery 的使用 ·············································································································· 108

3.4 Sarissa ·················································································································· 121

3.4.1 Sarissa 介绍 ················································································································· 121

3.4.2 Sarissa 的使用 ·············································································································· 122

3.5 本章小结 ············································································································· 126

·IX·

第Ⅱ部分 JSF 应用开发

第 4 章 JSF 介绍 ···································································································· 128

4.1 什么是 JSF··········································································································· 128

4.1.1 JSF 简介 ······················································································································· 128

4.1.2 JSF 的体系结构 ············································································································ 130

4.1.3 为什么要使用 JSF ········································································································ 134

4.2 使用条件 ············································································································· 138

4.3 配置 JSF ·············································································································· 139

4.3.1 下载 ······························································································································ 139

4.3.2 安装配置 ······················································································································ 140

4.3.3 HelloWorld ··················································································································· 141

4.4 JSF 的元素··········································································································· 149

4.4.1 UI 组件 ························································································································· 149

4.4.2 JSF 生命周期 ··············································································································· 163

4.4.3 数据转换与验证 ··········································································································· 171

4.4.4 JSF 事件处理 ··············································································································· 185

4.4.5 JSF 表达式语言 ············································································································ 190

4.5 JSF 与 Spring 结合 ······························································································ 192

4.6 JSF 解决方案 ······································································································· 196

4.6.1 DataTable 分页 ············································································································· 196

4.6.2 一般分页 ······················································································································ 197

4.6.3 On-Demand 分页 ·········································································································· 199

4.6.4 Exception 统一处理 ······································································································ 205

4.6.5 Shale 框架验证 ············································································································· 208

4.7 本章小结 ············································································································· 209

第 5 章 Facelets ····································································································· 210

5.1 Facelets 简介 ······································································································· 210

5.2 配置 Facelets········································································································ 211

5.2.1 下载 ······························································································································ 211

5.2.2 安装和配置 ·················································································································· 211

5.2.3 Hello World 示例 ·········································································································· 212

5.3 Facelets 模板和扩展机制 ···················································································· 220

5.3.1 UI Component 和 UIInsert ···························································································· 220

5.3.2 Facelets include 标签的的用法 ····················································································· 222

5.4 Facelets 自定义标签 ···························································································· 225

·X·

5.5 本章小结 ············································································································· 226

第 6 章 使用 RichFaces ·························································································· 228

6.1 RichFaces 简介 ···································································································· 228

6.2 使用条件 ············································································································· 229

6.3 配置 RichFaces ···································································································· 230

6.3.1 下载 ······························································································································ 230

6.3.2 安装及配置 ·················································································································· 231

6.3.3 HelloWorld 示例 ··········································································································· 232

6.3.4 RichFaces 配置进阶 ····································································································· 237

6.4 RichFaces 的基本原理 ························································································ 239

6.4.1 简介 ······························································································································ 239

6.4.2 RichFaces 的体系架构 ································································································· 248

6.4.3 如何发送 Ajax 请求 ····································································································· 250

6.4.4 确定要发送的内容 ······································································································· 251

6.4.5 决定要重绘的区域 ······································································································· 251

6.5 RichFaces 开发工具介绍 ···················································································· 251

6.5.1 Red Hat Developer Studio ····························································································· 251

6.5.2 Eclipse WTP ················································································································· 252

6.6 RichFaces 常用组件介绍 ···················································································· 253

6.6.1 Ajax 日志 <a4j:log> ···································································································· 253

6.6.2 Ajax 监听器<a4j:ajaxListener> ···················································································· 255

6.6.3 参数 <a4j:actionparam> ······························································································· 256

6.6.4 按钮 <a4j:commandButton> ························································································ 259

6.6.5 链接 <a4j:commandLink> ··························································································· 262

6.6.6 Ajax 状态 <a4j:status> ································································································· 268

6.6.7 扩展 Ajax 事件 <a4j:support> ····················································································· 270

6.6.8 日历控件 <rich:calendar> ···························································································· 274

6.6.9 列表移动<rich:listShuttle> ··························································································· 284

6.6.10 可排序列表<rich:orderingList> ·················································································· 292

6.6.11 下拉菜单 <rich:dropDownMenu> ·············································································· 300

6.6.12 模式对话框<rich:modalPanel> ··················································································· 304

6.6.13 面板条<rich:panelBar> ······························································································· 306

6.6.14 可滚动的数据表格<rich:scrollableDataTable> ··························································· 308

6.6.15 可折叠的面板<rich:simpleTogglePanel> ···································································· 319

6.6.16 标签页<rich:tabPanel> ······························································································· 321

6.6.17 工具提示<rich:toolTip> ······························································································ 324

6.6.18 自动完成对话框<rich:suggestionbox> ······································································· 331

·XI·

6.6.19 树<rich:recursiveTreeNodesAdaptor> ········································································· 339

6.6.20 内容菜单<rich:contextMenu>····················································································· 343

6.6.21 页面效果<rich:effect> ································································································ 345

6.6.22 数据表格<rich:dataTable> ·························································································· 348

6.7 优化 Ajax 请求 ···································································································· 351

6.7.1 优化 Ajax 队列 ············································································································· 351

6.7.2 优化数据 ······················································································································ 352

6.8 异常处理 ············································································································· 353

6.8.1 Ajax 请求错误处理 ······································································································ 353

6.8.2 Session 过期处理 ········································································································· 353

6.9 局限和不足 ·········································································································· 353

6.10 本章小结 ··········································································································· 354

第 7 章 Seam 入门 ································································································· 355

7.1 Seam 简介 ············································································································ 355

7.2 配置 Seam ············································································································ 357

7.2.1 下载 ······························································································································ 357

7.2.2 安装和配置 ·················································································································· 358

7.2.3 HelloWorld ··················································································································· 360

7.3 Seam 与 JSF ········································································································· 367

7.4 再谈数据表格(dataTable) ···················································································· 373

7.4.1 单纯 RichFaces 的解决方案 ························································································· 373

7.4.2 RichFaces + Seam 的解决方案 ····················································································· 381

7.5 本章小结 ············································································································· 386

第Ⅲ部分 Ext 应用开发

第 8 章 ExtJS 框架的介绍和使用 ············································································ 389

8.1 JavaScript 面向对象编程 ···················································································· 389

8.2 ExtJS 简介及第一个例子(HelloWorld) ·························································· 395

8.3 ExtJS 布局(layout) ·························································································· 398

8.4 嵌套布局(NestedLayout) ················································································ 401

8.5 表单组件(Ext.form.FormPanel) ······································································ 405

8.6 树组件(Ext.tree.TreePanel) ············································································· 408

8.7 对话框组件(Ext.Window) ·············································································· 413

8.8 表格组件(Ext.grid.GridPanel) ········································································ 417

8.9 菜单组件(Ext.menu.Menu) ············································································· 425

8.10 Utility 组件 ········································································································ 432

·XII·

8.10.1 Ajax 组件 ·················································································································· 432

8.10.2 Template 和 XTemplate 组件 ······················································································ 434

8.10.3 DomHelper 组件 ········································································································· 435

8.11 国际化················································································································ 437

8.12 开发工具 ··········································································································· 438

8.13 本章小结 ··········································································································· 438

第 9 章 DWR 和 JSON-RPC-Java 的介绍及使用 ···················································· 439

9.1 DWR 框架的介绍和使用 ···················································································· 439

9.1.1 DWR 基本概念 ············································································································ 439

9.1.2 使用 DWR ···················································································································· 440

9.1.3 使用自定义对象 ··········································································································· 445

9.1.4 DWR 的配置 ················································································································ 459

9.2 JSON-RPC-Java 框架的介绍和使用 ··································································· 465

9.2.1 基本概念 ······················································································································ 465

9.2.2 安装和配置 ·················································································································· 465

9.3 DWR 和 JSON-RPC-Java 的简单对比································································ 468

9.4 本章小结 ············································································································· 468

第Ⅳ部分 JSF 与 Ext 的结合应用

第 10 章 基于 Ext 的 JSF 组件——ExtFaces ·························································· 470

10.1 ExtFaces 简介 ···································································································· 470

10.1.1 ExtFaces 的来源 ········································································································· 470

10.1.2 ExtFaces 采用的技术 ································································································· 471

10.1.3 JSF 开发现状 ············································································································· 471

10.1.4 Ext 和 JSF 的结合方案······························································································· 473

10.2 ExtFaces 原理 ···································································································· 474

10.3 ExtFaces 目标 ···································································································· 477

10.4 ExtFaces 组件介绍 ···························································································· 477

10.4.1 简单组件 ···················································································································· 477

10.4.2 嵌套组件 ···················································································································· 480

10.5 开发计划 ··········································································································· 481

10.6 本章小结 ··········································································································· 482

第Ⅰ部分 Web UI 开发基础

CHAPTER

1

1.1 Web 2.0 介绍

在介绍 Web 2.0 之前,还是让我们从 Web 的发展历史开始说起吧。

1.1.1 Web 的发展历史

1.1.1.1 从无到有

Web 是一种典型的分布式应用架构。Web 应用中的每一次信息交换都要涉及客户端和

服务器端两个层面。因此,Web 开发技术大体上也可以被分为客户端技术和服务器端技术

两大类。我们先来谈谈客户端技术的萌芽和演进过程。

Web 客户端的主要任务是展现信息内容,而 HTML 语言则是信息展现的 有效载体之

一。作为一种实用的超文本语言,HTML 的历史 早可以追溯到 20 世纪 40 年代。

1945 年,Vannevar Bush 在一篇文章中阐述了文本和文本之间通过超级链接相互关联的

思想,并在文中给出了一种能实现信息关联的计算机 Memex 的设计方案。Doug Engelbart

等人则在 1960 年前后,对信息关联技术做了 早的实验。与此同时,Ted Nelson 正式将这

种信息关联技术命名为超文本(Hypertext)技术。

1969 年,IBM 的 Charles Goldfarb 发明了可用于描述超文本信息的 GML(Generalized

Markup Language)语言。1978 到 1986 年间,在 ANSI 等组织的努力下,GML 语言进

一步发展成为著名的标准通用标记语言(Standard Generalized Markup Language,

SGML)。

第 1 章

Web UI 编程综述

本章主要介绍 Web 的发展历史,以及目前流行的 Web 2.0

的相关技术。Web 2.0 的最大挑战是如何定义和提升用户体验,

而 Ajax 及相关的 RIA 技术则是解决这方面问题的重要手段。

CHAPTER 第 1 章 Web UI 编程综述

3

1

1989 年,欧洲粒子物理研究所(European Council for Nuclear Research,CERN)的 Tim

Berners-Lee 意识到,与其简单地引用其他人的工作,为什么不干脆链接呢?读一篇文章时,

科学家可以打开所引用的文章。超文本(Hypertext)当时相当流行,并利用了他先前在文

档和文本处理方面的研究成果。Tim Berners-Lee 同时感到 SGML 是描述超文本信息的一个

上佳方案,但美中不足的是 SGML 过于复杂,不利于信息的传递和解析。于是,Tim

Berners-Lee 对 SGML 语言做了大刀阔斧的简化和完善,提出了 SGML 的一个子集,称为

超文本标记语言(HyperText Markup Language,HTML)。TimBerners-Lee 不仅创建了一个

称为超文本传输协议(HyperText Transfer Protocol,HTTP)的简单协议,还发明了第一个

Web 浏览器,叫做 WorldWideWeb。Tim Berners-Lee 提出的 HTML 获得巨大的应用,他本

人也由此获得了“互联网之父”的称号。

1.1.1.2 Web 1.0 发展概述

在 HTML 提出之后,Web 获得了飞快的发展,下面从服务器端和客户端两个方面

说起。

1. 客户端 Web 的发展

初的 HTML 语言只能在浏览器中展现静态的文本或图像信息,这满足不了人们对信

息丰富性和多样性的强烈需求——这件事情 终的结果是,由静态技术向动态技术的转变

成为了 Web 客户端技术演进的永恒定律。

能存储、展现二维动画的 GIF 图像格式早在 1989 年就已发展成熟。Web 出现后,

GIF 第一次为 HTML 页面引入了动感元素。但更大的变革来源于 1995 年 Java 语言的问

世。Java 语言天生就具备的平台无关的特点,让人们一下子找到了在浏览器中开发动态

应用的捷径。1996 年,著名的 Netscape 浏览器在其 2.0 版中增加了对 Java Applet 和

JavaScript 的支持。Netscape 的竞争对手,Microsoft 的 IE 3.0 浏览器也在这一年开始支

持 Java 技术。现在,开发人员可以用 Java 和 JavaScript 语言开发丰富 HTML 页面的功

能了。顺便说一句,JavaScript 语言在所有客户端开发技术中占有非常独特的地位:它

是一种以脚本方式运行的、简化了的 Java 语言,这也是脚本技术第一次在 Web 世界里

崭露头角。为了用 Microsoft 的技术与 JavaScript 抗衡,Microsoft 还为 1996 年的 IE 3.0

设计了另一种后来也声名显赫的脚本语言——VBScript 语言。

真正让 HTML 页面表现更加丰富和强大的,则是 CSS(Cascading Style Sheets)和

DHTML(Dynamic HTML)技术。1996 年年底,W3C 提出了 CSS 的建议标准,同年,IE 3.0

引入了对 CSS 的支持。CSS 大大提高了开发者对信息展现格式的控制能力。1997 年的

Netscape 4.0 不但支持 CSS,而且增加了许多 Netscape 公司自定义的动态 HTML 标记,这

些标记在 CSS 的基础上,让 HTML 页面中的各种要素相互交互。1997 年,Microsoft 发布

了 IE 4.0,并将动态 HTML 标记、CSS 和动态对象模型(DHTML Object Model)发展成了

一套完整、实用、高效的客户端开发技术体系,Microsoft 称其为 DHTML。同样是实现 HTML

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

4

页面的动态效果,DHTML 技术无须启动 Java 虚拟机或其他脚本环境,可以在浏览器的支

持下,获得更好的展现效果和更高的执行效率。

为了在 HTML 页面中实现音频、视频等更为复杂的多媒体应用,1996 年的 Netscape

2.0 成功地引入了对 QuickTime 插件的支持,插件这种开发方式也迅速风靡了浏览器的

世界。在 Windows 平台上,Microsoft 将客户端应用集中在了 20 世纪 90 年代中期刚刚

问世的 COM 和 ActiveX 身上。1996 年,IE 3.0 正式支持在 HTML 页面中插入 ActiveX

控件的功能,这为其他厂商扩展 Web 客户端的信息展现方式开辟了一条自由之路。1999

年,RealPlayer 插件先后在 Netscape 和 IE 浏览器中取得了成功,与此同时,Microsoft

将媒体播放软件 Media Player 也被预装到了各种 Windows 版本之中。同样值得纪念的还

有 Flash 插件的横空出世:20 世纪 90 年代初期,Jonathan Gay 在 FutureWave 公司开发

了一种名为 Future Splash Animator 的二维矢量动画展示工具,1996 年,Macromedia 公

司收购了 FutureWave,并将 Jonathan Gay 的发明改名为我们熟悉的 Flash。从此,Flash

动画成了 Web 开发者表现自我、展示个性的 佳方式。

除了编写 HTML 页面之外,客户端应用的开发者还可以利用一些成熟的技术将浏览器

的功能添加到自己的应用程序中。从 1992 年开始,W3C 就免费向开发者提供 libwww 开发

库。借助 libwww,我们可以自己编写 Web 浏览器和 Web 搜索工具,也可以分析、编辑或

显示 HTML 页面。1999 年,Microsoft 在 IE 5.0 中引入的 HTAs(HTML Applications)技术

则允许我们直接将 HTML 页面转换为一个真正的应用程序。从 1997 年的 IE 4.0 开始,

Microsoft 为开发者提供了 WebBrowser 控件和其他相关的 COM 接口,允许程序员在自己

的程序中直接嵌入浏览器窗口,或调用各种浏览器的功能,如分析或编辑 HTML 页面等。

Windows 98 及其后的 Windows 操作系统甚至还利用 WSH(Windows Script Host)技术将原

本只在浏览器中运行的 JavaScript、VBScript 变成了可以在 Win32 环境下使用的通用脚本语

言,这些都极大地扩展和增强了 Web 客户端开发技术。

2. 服务器端技术的成熟与发展

与客户端技术从静态向动态的演进过程类似,Web 服务器端的开发技术也是由静态向

动态逐渐发展、完善起来的。

早的 Web 服务器简单地响应浏览器发来的 HTTP 请求,并将存储在服务器上的

HTML 文件返回给浏览器。一种名为 SSI(Server Side Includes)的技术可以让 Web 服务器

在返回 HTML 文件前,更新 HTML 文件的某些内容,但其功能非常有限。第一种真正使

服务器能根据运行时的具体情况,动态生成 HTML 页面的技术是大名鼎鼎的 CGI(Common

Gateway Interface)技术。1993 年,CGI 1.0 的标准草案由 NCSA(National Center for

Supercomputing Applications)提出;1995 年,NCSA 开始制定 CGI 1.1 标准;1997 年,CGI

1.2 也被纳入了议事日程。CGI 技术允许服务器端的应用程序根据客户端的请求,动态生成

HTML 页面,这使客户端和服务器端的动态信息交换成为了可能。随着 CGI 技术的普及,

聊天室、论坛、电子商务、信息查询、全文检索等各式各样的 Web 应用蓬勃兴起,人们可

CHAPTER 第 1 章 Web UI 编程综述

5

1

以享受到信息检索、信息交换、信息处理等更为便捷的信息服务。

早期的 CGI 程序大多是编译后的可执行程序,其编程语言可以是 C、C++、Pascal

等任何通用的程序设计语言。为了简化 CGI 程序的修改、编译和发布过程,人们开始探

寻用脚本语言实现 CGI 应用的可行方式。在此方面,不能不提的是 Larry Wall 于 1987

年发明的 Perl 语言。Perl 结合了 C 语言的高效,以及 sh、awk 等脚本语言的便捷,似乎

天生就适用于 CGI 程序的编写。1995 年,第一个用 Perl 写成的 CGI 程序问世。很快,

Perl 在 CGI 编程领域的风头就盖过了它的前辈 C 语言。随后,Python 等著名的脚本语

言也陆续加入了 CGI 编程语言的行列。

1994 年,Rasmus Lerdorf 发明了专用于 Web 服务器端编程的 PHP(Personal Home

Page Tools)语言。与以往的 CGI 程序不同,PHP 语言将 HTML 代码和 PHP 指令合成

为完整的服务器端动态页面,Web 应用的开发者可以用一种更加简便、快捷的方式实

现动态 Web 功能。

1996 年,Microsoft 借鉴 PHP 的思想,在其 Web 服务器 IIS 3.0 中引入了 ASP 技

术,ASP 使用的脚本语言是我们熟悉的 VBScript 和 JavaScript。借助 Microsoft Visual

Studio 等开发工具在市场上的成功,ASP 迅速成为了 Windows 系统下 Web 服务器端的

主流开发技术。

在此期间,以 Sun 公司为首的 Java 阵营在这种潮流中也陆续推出了一些新的技术。1997

年,Servlet 技术问世;1998 年,JSP 技术诞生。Servlet 和 JSP 的组合(还可以加上 JavaBean

技术)让 Java 开发者同时拥有了类似 CGI 程序的集中处理功能和类似 PHP 的 HTML 嵌入

功能。此外,Java 的运行时编译技术也大大提高了 Servlet 和 JSP 的执行效率——这也正是

Servlet 和 JSP 被后来的 J2EE 平台吸纳为核心技术的原因之一。

Web 服务器端开发技术的完善使开发复杂的 Web 应用成为了可能。在此起彼伏的电子

商务大潮中,为了适应企业级应用开发的各种复杂需求,为了给 终用户提供更可靠、更

完善的信息服务,两个 重要的企业级开发平台——J2EE 和.Net 在 2000 年前后分别诞生,

它们随即就在企业级 Web 开发领域展开激烈的竞争。这种竞争关系促使了 Web 开发技术以

前所未有的速度在提高和跃进。

J2EE 是纯粹基于 Java 的解决方案。1998 年,Sun 发布了 EJB 1.0 标准,EJB 为企业级

应用中必不可少的数据封装、事务处理、交易控制等功能提供了良好的技术基础。至此,

J2EE 平台的三大核心技术 Servlet、JSP 和 EJB 都已先后问世。1999 年,Sun 正式发布了 J2EE

的第一个版本。紧接着,遵循 J2EE 标准,为企业级应用提供支撑平台的各类应用服务软件

争先恐后地涌现出来。IBM 的 WebSphere、BEA 的 WebLogic 都是这一领域里极为成功的

商业软件平台。随着开源运动的兴起,JBoss 等开源世界里的应用服务新秀也吸引了许多用

户的注意力。到 2003 年时,Sun 的 J2EE 版本已经升级到了 1.4 版,其中 3 个关键组件的

版本也演进到了 Servlet 2.4、JSP 2.0 和 EJB 2.1。至此,J2EE 体系及相关的软件产品已经成

为了 Web 服务器端开发的一个强有力的支撑环境。

和 J2EE 不同的是,Microsoft 的.Net 平台是一个强调多语言间交互的通用运行环境。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

6

尽管.Net 的设计者试图以.Net 平台作为绝大多数 Windows 应用的首选运行环境,但.Net 首

先吸引的却是 Web 开发者的目光。2001 年,ECMA 通过了 Microsoft 提交的 C#语言和 CLI

标准,这两个技术标准构成了.Net 平台的基石,它们也于 2003 年成为了 ISO 的国际标准。

2002 年,Microsoft 正式发布.Net Framework 和 Visual Studio .Net 开发环境。早在.Net 发布

之前,就已经有许多 Windows 平台的 Web 开发者迫不及待地利用 Beta 版本开发 Web 应用

了。这大概是因为,.Net 平台及相关的开发环境不但为 Web 服务器端应用提供了一个支持

多种语言的、通用的运行平台,而且还引入了 ASP.Net 这样一种全新的 Web 开发技术。

ASP.Net超越了ASP的局限,可以使用VB.Net、C#等编译型语言,支持Web Form、.Net Server

Control、ADO.Net 等高级特性。客观地讲,.Net 平台,尤其是.Net 平台中的 ASP.Net,为

Web 开发技术在 Windows 平台上发展提供了良好的平台基础。

1.1.1.3 从 Web 1.0 到 Web 2.0

Web 2.0 是相对 Web 1.0 的新的一类互联网应用的统称。由 Web 1.0 单纯地通过网络

浏览器浏览 HTML 网页模式向内容更丰富、联系性更强、工具性更强的 Web 2.0 互联网

模式的发展已经成为互联网新的发展趋势。Web 1.0 与 Web 2.0 在互联网应用经过一段时

间发展之后,理念上产生了差异。这种差异的核心并不复杂, 简单的说法就是以信息

为中心,还是以人为中心。具体一点说,Web 1.0 是以信息为中心,是人与信息的关系;

Web 2.0 是以人为中心,是人与人的关系。Web 1.0 的主要特点在于用户通过浏览器获取

信息,Web 2.0 则更注重用户的交互作用,用户既是网站内容的消费者(浏览者),也是

网站内容的制造者。

Web 1.0 到 Web 2.0 的转变,具体地说,从模式上是单纯的“读”向“写”、“共同建设”

发展;从基本构成单元上,是由“网页”向“发表/记录的信息”发展;从工具上,是由互

联网浏览器向各类浏览器、RSS 阅读器等内容发展;从运行机制上,由“Client Server”向

“Web Services”转变;作者则由程序员等专业人士向全部普通用户发展。

目前 Web 2.0 还没有一个明确的概念,目前一个比较清晰的定义是这样定义 Web 2.0

的:

“Web 2.0 是以 Flickr、Craigslist、Linkedin、Tribes、Ryze、Friendster、Del.icio.us、

43Things.com 等网站为代表,以 Blog、TAG、SNS、RSS、WIKI 等应用为核心,依据六度

分隔、XML、Ajax 等新理论和技术实现的互联网新一代模式。”

虽然 Web 2.0 的定义模糊,不过一个普遍的特征是与使用者互动性更高。另一项特

征是 Aajx 技术的应用,使得前端 Web 应用可以和后端数据库互动,将 XML 信息重新

整理、重组再显示到网页上。这也使得新一代的网站应用必须兼顾使用者经验及信息与

商业流程。

如果 Netscape 可以称为 Web 1.0 的前驱,那么 Google 几乎可以肯定是 Web 2.0 的旗手,

只要看看他们的首次公开上市是如何地揭示了各自的时代就清楚了。所以我们就从这两个

公司和其定位的差别入手。

CHAPTER 第 1 章 Web UI 编程综述

7

1

Netscape 以传统的软件摹本来勾勒其理念“互联网作为平台”:他们的旗舰产品是互

联网浏览器,一个桌面应用程序。同时,他们的战略是利用他们在浏览器市场的统治地

位,来为其昂贵的服务器产品建立起市场。从理论上讲,在浏览器中控制显示内容和程

序的标准,赋予了 Netscape 一种市场支配力,如同微软公司在个人计算机市场上所享受

的一样。

终,浏览器和网络服务器都变成了“日用品”,同时价值链条也向上移动到了在互联

网平台上传递的服务。

作为对比,Google 则以天生的网络应用程序的角色问世,它从不出售或者打包其程序,

而是以服务的方式来传递。客户们直接或间接地为其所使用的服务向 Google 付费。原有软

件工业缺陷荡然无存,没有了定期的软件发布,只需要持续地改善;没有了许可证或销售,

只需要使用;没有了为了让用户在其设备上运行软件而不得不进行的平台迁移,只需要搭

建宏大的、由众多个人计算机组成的、可伸缩的网络,其上运行开源操作系统,及其自行

研制的应用程序和工具,而公司之外的任何人则永远无法接触到这些东西。

虽然 Netscape 和 Google 都可以被描述为软件公司,但显然 Netscape 可以归到 Lotus、

Microsoft、Oracle、SAP,以及其他发源于 20 世纪 80 年代软件革命的那些公司所组成的软

件世界。

而 Google 的同伴们,则是像 eBay、Amazon、Napster 乃至 DoubleClick 和 Akamai

这样的互联网公司。这些互联网公司的兴起,标志着 Web 2.0 正式走上了 Web 历史舞

台。

1.1.1.4 Web 2.0 特征

如前所述,Web 2.0 并不是一个具体的事物,而是一个阶段,是促成这个阶段的各

种技术和相关的产品服务的一个称呼。所以,我们无法说,Web 2.0 是什么;但是可以

说,哪些是 Web 2.0。

WikiPedia 的 Web 2.0 条目下列出了这些条件(引自维基百科):

CSS 和语义相关的 XHTML 标记;

Ajax 技术;

Syndication of data in RSS/ATOM;

Aggregation of RSS/ATOM data;

简洁而有意义的 URL;

支持发布为 Web log;

RESTian(preferred)或者 XML WebService API;

一些社会性网络元素。

必须具备的要素有:

网站应该能够让用户把数据在网站系统内外倒腾;

用户在网站系统内拥有自己的数据;

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

8

完全基于 Web,所有的功能都能通过浏览器完成。

虽然这只是一家之言,不过,对于其中谈到的几个要素,大家还是公认的。

基于 RSS/ATOM/RDF/FOAF 等 XML 数据的同步、聚合和迁移。

社会性因素。内容跟着人走,内容又能够被用户自由地组合,也就是说,用户能够

自由地借助内容媒介,创建起一个个的社群,发生各种社会性的(网络)行为。此

外还有标签,以及建立在开放标签系统之上的 Folksonomy。

开放 API。目前 Amazon、Flickr、Google map 等都开放了 API,可以利用这些 API

快速建立各种 Web 2.0 应用。

上面这些内容中,主要包括两条:微内容和用户个体。除了这两条 基本的外,还可

以考虑社群内的分享,以及提供 API,如图 1-1 所示。

图 1-1 Web 2.0

微内容:英文是 microcontent。用户所生产的任何数据都算是微内容,比如一个 Blog、

评论、图片、收藏的书签、喜好的音乐列表、想要做的事情、想要去的地方、新的

朋友等。这些微内容,充斥着我们的生活、工作和学习,它的数量、重要性,还有

我们对它的依赖,这些都是 Web 2.0 重要的数据基础。我们每天都生产众多的微内

容,也消费着同样多的微内容。对于 Web 2.0 来说,对微内容的重新发现和利用,

如何帮助用户管理、维护、存储、分享、转移微内容,就成了关键。

用户个体。对于 Web 1.0 的典型产品/服务来说,用户没有具体的面貌、个性,它只

是一个模糊的群体的代名词而已。但是对于 Web 2.0 的产品和服务来说,用户是个

实实在在的人。Web 2.0 所服务的,是具体的人,而不是一个如同幽灵般的概念。

CHAPTER 第 1 章 Web UI 编程综述

9

1

并且,这个人的具体性,会因为服务本身而不断地充实起来。如何为这个具体的个

体服务,是 Web 2.0 设计的起点。

因此,可以被称作 Web 2.0 的产品/服务将是这样的:

服务于用户个体的微内容的收集、创建、发布、管理、分享、合作、维护等的平台。

微内容的 XML 表现、微内容的聚合、微内容的迁移、社会性关系的维护、界面的

易用性等,以及是否开源、参与、个人价值、草根、合作等。

归纳起来,Web 2.0 与 Web 1.0 的区别就在于:“Web 1.0 天天谈门户,Web 2.0 谈个人

化;Web 1.0 谈内容,Web 2.0 谈应用;Web 1.0 谈商业模式,Web 2.0 谈服务;Web 1.0 谈

密闭、大而全,Web 2.0 大家谈开放、谈联合;Web 1.0 谈网站中心化,Web 2.0 谈个人中

心化;Web 1.0 谈一对一,Web 2.0 谈社会性网络。”

1.1.1.5 未来趋势与 Web 3.0

所有人都在关心 Web 的发展前景,所有人都想知道 10 年以后的 Web 会成长成什么样

子。要回答这些问题,没有谁比 W3C 更有权威了。W3C 明确地告诉我们:Web 的未来是

语义化的 Web(Semantic Web)。今天的 Web 可以方便地生成、传递和展现各式各样的信息,

但它还只是一个信息的“容器”,很难揭示出信息本身的内容和特性。与此相对的是,未来

的语义化 Web 是一种懂得信息内容的 Web,是真正的“信息管理员”。

从技术角度看,XML 语言统一了信息的表达方式,但这离揭示信息内容的目标还相距

甚远。1998 年,W3C 和一些研究机构开始对元数据(Metadata)进行研究。元数据是描述

数据的数据,可以揭示信息的内容特性。1999 年,Netscape 提出的 RSS(Rich Site Summary)

建议标准是用元数据技术描述新闻等信息内容的第一次尝试。1999 年,W3C 的研究小组提

出了 RDF(Resource Description Framework)标准草案。RDF 在 XML 语法的基础上,规定

了元数据的存储结构和相关的技术标准。使用 RDF 语言,我们可以用统一的、可交换的格

式揭示出信息本身的各种特性。2001 年,W3C 又开始着手制定 OWL(Web Ontology

Language)标准。OWL 语言也是一种符合 XML 标准的语言,它比 RDF 又前进了一步,可

以更加深入、细致地描述信息内容。在 RDF 和 OWL 语言的帮助下,我们能让 Web 上的信

息内容变得更容易理解,更便于交换和共享。2003 年,W3C 成立了语义化 Web Service 研

究小组(Semantic Web Services Interest Group),研究在 Web Service 中加入语义技术的相关

问题。2004 年 2 月,W3C 宣布 RDF 和 OWL 标准正式成为 W3C 的建议方案,这标志着语

义化 Web 的研究正式开始。

随着语义化 Web 的诞生和发展,Web 开发技术也必将经历更为重大的变革。可以预见

的是,在未来的几年里,还会有许多新的开发技术或开发平台出现。从静态技术到动态技

术,从开发平台到应用模型,从传统 Web 到语义化 Web……为了让更多的人获得更有价值

的信息服务,Web 开发者们也许还会经历一次又一次的技术浪潮,还会面临更为严峻的技

术挑战,但这一切都是为了互联网的根本目的即信息共享服务的。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

10

1.1.2 Web 2.0 相关技术

1.1.2.1 XML 语言及相关技术

如果说 HTML 语言给 Web 世界赋予了无限生机的话,那么,XML 语言的出现可以算

成是 Web 的又一次革命。按照 Tim Berners-Lee 的说法,Web 是一个“信息空间”。HTML

语言具有较强的表现力,但也存在结构过于灵活、语法不规范的弱点。当信息都以

HTML 语言的面貌出现时,Web 这个信息空间是杂乱无章、没有秩序的。为了让 Web

世界里的所有信息都有章可循、有法可依,就需要一种更为规范、更能够体现信息特

点的语言。

1996 年,W3C 在 SGML 语言的基础上,提出了 XML(Extensible Markup Language)

语言草案。1998 年,W3C 正式发布了 XML 1.0 标准。XML 语言对信息的格式和表达方法

做了 大程度的规范,应用软件可以按照统一的方式处理所有 XML 信息。这样,信息在

整个Web世界里的共享和交换就有了技术上的保障。HTML语言关心的是信息的表现形式,

而 XML 语言关心的是信息本身的格式和数据内容。从这个意义上说,XML 语言不但可以

将客户端的信息展现技术提高到一个新的层次,而且可以显著提高服务端的信息获取、生

成、发布和共享能力。为了将 XML 信息转换为 HTML 等不同的信息展现形式,1999 年,

W3C 制定出了 XSLT 标准。同一年,IE 5.0 增加了对 XML 和 XSLT 的支持。

现在,网站的开发者可以直接使用 XML 语言发布信息了。针对不同的应用领域,人

们还制定了许多专门的 XML 规范。例如,2001 年 W3C 发布的 SVG(Scalable Vector

Graphics)1.0 标准就是一种用 XML 语言表达的、全新的二维矢量图形格式。开发者可以

用 SVG 格式描述大多数已有的 Flash 动画。与 Flash 格式相比,符合 XML 标准的 SVG 格

式显然更有利于信息交换和共享。

Web 本身就是一个 大的分布式应用系统。对于分布式开发而言,XML 技术也大有用

武之地。一个明显的事实是,如果能让分布式应用借助 XML 格式交换信息,那么,以往

横亘在分布式架构上的信息交换难题也就迎刃而解了。1999 年,W3C 和相关的企业开始讨

论设计基于 XML 的通信协议。2000 年,W3C 发布 SOAP(Simple Object Access Protocol)

协议的 1.1 版。利用 SOAP 协议传递 XML 信息的分布式应用模型就是 Web Service。2001

年,W3C 发布了 WSDL(Web Services Description Language)协议的 1.1 版。SOAP 协议和

WSDL 协议共同构成了 Web Service 的基础。随后,J2EE 和.Net 这两大企业级开发平台先

后实现了 Web Service,并将其视为平台的一项核心功能。

Web Service 对于 Web 开发者的重要意义在于,当需要在不同的服务器端、不同的客户

端乃至不同的应用类型、不同的计算设备之间传递信息时,以往的分布式开发技术或者因

为适应性不强,或者因为扩展能力不足,都难以满足现代 Web 开发的需要,而 Web Service

正好填补了这一空白。

CHAPTER 第 1 章 Web UI 编程综述

11

1

1.1.2.2 Ajax 技术

基于 XML 的异步 JavaScript,简称 Ajax,是当前 Web 发展(称为 Web 2.0)中的一

个重要技术。这个术语源自描述从基于网页的 Web 应用到基于数据的应用的转换。在基

于数据的应用中,用户需求的数据如联系人列表,可以从独立于实际网页的服务器端取

得并且可以被动态地写入网页中,这极大得地提高了缓慢的 Web 应用体验,使之像桌面

应用一样具有良好的交互性。

Ajax 的 大特色在于用户体验。在使应用更快响应和创新的过程中,定义 Web 应用的

规则正在被重写,例如用户通常希望每一次按钮点击会导致几秒的延迟和屏幕刷新,但 Ajax

正在打破这种长时间的状况。因此用户需要重新体验按钮点击的响应了。

可用性是 Ajax 令人激动的地方,而且已经产生了几种新颖的技术。其中 引人注目的

是一种称为“黄色隐出”的技术,它在数据更新之前将用户界面变为黄色,更新完成后立

刻恢复原来的颜色。Ajax 开发人员将用户从 Web 应用,的负载中解放出来;小心地利用

Ajax 提供的丰富接口,Ajax 开发人员就可以开发像桌面应用一样的 Web 应用。

1.1.2.3 Blog

Blog 的全名应该是 Web log,后来缩写为 Blog。Blog 是一个易于使用的网站,你可以

在其中迅速发布想法、与他人交流或从事其他活动,所有这一切都是免费的。Blogger 就是

写 Blog 的人。从理解上讲,博客是“一种表达个人思想、网络链接、内容,按照时间顺序

排列,并且不断更新的出版方式”。简单地说,Blogger 是一类人,这类人习惯于在网上写

日记。

关于 Blog/Blogger 的中文名称,其实有很多争论,有称为“博客”,也有称为“网志”,

中国台湾还有称之为“部落格/部落客”。现在基本统一为“博客”,其中根据博客内容的不

同,又有博客、播客之分,博客强调文字性,而播客则强调视频。

博客是继 E-mail、BBS、ICQ 之后出现的第 4 种网络交流方式,代表着新的生活方

式和新的工作方式,更代表着新的学习方式。通过博客,让自己学到很多,让别人学到

更多。

1.1.2.4 RSS

RSS 是站点用来和其他站点之间共享内容的一种简易方式(也叫聚合内容)的技术。

初源自浏览器“新闻频道”的技术,现在通常被用于新闻和其他按顺序排列的网站,例

如 Blog。但是 RSS 没有一个统一的名称,下面是 RSS 的缩写。

Really Simple Syndication(真正简易的聚合)

Rich Site Summary(丰富的站点摘要)

RDF Site Summary(RDF 站点摘要)

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

12

利用 RSS,我们可以:

(1)订阅 Blog(在 Blog 上,你可以订阅自己工作中所需的技术文章;也可以订阅与

自己有共同爱好的作者的日志。总之,Blog 上你对什么感兴趣你就可以订什么)。

(2)订阅新闻(无论是奇闻怪事、明星消息、体坛风云,只要你想知道的,都可以订

阅)

通过 RSS,我们可以订阅 新的新闻消息,这些新闻具有如下特点:

没有广告或者图片来影响对标题或者文章概要的阅读。

RSS 阅读器自动更新定制的网站内容,保持新闻的及时性。

用户可以加入多个定制的 RSS 提要,从多个来源搜集新闻整合到单个数据流中。

(3)扩大了网站内容的传播面,也增加了网站访问量,因为访问者调阅的 RSS 文件

和浏览的网页,都是从网站服务器上下载的。

RSS 文件的网址是固定不变的,网站可以随时改变其中的内容。RSS 内容一旦更新,

浏览者看到的内容也随即更新了。

1.1.2.5 Tag

Tag(标签)是一种更为灵活、有趣的分类方式,你可以为每篇日志、每个帖子或者每

张图片等添加一个或多个 Tag(标签),你可以看到网站上所有和你使用了相同 Tag 的内容,

由此和他人产生更多的联系。Tag 体现了群体的力量,使得内容之间的相关性和用户之间

的交互性大大增强。

比如,你在一篇日志上添加了“读书”和“Tag”两个标签,就能通过这两个 Tag 看到

和你有相同兴趣的其他日志。同样,如果你给自己的网络书签贴上不同标签,那么,在下

一次去寻找时,会轻易找到自己想要的信息。

那么,如果我贴了 Tag,能产生什么效果呢?首先,信息将会条理化;其次,当积累

了一定数量的 Tag 之后,你会发现自己 关心的话题。Google 的“我的搜索历史”功能

就采用了标签,你的每次搜索关键词都可以成为 Tag,之后,你会了解自己这一天在关

心什么。

当然,你也可以看到有哪些人和自己使用了一样的 Tag(标签),进而找到和自己志趣相

投的人。

1.1.2.6 WiKi

WiKi 概念的发明人是 Ward Cunningham,该词来源于夏威夷语的“wee kee wee kee”,

原本是“快点快点”(quick)的意思。

WiKi——一种多人协作的写作工具。WiKi 站点可以由多人(甚至任何访问者)维护,

每个人都可以发表自己的意见,或者对共同的主题进行扩展或者探讨。

WiKi 指一种超文本系统,这种超文本系统支持面向社群的协作式写作,同时也包括

CHAPTER 第 1 章 Web UI 编程综述

13

1

一组支持这种写作的辅助工具。有人认为,WiKi 系统属于一种人类知识网格系统,我们

可以在 Web 的基础上对 WiKi 文本进行浏览、创建、更改,而且创建、更改、发布的代

价远比 HTML 文本小;同时 WiKi 系统还支持面向社群的协作式写作,为协作式写作提

供必要帮助;WiKi 的写作者自然构成了一个社群,WiKi 系统为这个社群提供简单的交流

工具。与其他超文本系统相比,WiKi 有使用方便及开放的特点,所以 WiKi 系统可以帮

助我们在一个社群内共享某领域的知识。

WiKi 适合做百科全书、知识库、整理某一个领域的知识等知识型站点,几个分

在不同地区的人利用 WiKi 协同工作共同写一本书等。WiKi 技术已经被较好地应用在百

科全书、手册/FAQ 编写、专题知识库方面。WiKipedia,中文名称为“维基百科”,是

目前基于 WiKi 的全球 大的自由的网络百科全书,是一个自由、免费、内容开放的百

科全书协作计划,参与者来自世界各地,并且其内容和规模都已经超过了《大英百科全

书》。 WiKi 具有如下特点:

(1)使用方便

维护快捷:快速创建、存取、更改超文本页面(这也是为什幺叫作“wiki wiki”的

原因)。

格式简单:用简单的格式标记来取代 HTML 的复杂格式标记(类似所见即所得的风

格)。

链接方便:通过简单标记,直接以关键字名来建立链接(页面、外部链接、图像等)。

命名平易:关键字名就是页面名称,并且被置于一个单层、平直的名空间中。

(2)有组织

自组织的:同页面的内容一样,整个超文本的组织结构也是可以修改、演化的。

可汇聚的:系统内多个内容重复的页面可以被汇聚于其中的某个上,相应的链接结

构也随之改变。

(3)可增长

可增长:页面的链接目标可以尚未存在,通过单击链接,可以创建这些页面,从而

使系统得到增长。

修订历史:记录页面的修订历史,页面的各个版本都可以被获取。

(4)开放性

开放的:社群的成员可以任意创建、修改、删除页面。

可观察:系统内页面的变动可以被访问者观察到。

1.1.2.7 网摘

“网摘”又名“网页书签”,起源于一家叫做 Del.icio.us 的美国网站自 2003 年开始提

供的一项叫做“社会化书签”(Social Bookmarks)的网络服务,网友们称之为“美味书签”

(Delicious 在英文中的意思就是“美味的、有趣的”)。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

14

1.1.2.8 SNS

SNS 是英文 Social Networking Service 的缩写,直译就是“社交网络服务”。SNS 是 Web

2.0 体系下的一个技术应用架构。SNS 基于六度分隔理论运作,这个理论的通俗解释是:“在

人脉网络中,要结识任何一位陌生的朋友,中间 多只要通过 6 位朋友就可以达到目的。”

放在 Web 2.0 的背景下,每个用户都拥有自己的 Blog、社会化书签。用户通过 Tag 、RSS

或者 IM、邮件等方式连接到一起,按照六度分隔理论,每个个体的社交圈都不断放大,

后成为一个大型网络,这就是 SNS。

1.1.3 SOA 与 Web 2.0

让 Web 作为机器之间、应用程序之间相互交流协作的平台,就必须在 Web 中传递

程序和数据,而使得这种传递成为可能的技术体系,我们称为 Web 服务。随着 Web 2.0

的崛起,Ajax 带来了客户端体验的划时代进步,而被称为 REST 的新的 Web 应用架构,

实现了 HTTP 风格 Web 服务的文艺复兴。Web 2.0 时代的 Web 服务是这样的:完全符

合 Web 标准,充分利用 HTTP 协议的特点,通过 REST 风格暴露服务,支持 Ajax/RIA

异步数据传输和界面刷新机制,使用 Syndication 机制推送数据,以 XML 表达丰富的、

具有初步语义信息的数据,并可方便地被 mash-up。SOA 的润滑油企业应用就是支撑企

业运营的软件系统。纯粹从编程技术上来看,企业应用的开发通常是平淡无奇的,既没

有高超的算法,也不涉及精妙的底层炫技,然而企业应用之难却是业界公认的。企业应

用需求多变,业务逻辑复杂,工期紧张,对分布性、整合、性能、可靠性和可监管性要

求甚高,构成企业应用之难的根本原因。然而,当前我们采用的基于构件的软件思想与

业务层面距离过大,各技术流派之间难以整合,这些偶发复杂性加剧了企业应用的难度。

SOA 正是试图从这两方面来降低企业应用开发和运维的复杂性。首先,SOA 以服务为

基础。这一概念既可以作为描述业务的元素和语言,也可以在技术空间得到直接的表达,

从而成为沟通业务与技术的桥梁,也缓解了技术与业务之间“阻抗不匹配”的困难,使

得 IT 能够随业务需求灵活应变。其次,SOA 通过标准化的、跨平台的技术规范,使得

运行在不同地点、不同环境中的服务能够被统一调配组装,从而在业务流程上实现整合。

Web 2.0 的技术浪潮给 SOA 带来一些新的思路。Web 是天生的标准化、高效、高度可

扩展的分布式文档系统,通过 Ajax/RIA 表现出丰富的人机交互界面。mash-up 是构建

组合应用简单而理想的方式,而且 mash-up 所生成的新应用本身可以作为 XML 数据源

而成为新组合应用的基础,这些都与 SOA 的目标惊人的一致。能否将 Web 2.0 的技术

根据 SOA 的需求加以整合改造,从而形成一个轻量级的 SOA 技术栈呢?这是一个很自

然,同时又令人心驰神往的境界。APP(ATOM Publish Protocol)协议已经具备了丰富

的数据操作能力,XQuery 语言也已经成为 W3C 正式标准,有人提出要给 REST 架构

添加一个类似 WSDL 的服务契约描述协议,也有一些企业正在研制完整的 Web Oriented

CHAPTER 第 1 章 Web UI 编程综述

15

1

Architecture 技术栈,将 Web 技术引入企业 IT 领域。Web 1.0 给企业应用带来的变化

是巨大的,所谓 B/S 模型现在已经成为企业应用的绝对主流。但是从根本上讲,Web 1.0

没有触及到企业应用的核心问题,只是给企业应用安装了一副脸面。能够深刻改变企业

IT 的是 SOA,而 Web 2.0 又恰好与 SOA 形成默契的搭档。所以,Web 2.0 不但将成为

企业应用的新面孔,而且成为 SOA 的润滑剂,会深入到 SOA 当中,深刻改变企业应用

的开发思路。这部分关于 SOA 的具体技术内容,我们将在后续的系列丛书中进行专门

的介绍。

1.2 UI 编程技术

Internet 已经日益成为应用程序开发的默认平台。用户对应用程序复杂性要求日增,

但现在的 Web 应用程序对完成复杂应用方面却始终跟不上步伐。用户与今天中等复杂

程度的 Web 应用程序交互时,其体验并不能令人满意。Web 模型是基于页面的模型,

缺少客户端智能机制。而且,它几乎无法完成复杂的用户交互(如传统的 C/S 应用程序

和桌面应用程序中的用户交互)。这样的技术使得 Web 应用程序难以使用,支持成本高,

并且在很多方面无法发挥效应。因此,Web 2.0 的 大挑战是如何定义和提升用户体验。

目前有两种比较流行的解决方案:一种是在传统的 B/S 编程中融入 JavaScript/Ajax 的技

术(当前流行的技术热点,也是本书内容的基础);另外一种是 RIA(或许未来可能成

为主流的 UI 技术)。也有将 JavaScript/Ajax 作为 RIA 的一种方式,也归在 RIA 的技术

体系范畴中。

1.2.1 Rich Client Internet 编程

Rich Internet Applications(RIA)这些应用程序结合了桌面应用程序的反应快、交互

性强的优点,以及 Web 应用程序的传播范围广及容易传播的特性。RIA 简化并改进了 Web

应用程序的用户交互,这样,用户开发的应用程序可以提供更丰富、更具有交互性和响

应性的用户体验。RIA 将桌面型计算机软件应用的 佳用户界面功能性与 Web 应用程序

的普遍采纳和低成本部署,以及互动多媒体通信的长处集于一体,终于成就了一种可以

提供更直观、响应性和有效的用户体验应用程序。它所具备的桌面型计算机长处包括了

在确认和格式编排方面提供互动用户界面;在无刷新页面之下提供快捷的界面响应时间;

提供通用的用户界面特性如拖放式(Drag and Drop),以及在线和离线操作能力。Web 应

用程序的长处如立即部署、跨越平台可用性、采用逐步下载来检索内容和数据、拥有杂

志式布局的网页,以及充分利用被广泛采纳的互联网标准;通信的长处则包括双向互动

声音和图像。

客户机在 RIA 内的作用不仅是展示页面,它还可以在后台与用户请求异步地进行计算、

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

16

递送和检索数据,重新画出屏幕的一部分和密切综合使用声音和图像,这一切都可以在

不依靠客户机连接的服务器或后端的情况下进行。RIA 提供一个强劲的技术平台,使客

户机的能力复原到差不多与桌面型计算机软件应用或传统的 C/S 系统中的客户机能力

相似。它适合传统的 N 层开发过程,同时也能够和遗旧的环境集成以延展现有的应用程

序而无须进行修改。它也可以作为基础网络服务的互动表现层,允许用户在线和离线工

作。RIA 有能力解决各种复杂性问题,使需要复杂性的应用得以开发并且减少开发成本,

同时在很多时候这类应用之所以能够成型主要是拜 RIA 所赐。目前,比较流行的 RIA 技术

如下。

1.2.1.1 WPF/Silverlight

WPF 的全称是 Windows Presentation Foundation,是微软新发布的 Vista 操作系统的三

大核心开发库之一,其主要负责图形显示,所以叫做 Presentation(呈现)。

作为新的图形引擎,WPF 是基于 DirectX 的新增加了很多新的功能。其 2D 和 3D

引擎的强大之处,可以通过 Vista 的界面得以了解,再加上其对 Aero 图形引擎的支持,

将图形渲染功能更是增加到极致。顺便提一下,Aero 是专门为 3D 桌面开发的引擎,可

以让桌面实现神奇的 3D 翻转,这是操作系统有史以来的一次神奇尝试;虽然对硬件设

定的要求也是惊人的,此前已有相关报道称,Vista 对显卡十分挑剔就是出于执行 Aero

的考虑。

WPF 其实不仅仅是图形引擎而已,它将给 Windows 应用程序的开发带来一次革命,因

为新的架构提供了一种全新的开发模式。当然对于普通使用者而言, 直观的就是界面越

来越漂亮,看起来越来越舒服了;但对于开发人员而言,界面显示和程序将得到更好地分

离,这与从前的桌面应用程序开发有很多不同(界面显示和程序是融合在一起的),这是比

较具有革命性的改变之一。还有就是桌面应用程序和浏览器应用程序的融合,即 Silverlight,

将为基于 WPF 的应用程序提供全面的浏览器支持,这意味着未来开发出的应用程序将可以

基于浏览器在不同的操作系统上执行。当然,由于目前还在开发中,我们并不确定会不会

有一定的限制,根据 Silverlight 开发组的定义,Silverlight 仍然是 WPF 的子集,而不是后

继版本。总体而言,WPF 的前景应该是一片光明。

随着 Vista RTM 的发布,微软新一轮的技术推广已经开始。其实在此之前,WPF 已经

有很大的推广,因为 CTP 版本已经发布有一段日子了。当然很多开发人员主要以技术研究

为主,也有少数公司已经开始从事基于 WPF 的产品研发工作。

目前已有很多人开始考虑或者已经转向 WPF,一场新的学习热潮已经开始。但

根据笔者 近的学习和了解,国内关于 WPF 的资料很少,除了 MSDN 提供的资料以

外,基本都是来自国外的资料,有些则是国外开发人员 Blog 上的资料,当然都是英

文的。

CHAPTER 第 1 章 Web UI 编程综述

17

1

1.2.1.2 Flash/Flex/AIR

Flex 是 Macromedia(现已被 Adobe 公司收购)发布的 Presentation Server(展现服务),

它是 Java Web Container 或者.Net Server 的一个应用,根据.mxml 文件(纯粹的 XML 描述

文件和 ActionScript)产生相应的.swf 文件,传送到客户端,由客户端的 Flash Player 或者

Shockwave Player 解释执行,给用户以丰富的客户体验。

Macromedia Flash 是强大的矢量动画编辑工具,在做动画起家之后,Flash 一直在谋

求 RIA,(Rich Internet Application,富网络应用)的霸主地位, 有影响的是,已经推

出了面向对象的编程脚本 ActionScript 3.0,并且建立起类似于 Java Swing 的类库和相应

的 Component(组件)。Flex 是通过 Java 或者.Net 等非 Flash 途径,解释.mxml 文件组织

Component,并生成相应的.swf 文件。Flex 的 Component 和 Flash 的 Component 很相似,

但是有所改进和增强。

运用 Flash 是完全可以做到 Flex 的效果的,那为什么还需要 Flex 呢?这主要是为了

吸引更多的程序开发人员。Flash 天生是为 Designer(设计者)设计的,界面还有 Flash

的动画概念和程序开发人员是格格不入的,为了吸引更多的 JSP/ASP/PHP 等程序开发人

员,Macromedia 推出的 Flex 用非常简单的.mxml 来描述界面给 JSP/ASP/PHP 程序人员使

用。

Adobe Integrated Runtime(AIR)是一个跨操作系统的运行时,利用现有的 Web 开发

技术(Flash、Flex、HTML、JavaScript、Ajax)来构建富 Internet 应用程序并部署为桌面应

用程序。换句话说,它可以将 Web 应用搬到浏览器之外,同样没有 C/S 笨重、部署不容易

的特点,用户所做的只是下载一个 air 并安装即可。

AIR 可以让你用 熟练的技术来开发你所见过的 具用户体验的 RIA 程序,例如,一

个 AIR 程序可以使用如下一种或多种组合技术构建。

Flash/Flex/ActionScript;

HTML/JavaScript/CSS/Ajax;

PDF 可嵌入任何应用程序中。

作为结果,AIR 应用程序可以是:

基于 Flash 或 Flex,应用程序根内容(理解为容器)为 Flash/Flex(SWF);

基于 Flash 或 Flex 的 HTML 或 PDF,应用程序的根内容为基于 Flash/Flex(SWF)

的 HTML(HTML、JS、CSS)或 PDF;

基于 HTML,应用程序根内容为 HTML、JS、CSS;

基于 HTML 的 Flash/Flex 或 PDF,应用程序根内容为基于 HTML 的 Flash/Flex

(SWF)或 PDF。

用户使用 AIR 应用程序的方式和使用传统桌面应用程序是一样的,当运行时环境安装

好后,AIR 程序就可以和其他桌面使用程序一样运行了,如图 1-2 所示。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

18

图 1-2 AIR 应用架构

因为 AIR 是应用程序运行时环境,因此它很小且对用户来说不可见。运行时环境

提供了一套一致的跨操作系统平台和框架来开发和部署应用程序,因此你的程序不必

到每个平台上进行测试,在一个平台上开发好就可以在其他平台上运行了。这有很多

好处:

开发 AIR 应用程序不必做额外的跨平台工作,节省了时间,因为跨平台的工作 AIR

都帮我们做好了(只要其他平台能支持 AIR)。

比起 Web 技术及其设计模式,AIR 应用程序开发迅速,它允许将 Web 开发技术搬

到桌面上来而不用另外去学习桌面应用程序开发技术或复杂的底层代码,这比起低

级语言如 C 和 C++更容易学习,且不用去处理每个操作系统复杂的底层 API。如果

与微软的RIA技术相对照,Flex犹如Silverlight,AIR犹如WPF,MXML犹如XAML。

1.2.1.3 Laszlo

Laszlo 是开发和将富 Internet 应用程序(RIA)发布到 Web 上的开源平台。

Laszlo 平台由 LZX 标记语言和 Laszlo 表示服务器组成。

LZX 是基于 XML 和 JavaScript 的描述语言,类似于 XUL 和 XAML;LZX 使能声

明式、基于文本的开发过程,支持快速原型和软件开发 佳实践。

Laszlo 表示服务器(LPS)是一个编译 LZX 应用程序为目标运行环境中可执行二进

制的 Java Servlet;Laszlo 目前的目标是 Flash Player,LPS 将 LZX 应用程序编译成

SWF 字节码,为任何使能 Flash 5 及其以后版本的 Web 浏览器提供这些被编译应用

CHAPTER 第 1 章 Web UI 编程综述

19

1

程序的服务和缓存,同时为后端的 XML 数据源和 Web 服务代理应用程序请求。

Laszlo 应用架构如图 1-3 所示。

图 1-3 Laszlo 应用架构

Laszlo 的主要特征如下:

(1)使用基于 XML 和 JavaScript 的代码开发标准的 RIA。

(2)发布后运行在 Linux、UNIX、Windows 或 Mac OS X 下的任何 J2EE 应用服务器

或 Java Servlet 容器中。

(3)运行在 Flash 5 及其以后版本的 Web 浏览器中,达到所有 Web 使能桌面系统

的 97%。

1.2.1.4 JavaFX

2008 年 12 月 5 日,Sun 微系统公司发布了 JavaFX 技术的正式版,利用 JavaFX 编程

语言可以开发富 Internet 应用程序(RIA)。JavaFX Script 编程语言(以下称为 JavaFX)是

Sun 微系统公司开发的一种声明式、静态类型(declarative、statically typed)脚本语言。JavaFX

技术有着良好的前景,包括可以直接调用 Java API 的能力。因为 JavaFX 脚本是静态类型,

它同样具有结构化代码、重用性和封装性,如包、类、继承、单独编译和发布单元,这些

特性使得使用 Java 技术创建和管理大型程序变为可能。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

20

JavaFX 脚本是一种声明式、静态类型编程语言,具有一等函数(first-class functions)、

声明式的语法、列表推导(list-comprehensions)及基于依赖关系的增量式求值(incremental

dependency-based evaluation)等特征。JavaFX 脚本为多种多样的操作提供了声明式、无中

间程序逻辑的语法,这些操作包括创建 2D 动画、设置属性或者声明在模式和视图对象之

间的绑定依赖关系。JavaFX 架构如图 1-4 所示。

图 1-4 JavaFX 架构

1.2.2 Ajax 编程

随着 Ajax 迅速地引人注目起来,开发人员对这种技术的期待也会迅速地增加。

就如同任何新技术一样,Ajax 的兴旺也需要一整套开发工具/编程语言及相关技术系

统来支撑。

如名字所示,Ajax 的概念中 重要也 容易被忽视的是,它也是一种 JavaScript 编程

语言。JavaScript 是一种黏合剂,使 Ajax 应用的各部分集成在一起。在大部分时间内,

JavaScript 通常被服务器端开发人员认为是一种企业级应用不需要使用的东西应该尽力避

免。这种观点来来自于以前编写 JavaScript 代码的经历:繁杂而又易出错的语言。类似的,

它也被认为将应用逻辑任意地散布在服务器端和客户端中,这使得问题很难被发现而且代

码很难重用。在 Ajax 中 JavaScript 主要被用来将用户界面上的数据传递到服务器端并返回

结果。XMLHttpRequest 对象用来响应通过 HTTP 传递的数据,一旦数据返回到客户端就可

以立刻使用 DOM 将数据放到网面上。

CHAPTER 第 1 章 Web UI 编程综述

21

1

1.2.2.1 XMLHttpRequest

XMLHttpRequest 对象在大部分浏览器上已经实现而且拥有了一个简单的接口,允许数

据从客户端传递到服务器端,但并不会打断用户当前的操作。使用 XMLHttpRequest 传递

的数据可以是任何格式,虽然从名字上建议是 XML 格式的数据。

开发人员应该已经熟悉了许多其他 XML 相关的技术。XPath 可以访问 XML 文档中

的数据,但理解 XML DOM 是必需的。类似的,XSLT 是 简单而快速地从 XML 数据生

成 HTML 或 XML 的方式。许多开发人员已经熟悉了 XPath 和 XSLT,因此 Ajax 选择 XML

作为数据交换格式是有意义的。XSLT 可以被用在客户端和服务器端,它能够减少大量的

用 JavaScript 编写的应用逻辑。

1.2.2.2 CSS

为了正确地浏览 Ajax 应用,CSS 是 Ajax 开发人员所需要的一种重要武器,CSS

提供了从内容中分离应用样式和设计的机制。虽然 CSS 在 Ajax 应用中扮演着至关重

要的角色,但它也是构建跨浏览器应用的一大阻碍,因为不同的浏览器厂商支持各种

不同的 CSS 级别。

1.2.2.3 服务器端

不像在客户端,在服务器端 Ajax 应用还是使用建立在如 Java、Net 和 PHP 语言基础上

的机制,并没有改变这个领域中的主要方式。

既然如此,我们对 Ruby on Rails 框架的兴趣也就迅速增加了。在一年多前,Ruby

on Rails 已经吸引了大量开发人员基于其强大功能来构建 Web 和 Ajax 应用。虽然目

前还有很多快速应用开发工具存在,但 Ruby on Rails 看起来已经具备了简化构建

Ajax 应用的能力。

1.2.2.4 开发工具

在实际构建 Ajax 应用中,你需要的不只是文本编辑器。既然 JavaScript 是非编译的,

它可以容易地编写和运行在浏览器中;然而,许多工具提供了有用的扩展,如语法高亮

和智能完成。

不同的 IDE 提供了对 JavaScript 支持的不同等级。Microsoft Visual Studio 的新版本

中也改善了对 Ajax 的支持,来自 JetBrains 的 IntelliJ IDEA 是一个更好的 JavaScript 开

发的 IDE。Eclipse 包含了两个免费的 JavaScript 编辑器插件和一个商业的来自 ActiveStat

的 Komodo IDE。

JavaScript 和 Ajax 开发中的另一个问题是调试困难。不同的浏览器提供不同的通常是

隐藏的运行时错误信息,而 JavaScript 的缺陷如双重变量赋值(通常是由于缺少数据类型)

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

22

使得调试更加困难。在 Ajax 的开发中,调试就更复杂了,因为需要标识究竟是客户端还是

服务器端产生的错误。在过去,JavaScript 的调试方法是删除所有代码,然后一行行地增加

直到错误出现。现在,更多的开发人员使用为 IE 准备的 Microsoft Script Debugger 和为

Mozilla 浏览器准备的 Venkman。

1.2.2.5 浏览器兼容性

JavaScript 编程的 大问题来自于不同的浏览器对各种技术和标准的支持。构建一个运

行在不同浏览器(如 IE 和火狐)上的框架是一项困难的任务,因此几种 AjaxJavaScript 框

架或者生成基于服务器端逻辑或标记库的 JavaScript,或者提供符合跨浏览器 Ajax 开发的

客户端 JavaScript 库。一些流行的框架包括:Ajax.NET、Backbase、Bitkraft、Django、DOJO、

DWR、MochiKit、Prototype、Rico、Sajax、Sarissa 和 Script.aculo.us。

这些框架给开发人员更多的空间,使得他们不需要担心跨浏览器的问题。虽然这

些框架提升了开发人员构建应用的能力,但由于厂商已经开发了更细节的用户界面的

打包组件解决方案,因此在 Ajax 组件市场中需要考虑其他一些因素。例如,提供通用

用户界面的组件如组合框和数据栅格的几个厂商,都可以被用来在应用中创建良好的

通过类似于电子数据表的方式来查看和编辑数据的体验。但这些组件不仅封装了组件

的用户界面,而且包括了与服务器端数据的通信方式,这些组件通常使用基于标记的

方式来实现如 ASP.NET 或 JSF 控件。

1.2.3 Ajax in JSF 编程

Ajax 是 火爆的 Web 2.0 技术之一,JSF 是 J2EE Web 应用开发的标准,并且拥有

多的开发人员支持。如何将两种技术融合在一起,是非常受关注的一个问题。当第 1

篇关于在 JSF 中使用 Ajax 的文章在 TheServerSide 网站上发布时,受到了无数 Ruby 爱

好者的嘲讽,因为在 RoR 中仅仅需要两三行代码就能实现的功能,而 JSF 程序员不得

不花费大量的时间和精力来编写众多的基础性代码才能实现。再一个原因就是,由于 JSF

本身生命周期(Lifecycle)的影响,在 JSF 中使用 Ajax 技术并不像普通的 Ajax + Servlet

那样简单。还有,就是众多的标准 JSF 组件并不支持 Ajax 功能,要为这些组件重写一

遍增加 Ajax 功能的话,可以想想任务是多么的庞大。

Ajax4jsf 的出现令众多 JSF 爱好者眼前一亮,它的目标定位在用于 JSF 应用程序的丰

富(Rich)用户界面开发,而更加关键的是 Ajax4jsf 完全与 JSF 生命周期紧密结合,其他

框架只给你访问 Managed Bean 的能力,Ajax4jsf 能够在 Ajax 请求响应(Request-Response)

期间利用 Action、值变化监听器(Value Change Listener)、调用服务器端校验和类型转换器。

这个框架使用一个组件库来实现,它能够为现有的 JSF 页面添加 Ajax 功能,而不需要编写

任何 JavaScript 代码或使用新的 Ajax Widget 来代替现有的组件。Ajax4jsf 能够实现整个页

面范围的 Ajax 支持,而不是传统的组件范围支持。

CHAPTER 第 1 章 Web UI 编程综述

23

1

除了核心 Ajax 功能之外,Ajax4jsf 还支持各种资源的管理,如图片、JavaScript 代码和

CSS 样式表等,这个资源框架让你能够很方便地把这些资源与你自定义组件的代码一起打

包到 Jar 文件中。这个资源框架还有另外一个特性就是能够快速生成图片,创建的方式类

似于使用 Java Graphic 2D 包创建图片。

Ajax4jsf 的巨大成功,使一些 JSF 框架实现速度的支持 Ajax 功能,其中比较成功的有

Tomahawk、ICEfaces、QuipuKit 和 RichFaces。Ajax4jsf 与 RichFaces 同出于 JBoss 实验室,

而且目前两个项目已经合并为一个项目,仅从这一点就让笔者无法摆脱对 RichFaces 的钟

爱。本书的第 6 章将向你展示 RichFaces 是如何让开发 Ajax 应用变得简单的。

1.3 本章小结

本章概要性地介绍了 Web 2.0、SOA 与 Web 2.0、UI 编程技术。其中 UI 编程技术将是

本书后续章节讲解的重点,Ajax 与 JavaScript 的结合、Ajax 与 JSF 的结合角度来阐述 UI

编程。本书的主要目标也是为 Java 程序员在 UI 编程方面提供更多的帮助和思路。

CHAPTER

2

2.1 Ajax 的基本知识

2.1.1 Ajax 基本概念

现在,网络已经深入到人们工作和生活中的各个方面,例如新闻、通信、购物、交友

等,网络上丰富多彩的内容,深深吸引着人们的眼球,甚至很多人都有网络上瘾症的倾向,

在一些青少年身上则表现得更重一些。但是在互联网刚刚出现的时候,互联网远远没有现

在这样富有表现力,那还是 原始的 HTML 时代。那个时候,HTML 是互联网上的交互标

准,它主要是用来显示文字和图片,用户通过单击超链接,从一个页面导航到另一个页面。

这种简单的发布模式和查看模式有助于促使互联网迅速发展, 终形成今天这种全球互联

互通的局面。但是随着用户数量的增多,用户对这种以页面为中心的查看模式也提出了越

来越多的要求,例如希望页面像桌面应用一样富有交互性等,这些需求促使 Web 进一步发

展,CGI、ASP、JSP 等技术纷纷出现,给用户带来了更多的交互性,但是这些技术并没有

很好地实现像桌面应用一样的 Web 应用。

真正让世界眼前一亮的是,Google 的 Google Maps、Google Suggest、Gmail 等系列产

品的出现,这些应用缩短了用户同页面的交互时间,使得 Web 应用有着同桌面应用一样的

体验,这标志着 Ajax 技术的诞生。

Ajax 是 HTML 浏览器上运行的一种表现方式和一系列技术,它提高了页面和用户的交

第 2 章

Ajax 基础知识

本章主要介绍 Ajax 技术的基本知识。Web 2.0 是多方面的,

而 Ajax 则是组成 Web 2.0 的一个重要的技术手段。本章首先介

绍 Ajax 的基本概念,然后详细分析构成 Ajax 的一些技术。阅读

完本章后,读者能够对 Ajax 有一个全面而深入的了解。

CHAPTER 第 2 章 Ajax 基础知识

25

2

互性,在保留 Web 应用在服务器端部署,用户无须安装客户端优势的同时,提供了桌面应

用的风格和体验,使得 Ajax 应用迅速发展起来。以前的 Web 应用,都是用户与页面交互

后,页面开始更新,用户在页面更新完成前只能等待;Ajax 则使页面局部更新,其他页面

内容保持不变,这样,Ajax 促进了 HTML 从 DHTML 到 RIA(Rich Internet Application,

富网络应用)的转变。

对于从事 Web 开发的专业人员而言,Ajax 是很有吸引力的,它在保留了基础应用服务

器端化的同时,使用户获得了桌面应用的效果;用户对使用这种技术的应用也很有好感,

因为它使得 Web 应用更像一个桌面应用程序,并且还添加了很多额外的功能。

关于 Ajax 说了这么多,那么到底什么是 Ajax 呢?Ajax 是“Asynchronous JavaScript and

XML”(异步 JavaScript 和 XML)的缩写,是一种使用客户端脚本并能与 Web 服务器动态

交互的客户端 Web 开发技术。

具体而言,Ajax 包含以下技术:

基于 XHTML 和 CSS 标准的表示;

使用 DOM(Document Object Model,文档对象模型)进行动态显示和交互;

使用 XMLHttpRequest 与服务器进行异步通信;

使用 JavaScript 绑定一切。

Ajax 使用 HTML 和 CSS 标准化呈现,使用 DOM 实现动态显示和交互,使用 XML 和

XSTL 进行数据交换与处理,使用 XMLHttpRequest 对象(这个对象是浏览器提供的对象,

下面将详细讨论)进行异步数据读取,使用 JavaScript 绑定上面所有的东西。

因此,Ajax 不是一个新的技术,甚至 Ajax 中 为重要的 XMLHttpRequest 对象(简称

XHR,下同),也早在 1999 年微软发布 IE 5 的时候就出现了,因此说 Ajax 是“新瓶装旧

酒”。也就是 Jesse James Garrett 在 2005 年发表了一篇文章“Ajax: A New Approach to Web

Applications”,对 XHR 对象及相关的 XML 和 JavaScript 进行了系列的阐述和包装,才有了

Ajax 这个名称的出现。

2.1.2 Ajax 的基本特点

虽然 Ajax 不算新技术,但是却有其大行其道的理由,Ajax 的主要特点如下。

2.1.2.1 持续、动态的用户体验

HTML 应用和 Ajax 应用的主要区别在于,在用户和应用交互的同时,Ajax 还可以与

后台的服务器进行通信。下面是两种交互模式的区别。

图 2-1 显示了用户在使用 HTML 时的主要行为,即典型的“点击-等待-页面刷新”模

式。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

26

图 2-1 “点击-等待-页面刷新”模式下的 HTML 体验示意图

而基于 Ajax 的应用,用户体验如图 2-2 所示,它会给用户带来一个比较平滑、持续的

用户体验。Ajax 通过 Ajax 引擎与后台服务器交互,发送数据请求,而不是发送页面请求,

这样就减少了页面刷新的次数,有效地减少了用户的等待时间,带给用户 直接的体验就

是 Ajax 应用在浏览器上是持久而平滑的。

图 2-2 “点击-继续点击”模式下的 Ajax 体验示意图

2.1.2.2 桌面应用风格的界面

Ajax 应用支持富用户界面,在风格上更加贴近可安装的桌面应用。在 Ajax 中,典型的

富用户界面包括以下元素:

标准的用户界面控制元素(如按钮、输入框、下拉框等);

高级用户控制元素(如 Tab 框、日历组件、树组件和数据表格组件等);

灵活、动态的布局容器。这些布局容器可以根据内嵌内容和窗体大小自动调节自身

的布局大小;

浮动的面板和模态对话框;

动画和各种动画效果;

剪切、粘贴及拖拽效果等。

2.1.2.3 网络编程

Ajax 的网络编程能力,可以使 Ajax 应用与 Web 服务器异步交换数据成为可能,并支

持下一代合作性应用。Ajax 库配合服务器端的能力,就可以实现异步向客户端发送数据。

CHAPTER 第 2 章 Ajax 基础知识

27

2

现在您应该对什么是 Ajax,以及 Ajax 能够做什么有一个初步的了解了吧。下面的章节

将详细介绍有关 Ajax 的知识。

2.2 一切从浏览器说起

上面我们提到 Ajax 包含 DOM、JavaScript、XML、JSON 等各种技术,那么,Ajax 又

是怎样将这些技术整合在一起的呢?“知今必先通古”,要想了解这一切,还需要从互联网

和 Web 浏览器的发展历史开始说起。

2.2.1 互联网和 HTML 的诞生

1985 年,美国国家科学基金组织(NSF)采用 TCP/IP 协议将分布在美国各地的 6 个为

科研教育服务的超级计算机中心互联,并支持地区网络,形成 NSFnet,这标志着 Internet

的诞生。

图 2-3 “万维网之父”、HTML 发明人——Tim Berners-Lee

1988 年,Internet 开始对外开放。1989 年,Tim Berners-Lee(蒂姆·本尼斯李,见

图 2-3)根据当时的印刷行业规范 SGML 制定了 HTML(Hyper Text Markup Language)。

超文本是指信息内容的组织方式是非线性的,可以根据人的联想方式进行跳转,常见的

文字、课本、电视、录像等是线性的、不能跳转的,只能固定从一页到另一页或从一个

镜头接另一个镜头,而人类的思维方式是非线性的,可以对某些概念进行发散和跳转。

1945 年,美国计算机科学家范尼瓦·布什就提出了“有思维的人和所有的知识之间建

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

28

立一种新的关系”,但是他没有提出明确的概念。美国人泰得·纳尔逊在 1965 年 先提

出了超文本这个词汇,后来得到世界的公认,成了这种非线性信息管理技术的专用词汇。

而 HTML 就是非线性内容的表达语言,允许从当前阅读位置直接切换到超文本链接所

指向的目标;允许人们透明地共享网络上的信息,即使人们使用的计算机差别很大。

HTML 具备以下特点:

独立于平台,即独立于计算机硬件和操作系统。这个特性是至关重要的,因为在这

个特性中,文档可以在具有不同性能(即字体、图形和颜色差异)的计算机上以相

似的形式显示文档内容。

超文本。允许文档中的任何文字或词组参照另一文档,这个特性将允许用户在不同

计算机中的文档之间及文档内部漫游。

精确的结构化文档。该特性将允许某些高级应用,如 HTML 文档和其他格式文档

间互相转换,以及搜索文本数据库。

由于 HTML 的这些特点,HTML 一经推出就得到广泛的应用,并推动了 Internet 的发

展。1991 年,在连到 Internet 的计算机中,商业用户首次超过了学术界用户,这成为 Internet

发展史上的一个里程碑。

使用 HTML 标识的文档仅仅是一个文档,而 Web 浏览器则是 HTML 的解析器和

HTMLHTMLhtml ,渲染器。为了更好地表现 HTML,1993 年,美国超级计算机应用中心

(NCSA)开始构建 Web 浏览器,把自己需要的特性添加到 HTML 中,包括直接插入图

形和图表。这时的浏览器主要还是在学术界使用,显示在屏幕上的也只有一行一行的代

码与数据,并没有图形用户界面。伊利诺斯州大学的学生马克·安德森改变了这种状况,

发布了世界上第一个只需通过鼠标点击图形界面就可以浏览互联网的程序——Mosaic 浏

览器,1993 年第一个面向普通用户的 Mosaic 1.0 发布。Mosaic 的 大特色就是具有方便

易用的图形界面,人们通过 Mosaic,可以方便地阅读嵌有位图、照片和图表的 HTML 文

档,并且可以自由地跳转。Mosaic 是第一个被人普遍接受的浏览器,它让许多人了解了

Internet,因此马克·安德森也被称为互联网浏览器之父。

1994 年,当时开发 Mosaic 浏览器的马克·安德森和其他人联合成立 Netscape(网景)

公司,并在当年年底推出了 Mozilla 1.0,这是当时第一款商业 Web 浏览器。NCSA 开发的

Mosaic 浏览器是专门针对学术界使用的,Netscape 开发的 Mosaic 则强调其浏览器是面向普

通用户和普通带宽的,并且它不论在技术上还是在市场宣传方面都要比 NCSA 强大。

Netscape 的 Mosaic(后来改名为 Mozilla)浏览器提供了很多强大的浏览器新特性,并承诺

其浏览器支持当时所有的主流操作系统:Windows、Macintosh 和 UNIX,还把当时 Internet

上 3 个主要的技术 Web、Mail 和 News Group 都集成到 Mozilla 浏览器中,这一切都极大地

提高了 Mozilla 的市场占有率,同时也加快了 HTML 和 Web 的发展。1994 年年底,Internet

上 Web 应用的网络流量首次超过电信系统的流量,位居 FTP 网络流量之后;到 1995 年 4

月,Web 应用的流量就超过了 FTP,成为 Internet 上 主要的应用了。

刚开始 Mozilla 在市场上的占有率很高,几乎没有什么浏览器能和其抗衡。1995 年

CHAPTER 第 2 章 Ajax 基础知识

29

2

1 月,Microsoft 在发布 Windows 95 操作系统的同时,也发布了 Internet Explorer 1.0,这款

Web 浏览器几乎就是 Mosaic 的翻版,同时又是和操作系统捆绑在一起的,因此受到人们的

诟病,但是也免除了用户手动安装 Web 浏览器的麻烦,使得更多的人得以接触 Internet。

从此 Web 浏览器市场上开始了 Netscape 和 Microsoft 两家鼎立的局面,并展开了长达十多

年的竞争。在 1995 年夏,Netscape 凭借浏览器强大的功能(框架、Java、JavaScript 和插件

支持),占据 80%的市场,在 1995 年中期到 1996 年年末的这一段时间中,两家公司竞争更

加白热化,大概每个星期都会有一个 Web 浏览器的版本出现,竞争的结果则是不断有新的

Web 浏览器版本和新的 HTML 特性出现。这一时期的 HTML 发展很快,新的 HTML 标记

不断被浏览器引入,有一些新标记流行起来,而有一些又消失了;有些增加部分设计得很

糟,很多甚至不遵从 SGML 规范,这使得 HTML 的发展出现了混乱。

2.2.2 HTML 的发展

1995 年 11 月,在瑞士日内瓦举行的第一次 WWW 会议上成立了一个 HTML 工作

小组(World Wide Web Consortium's HTML Working Group)。它的主要任务是把 HTML

形式化成一种 SGML DTD,称之为 HTML Level 2(HTML 2.0,RFC1866,由蒂姆·本

尼斯李 初设计的 HTML 被定义为 Level 1)。尽管有关的各方从来没有取得完全一致的

共识,但万维网联盟 HTML 工作组还集中了 1996 年万维网发展的成果,产生了 HTML

3.2 版本。

HTML 3.2 及其以后版本继承了以前版本的所有特点,并在以下几个方面有所发展。

更加明确了文档的结构和表现形式上的区别,以鼓励使用格式表(Style Sheet)来

取代使用元素和属性进行表现的方式。

更加优良的表单性能,加入了访问关键词(Access Key),构建对称的表单控件能力,

构建对称的下拉菜单控件的能力和动态标签。

在文本描述的标记中包含对象。

一种新客户端的文本包括在图像映射元素(<map>)中,使得网页设计者可以集成

文本和图像链接。

可以将替代图像的文本包括在图像元素(<img>)中,也可以将图像映射(image map)

包含到区域元素(<area>)中。

在所有元素中支持 title 和 language 两个属性。

更多的表格属性,包括 caption、column groups 和方便的非可视信息(Non-visual

Reading)的表现机制。

在 HTML 规范以后,HTML 的发展逐步走上有序发展的道路,但 HTML 并没有停止

发展,Microsoft 在 1997 年推出的 IE 4.0 中首次提出了 DHTML(Dynamic HTML,动态

HTML)的概念,成为近年来网络发展进程中 振奋人心也 具实用性的创新之一。在没

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

30

有 DHTML 之前,HTML 是静态的,一旦页面加载完毕,它就像一个画面,怎么看也不会

发生变化,如果要改变元素只能重新编写 HTML 代码。DHTML 改变了这种情况,Web 开

发人员可以编写相应的代码,在不刷新页面的情况下动态改变元素的属性,动态 HTML 就

意味着 Web 页面对用户有响应,即 HTML 能自动变化。

为了解 DHTML,我们可以先来看一看微软中国站点中的一个重要界面特性——当

浏览者将鼠标指针移动到页面导航条上时,会动态地弹出一个菜单,在该菜单中移动鼠

标,所指向的菜单项字体变为浅绿色显示;如果将鼠标指针移出菜单所在范围,则菜单

自动隐藏;如果将鼠标指针移动到导航条上另外一个区域,则会弹出另外一个菜单(见

图 2-4)。

图 2-4 Microsoft 网站 DHTML 效果图

为了实现这种自动变化的动态效果,首先,DHTML 需要一种方法查阅文档,跟踪页

面上的每个元素,包括从 小的<H6>标题和<HR>水平标尺,到表单中所有元素。从本

质上说,每当页面被加载时,浏览器就建立起一个数据库,每个标记就成为数据库中的一

个记录,这个数据库就称作文档对象模型(Document Object Model, DOM),即把整个文档

看作一个连续的对象模型(在下一节我们将详细讲解 DOM)。其次,DHTML 还需要一种

方法来控制一个文档中所有需要控制的元素,这可以通过一些脚本语言来完成,例如

JavaScript 或 VBScript,脚本元素可以精确控制页面上的各个节点,使之发生预想的变化,

如在一个菜单项中对应的内容。为了表现页面上的变化,还需要用一些设置页面的属性,

如字体颜色变化等,这一般由 CSS 来完成。因此,DHTML 实际上是 DOM、JavaScript 和

CSS 的集合,它们共同组成了 DHTML 的全部内容。

凭借 IE 4.0 推出的 DHTML 等强大的功能,IE 打败了 Netscape,逐渐成为 Web 浏览器

市场的主流,并把这种霸主地位一直保持到今天。不久W3C的HTML工作组吸收了DHTML

的优点,正式承认了在 Web 页面中的动态效果,在代号为 Cougar 的 HTML 4.0 版本中,

CHAPTER 第 2 章 Ajax 基础知识

31

2

W3C 把动态超文本的实现分为了 3 个部分:脚本、支持动态效果的浏览器和 CSS,而支持

动态效果的浏览器,则是指实现了 DOM 规范的 Web 浏览器。

时至今日,微软的 IE 已经成为市场占有率 高的浏览器,Netscape 的市场份额逐渐

缩小,Netscape 在 2008 年 3 月 1 日正式退出浏览器舞台。但是互联网上的浏览器之战并

没有结束,反而愈演愈烈,基于 Netscape 内核的 Firefox 发展迅速,不断抢占 IE 的市场

份额,Opera、Safari 等也在不断崛起;而互联网巨头 Google 也不失时机地在 2008 年 9

月 2 日正式推出了自己的浏览器 Chrome,以整合自己在互联网上的应用。因此,正如前微

软首席执行官比尔·盖茨在总结 IE 浏览器市场份额超过 Netscape 时所说的那样:“这一切

都将继续”。

从 HTML 的发展历史我们可以看出,HTML 和 Web 浏览器的发展是相互促进的。从

某种意义上来说,Web 浏览器就是互联网的引擎,Web 浏览器的不断发展,使 HTML 的表

达能力更加丰富;Web 浏览器提供的功能越多,HTML 的表现力也就越丰富。作为例证,

Netscape 先在 Mozilla 上提供了 JavaScript、框架和插件等功能,这些功能提高了 HTML

页面和用户的交互性,很快 IE 也提供了类似的功能。而 有证明性的例子则是 IE 提供的

XMLHttpRequest 对象,也就是我们所说的 Ajax 功能,是 IE5.0 中推出的一个特性,允许用

户局部刷新 HTML 页面,而这个特性很快得到 Mozilla 和 Safari 等 Web 浏览器支持,成为

事实上的标准。

从 HTML 和 Web 浏览器的发展历史中我们也可以看到,Web 应用从来都不是单纯的

HTML 开发,还要涉及 Web 浏览器特性的开发。Web 浏览器支持的特性很多,而我们需要

关注的是那些 主流的特性,如 DOM、CSS、JavaScript 等,这些内容将在接下来的章节

中介绍。

2.3 DOM 简介

自从 IE 4.0 和 Netscape Navigator 4.0 开始支持 DHTML(Dynamic HTML,动态

HTML)以来,Web 开发人员首次能够在不重新载入网页的情况下修改网页外观。这是

Web 技术的一次飞跃,但是由于缺乏统一的标准,Netscape 和微软各自开发出不同的

DHTML,从而结束了 Web 开发者只编写一个 HTML 页面就可以在所有的浏览器中运行

的时期。

为了保证 Web 的跨平台特性,Web 通信标准的团体 W3C(World Wide Web

Consortium)开始制定 DOM 标准。1998 年 10 月,W3C 提出了 DOM Level 1,它由两个

模块构成,即 DOM Core 和 DOM HTML,前者提供了基于 XML 的文档的结构图,以方

便访问和操作文档的任意部分;后者提供了一些 HTML 专用的对象和方法,从而扩展了

DOM Core。

W3C 提出的 DOM 模型(Document Object Model,文档对象模型),是针对 XML 的基

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

32

于树形结构的 API,是 HTML 和 XML 应用程序的标准接口(API),把 HTML 或 XML 文

件规划成由节点层次构成的树,HTML 或 XML 页面的每一个部分都是节点的衍生物;同

时 DOM 也提供了操纵这些节点的标准方法。通过这些方法,就可以动态地添加、修改或

删除节点,动态更改页面信息而不需要重新解析页面,从而为统一 IE 和 Mozilla 两大浏览

器平台的动态 HTML 提供了标准。

DOM 是一个 API 标准,目前有很多语言都实现了 DOM 标准。Web 浏览器中的 DOM

已经用 ECMAScript 实现了,现在是 JavaScript 语言中一个很大的组成部分,这极大地增强

了 JavaScript 的语言能力。

应用 DOM,可以把任意 HTML 文件或 XML 文件规划为一个树形节点的集合,比如图

2-5 所示的 HTML 页面。

图 2-5 简单的 HTML 页面

这是一个比较简单的 HTML 页面,应用 DOM,可以把这个页面划分为一些节点的集

合,如图 2-6 所示。

图 2-6 DOM 节点示意图

从图 2-6 可以看出,html 作为根节点出现,head 和 body 是 html 的子节点,这样,整

个页面就可以表示为一个树形的节点集合。DOM 定义了节点的接口,以及多种节点类型来

表示 XML 节点的多个方面。下面是常用的几种节点类型。

Document—— 高层的节点,所有的其他节点都附属于它。

Element——表示起始标签和结束标签之间的内容。例如上例中的<title>和</title>,

这是唯一可以表示特性和子节点的节点类型。

CHAPTER 第 2 章 Ajax 基础知识

33

2

Attr——代表一对特性名和特性值。这个节点类型不能包括子节点。

Text——代表在 XML 的起始标签和结束标签之间的普通文本,或者 CData Section

内包含的普通文本。这个节点类型不能包含子节点。

CDataSection——<![CDTATA[]]的对象表现形式。这个节点类型仅能包含文本节点

作为子节点

除节点外,DOM 还定义了一些助手对象,它们可以和节点一起使用,但不是 DOM 文

档必有的部分。

2.3.1 HTML DOM 和 BOM

在 IE 和 Netscape Navigator 3.0 中提供了一种特性——BOM(Browser Document Model,

浏览器对象模型),可以独立于内容之外对浏览器窗口进行访问和操作。使用 BOM,可以

移动窗口、改变状态栏中的文本,以及执行与页面内容不直接相关的其他动作。BOM 是

JavaScript 实现的一部分,没有相关的标准。

BOM 主要处理浏览器窗口和框架,这些扩展包括:

弹出新的浏览器窗口(Window 对象);

移动、关闭浏览器窗口,以及调整窗口大小;

提供 Web 浏览器详细信息的导航对象;

提供装载到浏览器中页面的详细信息的定位对象;

提供用户屏幕分辨率详细信息的屏幕对象;

对 Cookie 的支持;

IE 扩展了 BOM,加入了 ActiveXObject 类,可以通过 JavaScript 实例化 ActiveX 对象。

BOM 基本结构示意图如图 2-7 所示。

图 2-7 BOM 基本结构示意图

由于 BOM 对象的存在,使得通过 JavaScript 操作页面和浏览器更加方便,本文中提出

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

34

的记录用户行为的方法主要就是通过读取 BOM 对象的有关属性实现的。

2.3.2 DOM 事件

事件是 DOM 的一部分,用户操作浏览器页面时,就会触发各种事件(event),比如页

面载入完成时,会出现一个事件;用户点击按钮时,点击也是一个事件。

事件有不同的类型,根据触发事件的事务和事件发生的对象,可将浏览器中发生的事

件分为几个类型,DOM 标准定义了如下事件:

1. 鼠标事件

鼠标事件是 Web 上 常用的事件类型,主要有以下事件:

click——用户点击鼠标左键时发生(如果右键也按下则不发生)。当用户的焦点在按

钮上,按下回车键时,同样会触发这个事件。

dblclick——用户双击鼠标左键时发生(如果右键也按下则不发生)。

mousedown——用户点击任意一个鼠标按钮时发生。

mouseup——用户松开任意一个鼠标按钮时发生。

mouseout——鼠标指针在某个元素上,且用户正要将其移出元素的边界时发生。

mouseover——鼠标移出某个元素,到另一个元素上时发生。

mousemove——鼠标在某一个元素上时持续发生。

2. 键盘事件

键盘事件由用户对键盘的动作引起,主要有以下几个键盘事件:

keydown——用户在键盘上按下某按键时发生,一直按着某按键,它则会不断触发。

keypress——用户按下一个按键,并产生一个字符时发生。一直按下按键,则一直会

发生。

keyup——用户释放按着的按键时发生。

所有的元素都支持键盘事件,但是在文本框中输入文字时, 容易看到这些事件。

3. HTML 事件

HTML 事件是由 IE 4.0 和 Netscape Navigator 4.0 的开发人员创建的事件模型中遗留下

来的事件组成的,主要有如下几个事件。

(1)load 和 unload 事件

load 事件在页面完全载入后,在 window 对象上触发;所有的框架都载入后,在

框架集上触发;<img/>完全载入后,在其上触发;对于 object 对象,当其完全载入后

触发。

CHAPTER 第 2 章 Ajax 基础知识

35

2

unload 事件在页面完全卸载后,在 window 对象上触发;所有的框架都卸载后,

在框架集上触发;<img/>完全卸载后,在其上触发;对于<object/>元素,完全卸载后

触发。

(2)abort 事件

用户停止下载过程时,如果<object/>元素还没有完全载入,就在其上触发。

(3)error 事件

JavaScript 对象出错时,在 window 对象上触发;某个<img/>对象无法载入时,在其上

触发;<object/>元素无法载入时触发;框架集中的一个或多个框架无法载入时触发。

(4)select 事件

用户选择文本框中的一个或多个字符时触发(<input/>或<textarea/>)。

(5)change 事件

文本框(<input/>或<textarea/>)失去焦点时,并且在它获取焦点后内容发生过改变时

触发;某个<select/>元素的值发生改变时触发。

(6)submit 事件

当单击提交按钮(<input type =“submit”>)时,在<form>上触发。

(7)reset 事件

当单击重置按钮(<input type =“reset” >)时,在<form>上触发。

(8)resize 事件

窗口或者框架的大小发生改变时触发,如 大化或 小化窗口时,或者在用户调整窗

口时。网页通常需要根据浏览器窗口的大小进行调整,通过 resize 事件可以判断何时动态

地改变了窗口大小。

(9)scroll 事件

用户在含有滚动条的对象上卷动页面时触发。<body>元素包含载入页面的滚动条。通

过 scroll 事件,结合窗口对象(body)的 scrollLeft 属性和 scrollTop 属性,可以跟踪窗口高

度和宽度的变化。

(10)focus 事件

任何元素或者窗口本身获取焦点(如用户点击它、Tab 键切换到它等)时触发。

(11)blur 事件

任何元素失去焦点时触发。

4. 突变事件

突变事件是 DOM 标准的一部分,但是目前还没有任何主流的浏览器实现,因而对于

构建点击流而言并无意义,在此不进行讨论。

上面讨论了 DOM 标准下的各种事件类型和主要的事件属性,这些 DOM 事件是捕捉

用户行为的主要来源。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

36

2.3.3 DOM 事件流

IE 4.0 和 Netscape Navigator 4.0 的开发小组都认为仅支持事件是不够的,于是他们各自

发明了事件流(Event Flow)。事件流意味着在页面上可以有一个或者多个元素响应同一个

事件。例如,单击页面上的按钮时,是先后单击了按钮、它的容器及整个页面。从逻辑上

说,每一个元素都按照特定的顺序响应事件。事件发生顺序(也就是事件流)的不同,是

IE 4.0 和 Netscape Navigator Navigator 4.0 在事件支持上的主要差别。

IE 4.0 提出的事件流称为冒泡(Dubbed Bubbling)技术。冒泡型事件的基本思想

是:事件按照从 特定的事件目标到 不特定的事件目标(document 对象)的顺序触

发。流程的方法之所以称为冒泡,是因为按照 DOM 的层次结构像水泡一样不断上升

至顶端。

Netscape Navigator 4.0 使用了另外一种称之为捕获型事件(Event Capturing)的解决方

案。捕获型事件和冒泡型刚好相反,在捕获型事件中,事件从 不特定的对象(document

对象)开始触发,然后到 精确的元素。

DOM 同时支持两种事件模型:捕获型事件和冒泡型事件,其中捕获型事件先发生。

两种事件流会触及 DOM 中的所有对象,从 document 对象开始,也在 document 对象结

束。DOM 事件模型 独特的本质在于,文本节点也可以触发事件,这在 IE 中是不可

以的。

2.3.4 事件处理函数

事件是用户或浏览器进行的特定行为。用于响应某个事件而调用的函数称为事件

处理函数(Event Handler),在 DOM 中称为事件监听函数(Event Listener)。事件处理

函数本质上就是 JavaScript 中的一个函数,可以在 JavaScript 或 HTML 中分配事件处

理函数。

在 JavaScript 中分配事件处理函数,如下所示:

var oDiv = document.getElementById("div1");//取得 DOM节点

//在节点上添加事件处理函数

oDiv.onclick = function () {

alert ("clicked");

}

如果是在 HTML 中分配事件处理函数,则如下所示:

<div onclick="clicked"></div>

在 IE 中,还可以应用 attachEvent()和 detachEvent()给对象分配事件处理函数。在 DOM

CHAPTER 第 2 章 Ajax 基础知识

37

2

方法中 addEventListener()和 removeEventListener()用来分配事件处理函数。

2.4 CSS

2.4.1 CSS 简介

CSS 是 Cascading Style Sheet 的缩写,也称作“层叠样式表”或“级联样式表”(以

下简称“样式表”)。1997 年,W3C 在颁布 HTML 4 标准的同时,也公布了有关样式表的

第一个标准 CSS 1。样式表是对以前的 HTML(3.2 版本以前的 HTML 版本)语法的一次

重大革新,在以前的 HTML 版本中,各种功能的实现是通过标记元素实现的,这也造成

了各个浏览器厂商为了标新立意创建各种只有自家支持的标记,各种标记互相嵌套,就

可以达到不同的效果。比如要在一段文字中把一部分文字变成红色,HTML 3.2 中应该是

这样的:

<p><font color=red>这里显示红色字</font></p>

而在样式表中,把某些标记(如上例中的“font”标记)属性化,利用样式表,上例可

以变成:

<p style="color:red">这里显示红色字</p>

CSS 和 HTML 的关系是:HTML 用于结构化内容;CSS 用于格式化结构化的内容。

HTML 的 Tag 主要是定义网页的结构和内容,CSS 使结构和表现分离,决定这些网页内容

如何显示,用于为 HTML 文档定义布局。例如,CSS 涉及字体、颜色、边距、高度、宽度、

背景图像、高级定位等方面。

2.4.2 基本的 CSS 语法

2.4.2.1 引入 CSS

上面展示了一个基本的 CSS,但是如何把 CSS 和 HTML 关联在一起,让 CSS 为显示

HTML 服务呢?下面将回答这个问题

将 CSS 引入页面,按其位置可以分成 3 种:

内嵌样式(Inline Style)

内部样式表(Internal Style Sheet)

外部样式表(External Style Sheet)

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

38

1. 内嵌样式(Inline Style)

内嵌样式是直接写在 HTML 标签的 style 属性里面的,内嵌样式只对所在的标签有效。

比如:

<p style="font-size:20pt; color:red">

这个 Style定义<p></p>里面的文字是 20pt字体,字体颜色是红色。

</p>

2. 内部样式表(Internal Style Sheet)

内部样式表是写在 HTML 的<head></head>里面的,内部样式表要用到 Style 这个标签,

内部样式表只对所在的网页有效。

<html>

<head>

<style type="text/css">

h1.mylayout {

border-width:1;

border:solid;

text-align:center; color:red

}

</style>

</head>

<body>

<h1 class="mylayout"> 这个标题使用了 Style。</h1>

<h1>这个标题没有使用 Style。</h1>

</body>

</html>

3. 外部样式表(External Style Sheet)

上面的两种样式表都只对同一个页面有效,如果很多网页需要用到同样的样式,那该

怎么办呢?办法就是将样式写在一个以.css 为后 的文件里,然后在每个需要用到这些样

式的网页里引用这个 CSS 文件。

外部样式表就是一个扩展名为.css 的文本文件。与跟其他文件一样,你可以把样式表

文件放在 Web 服务器上或者本地硬盘中。

例如,样式表文件名为 style.css,它通常被存放于名为 style 的目录中。就像图 2-8 所示这

样。

CHAPTER 第 2 章 Ajax 基础知识

39

2

图 2-8 外部样式表存放目录

现在的问题是:如何在一个 HTML 文档里引用一个外部样式表文件(style.css)

呢?答案是:在 HTML 文档里创建一个指向外部样式表文件的链接(link)即可,就

像下面这样:

<link rel="stylesheet" type="text/css" href="style/style.css" />

注意:

要在 href属性里给出样式表文件的地址。

这行代码必须被插入 HTML 代码的头部(header),即放在标签<head>和标签</head>

之间,如下所示:

<html>

<head>

<title>我的文档</title>

<link rel="stylesheet" type="text/css" href="style/style.css" />

</head>

<body>

...

这个链接告诉浏览器:在显示该 HTML 文件时,应使用给出的 CSS 文件进行布

局。

这种方法的优越之处在于:多个 HTML 文档可以同时引用一个样式表。换句话说,可

以用一个 CSS 文件来控制多个 HTML 文档的布局,如图 2-9 所示。

图 2-9 CSS 文件引用

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

40

这一方法可以省去许多工作。例如,假设要修改某网站的所有网页(比如有 100

个网页)的背景颜色,采用外部样式表可以避免手工一一修改这 100 个 HTML 文档。

采用外部样式表,这样的修改只需几秒钟即可完成——修改外部样式表文件里的代码

即可。

使用外部样式表,相对于内嵌样式和内部样式表,具有以下优点:

样式代码可以复用。一个外部 CSS 文件,可以被很多网页共用。

便于修改。如果要修改样式,只需要修改 CSS 文件,而不需要修改每个网页。

提高网页显示的速度。如果样式写在网页里,会降低网页显示的速度;如果网页引

用一个 CSS 文件,这个 CSS 文件多半已经在缓存区中(其他网页早已经引用过它),

网页显示的速度就比较快。

2.4.2.2 CSS 语法

样式表有自己独特的书写方法,掌握了它的语法特征,再了解它的各种属性,那么你

会发现在 Web 页面中运用样式表会非常轻松。

首先我们来熟悉一下 基本的 CSS 语法。例如,有一个 简单的 HTML 文档:

<html>

<body>

<p>Text goes here…<p>

</body>

</html>

我们可以用内部样式表规定样式:

<html>

<style>

<!--

p {color:red; font-weight:bold}

- ->

</style>

<body>

<p>这里显示红色字</p>

</bdoy>

</html>

可以看到,在这个文档里,多了 style 标记,之间用<!-- … - ->注释,以防止不能识别

样式表的低版本浏览器把样式表当作内容显示出来,然后是关键的一句:

p {color:red;font-weight : bold }

这一整行称为一个声明(Statement),在样式表中,声明分为两种:一种是像前面这样

CHAPTER 第 2 章 Ajax 基础知识

41

2

的,叫做“rule set”;另一种则称为“at-rule”。at-rule 以“@”作为关键字,放在元素的

前面,at-rule 通常用来对媒体(Media)进行声明,并且如果对同一个 at-rule 进行声明,那

么只有位置靠前的会起到作用,如:

@import "subs.css"

H1 {color:blue}

@import "list.css"

后一个 at-rule 无效。

而 rule set 是由选择器、属性和属性值等几个部分组成的,基本语法如图 2-10 所

示。

图 2-10 CSS 的基本语法

参数说明:

Selector ——选择器,本条 CSS 规则施加的对象。

property : value —— 样式表定义。属性和属性值之间用冒号(:)隔开。多个定义之

间用分号(;)隔开

selector { property: value }表示一条 CSS 规则,selector 是选择器,表示这条规则要施加

的对象,而大括号里面的则是 CSS 要达到的效果。选择器可以有很多种,下面的章节中还

会具体介绍。

上面例子中的“P”是选择器,表示选择页面中的段落标记元素<p>,“{}”为一个块

(Block),表示对标记属性的声明(Declaration),有多个属性时使用“;”隔开。属性在

样式表中的一般表示方法是:前面是一类属性的名称,后面是具体属性的名称,中间用

“-”隔开;而在脚本中使用属性时,则把“-”去掉,并把第二部分的开头字母大写。属

性值的表示可以使用十进制数值,十六进制数值(如#FFFFFF)、百分数(如 100%)、字

符串、URL(如 http://www.XMLasp.Net)和 RGB(如 rgb(255,255,255))等多种方式表示。

下面对其中的重点部分进行更详细的解释。

div{width:200;filter:blur(add=true,direction=35,strengh=20);}

在上面的语句中,div 是选择器,选择器可以是 HTML 中任何的标识符,比如 p、div、

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

42

img 甚至 body 都可以作为选择器。这里用 div 做选择器,就是说在 HTML 中,编辑在<div>

中的页面格式将以上面语句中大括号内定义的格式显示。

括号内的 width 和 filter 就是属性。

width 定义了 div 区域内的页面宽度,200 是属性值。

filter 定义了滤镜属性,blur 是它的属性值,该属性值产生的是一种模糊效果,其小

括号内定义的是 blur 属性值的一些参数。

add 参数有两个值:True 和 False,分别指定图片是否被设置成模糊效果。direction 参

数用来设置模糊的方向,0 度代表垂直向上,然后每 45 度一个单位,例子中的 135 代表底

部向右 135 度,每一个度数单位都代表一个模糊方向。如果你感兴趣的话,可以参照后面

章节中详细的参数设定。

Strengh 代表有多少像素的宽度将受到模糊影响,参数值是用整数来设置的。

2.4.3 CSS 属性

从上面的例子中我们可以看出,CSS 中有很多属性,这一节我们将对 CSS 属性进行系

统的介绍。

2.4.3.1 字体属性

字体属性是 基本的属性,我们经常会用到。它主要包括以下这些属性,如表 2-1 所

示。

表 2-1 CSS 的字体属性

属 性 属 性 含 义 属 性 值

font-family 使用什么字体 所有的字体

font-style 字体是否斜体 normal、italic、oblique

font-variant 字体是否用小体大写 normal、small-caps

font-weight 定义字体的粗细 normal、bold、bolder、lithter 等

font-size 定义字体的大小 absolute-size、relative-size、length、percentage 等

2.4.3.2 颜色和背景属性

下面介绍 CSS 的颜色和背景属性。先来看一下 CSS 的颜色和背景都有哪些属性,如表

2-2 所示。

表 2-2 CSS 的颜色和背景属性

属 性 属 性 含 义 属性书写格式示例 属 性 值

color 定义前景色 p{color:red} 颜色

background-color 定义背景色 body{background-color:yellow} 颜色

CHAPTER 第 2 章 Ajax 基础知识

43

2

续表

属 性 属 性 含 义 属性书写格式示例 属 性 值

background-image 定义背景图案 body{background-image(1.jpg)} 图片路径

background-repeat 背景图案重复方式 body{background-repeat:repeat-y} repeat-x、repeat-y、no-repeat

background-attachment 设置滚动 body{background-attachment:scroll} scroll、fixed

background-position 背景图案的初始位置 body{background:url(1.jpg) top center} percentage、length、top、left、

right、bottom 等

2.4.3.3 文本属性

1. 定义间距

前面介绍了如何用 CSS 定义字体、颜色和背景属性,下面讲述 CSS 的文本属性,先来

看一下文本属性的详细列表,如表 2-3 所示。

表 2-3 CSS 的文本属性

属 性 属 性 含 义 属 性 值

word-spacing 单词之间的距离 Normal <length>(必须以长度为单位)

letter-spacing 字母之间的距离 同上

text-decoration 文字的装饰样式 none|underline|overline|line-through|blink

vertical-align 元素在垂直方向的位置 baseline|sub|super|top|text-top|middle|bottom|text-bottom| <percentage>

text-transform 将文本转化为其他形式 capitalize|uppercase|lowercase|none

text-align 文字的对齐形式 left|right|center|justify

text-indent 文本的首行缩进 <length>|<percentage>

text-height 文本的行高 normal|<number>|<length>|<percentage>

从表格 2-3 中可以看到,在这里可以定义文本的文字间距、字母间距、装饰、对齐方

式、缩进方式和行高等属性。比如下面这段文字,如图 2-11(左图)所示。

图 2-11 设置 CSS 的文本属性示意图

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

44

对这段文字加上文本属性重新排版,排版后的效果如图 2-13(右图)所示。我们

看到经过文本属性处理的文本字与字之间多了间距,行与行之间多了行高,对齐方式

变成了两端对齐,并且段首缩进了两格。这是怎样实现的呢?很简单,只是在 HTML

中加入了如下代码:

<p style="letter-spacing:1em;text-align:justify;

text-indent:4em:line-height;17pt">……</p>

//*letter-spacing设置了字间距为 1em,其中 1em为一个长度单位;text-align

设置了对齐方式为两端对齐;indent设置了缩进格为 4em;line-height设置了行高为

17pt*//

从上面的例子中可以看出,利用 CSS 的文本属性可以方便地对页面中的文本进行修饰

排版。下面介绍文本的 text-decoration 属性的一个小应用。

2. 装饰超链接

网页默认的链接方式是这样的:未访问过的超链接是蓝色文字并带蓝色的下画线,访

问过的超链接是深紫色的文字并带深紫色的下画线。如果所有的网页都是这种样式,是不

是很单调呢?

其实,利用文本属性中的 text-decoration 属性就可以实现对超链接的修饰。先看下面的

这段代码:

<html>

<title>link css</title>

<head>

<style>

<!--

//*定义伪类元素(a:),大括号内定义了前景色属性和文本装饰属性,hover加上

'font-size'属性,目的是让鼠标激活链接时改变字体*//

a:link{color:green;text-decoration:none}

//*未访问时的状态,颜色为绿色(green),文本装饰属性(text-decoration)值为没有

(none)*//

a:visited{color:red;text-decoration:none}

//*访问过的状态,颜色为红色(red),文本装饰属性值为没有*//

a:hover{color:blue;text-decoration:overline;font-size:20pt}

//*鼠标激活的状态,颜色为蓝色(blue),文本装饰属性值为上画线(overline),字

体大小为 20pt*//

-->

</style>

</head>

<body>

CHAPTER 第 2 章 Ajax 基础知识

45

2

<p style="font-family:行书体;font-size:18pt">

<a href="http://www.hongen.com">未访问的链接</a></p>

//*加链接,显示 3种不同状态,并且定义了链接文本的字体和大小*//

<p> <a href="http://www.hongen.com">访问过的链接</a></p>

<p> <a href="http://www.hongen.com">鼠标激活的链接</a></p>

</body>

</html>

如果想看上述代码的效果,请运行这段代码。

在上面的例子中,没有访问过的链接以绿色显示,并且去掉了下画线;而访问过的

链接以红色显示且没有下画线显示;另外,当鼠标激活链接时,链接以蓝色显示,并且

加上了上画线。这种效果是怎么实现的呢?它除了运用了文本属性中的 text-decoration

属性外,还采用了伪类元素。通过上面的代码注释,相信你应该对伪类元素有了一个大

概认识。实际上,我们用到的这种伪类应当称为“锚伪类”,它规定了链接不同状态下

的效果。

2.4.3.4 CSS Box 模型

Box 模型(Box Model)是 CSS 布局中一个很重要的概念。在 CSS 中用矩形的区域来

对 Document 中的对象进行布局,布局效果如图 2-12 所示。

图 2-12 CSS Box 模型

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

46

图 2-14 就是 CSS 的 Box 模型,黑框包围的一个方块,就是一个盒子 (Box)。盒子里由

外至内依次是:

margin 边距

边距属性(margin)用来设置一个元素所占空间的边缘到相邻元素之间的距离。

border 边框

边框属性(border)用来设定一个元素的边线。

padding 填充

填充属性(padding)用来设置元素内容到元素边框的距离。

content(内容,比如文本、图片等)

当 Box 模型和元素的背景及内容联系在一起时,就会形成一个立体 Box 模型,如图 2-13

所示。

图 2-13 CSS Box 模型立体图

Box 模型除了具有上面的 4 个属性之外,还有另外一些属性如表 2-4 所示。

CHAPTER 第 2 章 Ajax 基础知识

47

2

表 2-4 CSS Box 模型属性

属 性 属 性 含 义 属 性 值

width 宽度 length|percentage|auto

height 高度 length|auto

float 是文字环绕在一个元素的四周 left|right|none

clear 元素的某一边是否有环绕文字 left|right|none|both

2.4.4 CSS 选择器

使用 CSS 就要对目标元素进行选择,前面的示例中使用过一些选择器,接下来更深入

地了解 CSS 的选择器。

选择器模式

首先,来看一下选择时使用的模式,如表 2-5 所示。

表 2-5 选择器模式

模 式 含 义 内容描述

* 匹配任意元素 通用选择器

E 匹配任意元素 E (例如一个类型为 E 的元素) 类型选择器

E F 匹配元素 E 的任意后代元素 F 后代选择器

E > F 匹配元素 E 的任意子元素 F 子选择器

E:first-child 当元素 E 是它的父元素中的第一个子元素时,匹配元素 E :first-child 伪类

E:link E:visited 如果 E 是一个目标还没有访问过(:link)或者已经访问过(:visited)

的超链接的源锚点时匹配元素 E link 伪类

E:active E:hover

E:focus 在确定的用户动作中匹配 E 动态伪类

E + F 如果一个元素 E 直接在元素 F 之前,则匹配元素 F 临近选择器

E[foo] 匹配具有“foo”属性集(不考虑它的值)的任意元素 E 属性选择器

E[foo="warning"] 匹配其“foo”属性值严格等于“warning”的任意元素 E 属性选择器

E[foo~="warning"] 匹配其“foo”属性值为空格分隔的值列表,并且其中一个严格等于

“warning”的任意元素 E 属性选择器

E[lang|="en"] 匹配其“lang”属性具有以“en”开头(从左边)的值的列表的任意

元素 E 属性选择器

E#myid 匹配 ID 等于“myid”的任意元素 E ID 选择器

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

48

下面的例子解释了“父元素”、“子元素”、“父/子”及“相邻”这几个概念。

<div title="这是一个 div">

<h1>这是 h1的内容</h1>

<p>这是一个段落p的内容!<strong>这里是strong的内容</strong>这是一个段落p的内容!

</p>

</div>

从以上代码中,可以找出这样的关系:

h1 和 p 同为 div 的“儿子”,两者分别与 div 形成“父/子”关系。

h1、p、strong 都是 div 的“子元素”(三者都包含在 div 之内)。

div 是 h1 和 p 的“父元素”。

strong 和 p 形成“父/子”关系,strong 的“父元素”是 p 。

但 strong 和 div 并非“父/子”关系,而是“祖孙”关系,但 strong 依然是 div 的

“子(孙)元素”。

div 是 h1、p、strong 三者的“祖先”,三者是 div 的“子(孙)元素”。

h1 和 p 两者是相邻的。

结合上面的实例来具体演示一下表 2-5 中 E、F 的关系。如果需要将 strong 元素中的

文字变为绿色,有哪些方法呢?

div strong {color:green;} /* - 正确。strong 是 div 的“子元素”*/

p > strong {color:green;} /* - 正确。strong 和 p 是“父/子”关系*/

div > strong {color:green;} /* - 错误。strong 虽然是 div 的“子(孙)元素”,

但两者乃是“祖孙”关系,而非“父/子”关系,因此不能用 > 符号连接*/

临近选择器和通用选择器:临近选择器用“+”表示,通用选择器以星号“*”

表示,可以用于替代任何页面元素。下面是联合使用临近选择器和通用选择器的例

子。

h2 + * { color:green }/*所有紧随 h2 的元素内的文字都将呈现红色*/

2.4.5 串联(Cascading)

CSS 的第一个字母的含义是 Cascading,意思为“串联”,是指不同来源的样式(Style)

可以合在一起,形成一种样式。

Cascading 的顺序是:

浏览器缺省(Browser Default),优先级 低);

外部样式表(Extenal Style Sheet);

CHAPTER 第 2 章 Ajax 基础知识

49

2

内部样式表(Internal Style Sheet);

内嵌样式(nline Style),优先级 高。

样式的优先级依次是内嵌(Inline)、内部(Internal)、外部(External)、浏览器缺省

(Browser Default)。假设内嵌样式中有 font-size:30pt,而内部样式中有 font-size:12pt,那

么内嵌样式就会覆盖内部样式。

2.5 JavaScript 简介

在介绍事件处理函数时已经使用过 JavaScript 函数了,在这里再简单地介绍一下

JavaScript。

JavaScript 是一种可以在 Web 上运行的脚本语言,是赋予网页活力与交互性的主要手

段之一。全世界每天都有无数网页在依靠 JavaScript 完成各种关键任务。随着 Web 2.0 和

Ajax 成为主流技术,JavaScript 已经被推到了 Web 开发的舞台中心,使用它来开发更大、

更复杂的程序势在必行。随着 dojo、jQuery、Prototype、script.aculo.us、Ext 等成熟的 JavaScript

框架不断发布,基于这些框架构建的优秀的 Web 应用也不断产生,因此,有理由相信

JavaScript 将会成为 Web 开发的一个重要方向。

2.5.1 JavaScript 入门示例

先来看一个 简单的例子,代码如下:

<html>

<head><title>一个 简单的 JavaScript示例(仅使用了 document.write)</title>

</head>

<body>

<script type="text/JavaScript">

document.write("Hello, World! ");

alert("Hello,World");

</script>

</body>

</html>

运行结果如图 2-14 所示。

在 HTML 网页里插入 JavaScript 语句,应使用 HTML 的<Script>标签。<Script>有一个

type 属性,type=“text/JavaScript”表示插入<Script>和</Script>中的为 JavaScript 语句。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

50

图 2-14 JavaScript 示例结果(一)

在上面的例子中,使用了 document.write,这是 JavaScript 中常用的语句,表示向当前

的页面文档中输出文本。另外使用了 alert 语句,表示弹出一个对话框。从实例分析中我们

可以看出,编写一个 JavaScript 程序确实非常容易。

2.5.2 JavaScript 基本数据结构

JavaScript 脚本语言与其他语言一样,有其自身的基本数据类型、表达式、运算符及程

序的基本框架结构。JavaScript 提供了 4 种基本数据类型用来处理数字和文字,而变量用于

存放信息,表达式则可以完成较复杂的信息处理。

1. 基本数据类型

在 JavaScript 中有 4 种基本数据类型:数值型(整数和实数)、字符串型(用""或''号括

起来的字符或数值)、布尔型(使用 True 或 False 表示)和空值。JavaScript 的基本数据类

型中的数据可以是常量,也可以是变量。由于 JavaScript 采用弱类型的形式,因而一个数

据的变量或常量不必首先声明,而是在使用或赋值时确定其数据的类型。当然也可以先声

明该数据的类型,它是通过在赋值时自动说明其数据类型的。

2. 常量

(1)整型常量

JavaScript 的常量通常又称为字面常量,它是不能改变的数据。整型常量可以使用十六

进制、八进制和十进制形式表示其值。

(2)实型常量

实型常量是由整数部分加小数部分表示的,如 12.32、193.98。可以使用科学或标准方

法表示,如 5E7、4e5 等。

CHAPTER 第 2 章 Ajax 基础知识

51

2

(3)布尔型常量

布尔型常量只有两种状态:True 或 False,主要用来说明或代表一种状态或标志,以说

明操作流程。它与C++是不一样的,C++可以用1或0表示状态,而 JavaScript 只能用 True

或 False 表示状态。

(4)字符型常量

使用单引号(')或双引号(")括起来的一个或几个字符,如 “This is a book of

JavaScript ”、“3245”、“ewrt234234”等。

(5)空值

JavaScript 中有一个空值 Null,表示什么也没有。如果试图引用没有定义的变量,则返

回一个 Null 值。

(6)特殊字符

与C语言一样,JavaScript 中同样有些以反斜杠(/)开头的不可显示的特殊字符,通

常称为控制字符。

3. 变量

变量的主要作用是存取数据、提供存放信息的容器。对于变量必须明确变量的命名、

变量的类型、变量的声明及其变量的作用域。

JavaScript 中的变量命名与其计算机语言非常相似,这里要注意以下两点:

必须是一个有效的变量,即变量以字母开头,中间可以出现数字,如 test1、text2

等。除下画线(-)作为连字符外,变量名称不能有空格、+、-、,或其他符号。

不能使用 JavaScript 中的关键字作为变量。

在 JavaScript 中定义了 40 多个类键字,这些关键字是 JavaScript 内部使用的,不能作

为变量的名称,如 var、int、double、true 不能作为变量的名称。

在对变量命名时, 好把变量的意义与其代表的意思对应起来,以免出现错误。

在 JavaScript 中,变量可以用关键字 var 进行声明:

var mytest;

该例子定义了一个 mytest 变量,但没有为它赋值。

var mytest="This is a book"

该例子定义了一个 mytest 变量, 同时为它赋了值。

在 JavaScript 中,变量可以不先进行声明,而是在使用时根据数据的类型来确定其类型。

例如:

x=100

y="125"

xy= True

cost=19.5

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

52

其中 x 为整数,y 为字符串,xy 为布尔型,cost 为实型。

4. 变量的声明及其作用域

JavaScript 变量可以在使用前先声明并赋值。通过使用 var 关键字对变量作声明。

对变量进行声明的 大好处就是能及时发现代码中的错误;因为 JavaScript 是采用动

态编译的,而动态编译不容易发现代码中的错误,特别是变量命名的方面。

对于变量还有一个重要方面──那就是变量的作用域。在 JavaScript 中同样有全局变量

和局部变量。全局变量定义在所有函数体之外,其作用范围是整个函数;而局部变量定义

在函数体之内,只对该函数是可见的,而对其他函数则是不可见的。

5. 表达式和运算符

(1)表达式

在定义完变量后,就可以对它们进行赋值、改变、计算等一系列操作了,这一过程通

常由表达式来完成。可以说表达式是变量、常量、布尔值及运算符的集合,因此表达式可

以分为算术表达式、字符串表达式、赋值表达式及布尔表达式等。

(2)运算符

运算符是完成操作的一系列符号,在 JavaScript 中有算术运算符,如+、-、*、/等;有

比较运算符如!=、= =等;有布尔逻辑运算符,如!(取反)、|、||;有字符串运算符,如+、

+=等。

在 JavaScript 中运算符分为双目运算符和单目运算符。双目运算符的组成如下:

操作数1 运算符 操作数2

即由两个操作数和一个运算符组成。如 50+40、“This”+“that”等。单目运算符只需

一个操作数,其运算符可在前或后。

算术运算符

JavaScript 中的算术运算符分为单目运算符和双目运算符。

双目运算符有:+(加)、-(减)、*(乘)、/(除)、%(取模)、|(按位或)、&(按位

与)、<<(左移)、>>(右移)、>>>(右移,零填充)。

单目运算符有:-(取反)、~(取补)、++(递加 1)、--(递减 1)

比较运算符

比较运算符的基本操作过程是:首先对它的操作数进行比较,然后返回一个 True 或

False 值。有8个比较运算符<(小于)、>(大于)、<=(小于等于)、>=(大于等于)、==

(等于)、!=(不等于)。

布尔逻辑运算符

在 JavaScript 中增加了多个布尔逻辑运算符!(取反)、&=(与之后赋值)、&(逻辑与)、

|=(或之后赋值)、|(逻辑或)、^=(异或之后赋值)、^(逻辑异或)、?:(三目操作符)、||

CHAPTER 第 2 章 Ajax 基础知识

53

2

(或)、= =(等于)、|=(不等于)。

其中三目操作符主要格式如下:

操作数?结果 1:结果 2

若操作数的结果为真,则表达式的结果为结果1;否则为结果2。

2.5.3 JavaScript 的基本构成

JavaScript 脚本语言基本上由控制语句、函数、对象、方法、属性等构成。

1. 程序控制流

在任何一种语言中,程序控制流是必需的,它能减小整个程序的混乱,使其顺利地按

照一定的方式执行。下面是 JavaScript 常用的程序控制流结构及语句。

(1)if 条件语句

基本格式:

if(表达式)

语句段1;

......

else

语句段2;

.....

功能:若表达式为 True,则执行语句段1;否则执行语句段2。

说明:if-else 语句是 JavaScript 中 基本的控制语句,通过它可以改变语句的执行顺序。

表达式中必须使用关系语句,来实现判断,它是作为一个布尔值来估算的,它将零和非零的

数分别转化成 False 和 True。若 if 后的语句有多行,则必须使用花括号将其括起来。

if语句的嵌套

if(布尔值)语句1;

else(布尔值)语句2;

else if(布尔值)语句3;

else 语句4;

在这种情况下,每一级的布尔表达式都会被计算,若为真,则执行其相应的语句;否

则执行 else 后的语句。

(2)for 循环语句

基本格式:

for(初始化;条件;增量)

语句集;

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

54

功能:实现条件循环,当条件成立时,执行语句集;否则跳出循环体。

说明:

初始化参数告诉循环的开始位置,必须为变量赋予初值;

条件:是用于判断循环停止时的条件。若条件满足,则执行循环体;否则跳出。

增量:主要定义循环控制变量在每次循环时按什么方式变化。

3 个主要语句之间,必须使用逗号分隔。

(3)while 循环语句

基本格式

while(条件)语句集;

该语句与 for 语句一样,当条件为真时,重复循环;否则退出循环。

for 与 while 语句都是循环语句,使用 for 语句在处理有关数字时更易看懂,也较紧凑;

而 while 循环语句对复杂的语句效果更特别。

(4)break 和 continue 语句

与 C++语言相同,使用 break 语句使循环从 for 或 while 中跳出,使用 continue 语句用

于跳过循环内剩余的语句而进入下一次循环。

2. 函数

通常在进行一个复杂的程序设计时,总是根据所要完成的功能,将程序划分为一些相

对独立的部分,每部分编写一个函数。从而使各部分充分独立,任务单一,程序清晰、易

懂、易读、易维护。JavaScript 函数可以封装那些在程序中可能要多次用到的模块,并可作

为事件驱动的结果而调用的程序,从而实现一个函数把它与事件驱动相关联。

(1)JavaScript 函数定义

function 函数名(参数,变元){函数体;.return 表达式;}

说明:

当调用函数时,所用变量或常量均可作为变元传递。

函数由关键字 function 定义。

函数名:定义函数的名字。

参数:是传递给函数使用或操作的值,其值可以是常量、变量或其他表达式。

返回值:通过指定函数名(实参)来调用一个函数。用 return 将值返回。

此外,函数名对大小写是敏感的。

(2)函数中的形式参数

在函数的定义中,我们看到函数名后有参数表,这些参数变量可能是一个或多个。

那么怎样才能确定参数变量的个数呢?在 JavaScript 中可以通过 arguments .length 来检

查参数的个数。

示例:

CHAPTER 第 2 章 Ajax 基础知识

55

2

function function_Name(exp1,exp2,exp3,exp4){

Number =function _Name . arguments .length;

if(Number>1)

document.wrile(exp2);

if (Number>2)

document.write(exp3);

if(Number>3)

document.write(exp4);

}

2.5.4 事件驱动及事件处理

JavaScript 是基于对象(Object-based)的语言。这与 Java 不同,Java 是面向对象的语

言。而基于对象的基本特征,就是采用事件驱动(Event Driven)。它是在图形界面的环境

下,使得一切输入变化简单化。通常对于鼠标或热键的动作我们称之为事件(Event),由

鼠标或热键引发的一连串程序的动作,称之为事件驱动。而对事件进行处理程序或函数,

则称之为事件处理程序(Event Handler)。

在 JavaScript 中对事件的处理通常由函数(Function)担任。其基本格式与函数完全一

样,可以将前面所介绍的所有函数作为事件处理程序。

格式如下:

function 事件处理名(参数表){

事件处理语句集;……}

JavaScript 事件驱动中的事件是通过鼠标或热键的动作引发的,主要的事件在“DOM

事件”一节中已经有详细的介绍,这里不再赘述。

2.6 XML 简介

在 Ajax 中传递数据,有两种主要的数据格式:XML 和 JSON(JavaScript Object

Notation),下面分别进行介绍。

XML 即为可扩展标记语言(eXtensible Markup Language)。XML 是一套定义语义标记

的规则,这些标记将文档分成许多部件并对这些部件加以标识。

XML 是标记语言,理解 XML,首先要理解标记。HTML 的标记(Markup),是一种

用来给文本添加标记的语言。在 HTML 里每个标记都是有确切含义的。例如,标记<b>的

含义是要求 HTML 浏览器将一段文本加粗表示;而标记<center>的含义是告诉浏览器将这

段文本在一行的中间显示。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

56

而 XML 并非像 HTML 那样,提供了一组事先已经定义好了的标记,而是提供了一个

标准,利用这个标准,你可以根据实际需要定义自己的标记语言,并为这个标记语言规定

它特有的一套标记。准确地说,XML 是一种源标记语言,允许你根据它所提供的规则,制

定各种各样的标记语言。

2.6.1 XML 的产生

XML 有两个先驱——SGML 和 HTML,这两种语言都是非常成功的标记语言,SGML

的全称是标准通用标记语言,它从 20 世纪 80 年代初开始使用。正如 XML 一样,SGML

也可用于创建成千上万种标记语言,它为语法标记提供了异常强大的工具,同时具有极好

的扩展性,因此在分类和索引数据中非常有用。目前,SGML 多用于科技文献和政府办公

文件中。SGML 非常复杂,其复杂程度对于网络上的日常应用简直不可思议。不仅如此,

SGML 还非常昂贵。HTML 免费、简单,而且获得了广泛的支持。HTML 是一种非常简单

的标记语言,可以方便普通用户使用。1996 年,人们开始致力于描述一种标记语言,它既

具有 SGML 的强大功能和可扩展性,同时又具有 HTML 的简单性。W3C 于 1998 年 2 月批

准了 XML 1.0 版本,一种崭新而大有前途的语言诞生了。

2.6.2 XML 的优点

XML 允许各种不同的专业(如音乐、化学、数学等)开发与自己的特定领域有关的标

记语言,这就使得该领域中的人可以交换笔记、数据和信息,而不用担心接收端的人是否

有特定的软件来创建数据。

应用间交换数据。由于 XML 使用的是非专有的格式并易于阅读和编写,就使得它成

为在不同的应用间交换数据的理想格式。XML 使用的是非专有的格式,不受版权、专利、

商业秘密或其他种类的知识产权的限制。XML 的功能是非常强大的,同时对于人类或计算

机程序来说,都容易阅读和编写,因而成为交换语言的首选。

现在有了 XML,你终于可以自由地制定自己的标记语言了,而不必再念念不忘

Microsoft、Netscape、W3C 的首肯。实际上,现在许多行业、机构都利用 XML 定义了自

己的标记语言。比较早而且比较典型的有下面几个:

化学标记语言 CML (Chemistry Markup Language);

数学标记语言 MathML (Mathematical Markup Language)。

地图置标语言 KML(Keyhole Markup Language)

CHAPTER 第 2 章 Ajax 基础知识

57

2

2.6.3 一个简单的 XML 文档

现在让我们看一个简单的 XML 文档。

<?xml version="1.0" standalone="yes"?>

<FOO>

Hello XML!

</FOO>

第一行是 XML 声明:

<?xml version="1.0" standalone="yes"?>

这是 XML 处理指令的例子。处理指令以<?开始,而以?>结束。在<?后的第一个单词

是处理指令名,在本例中是 XML。

XML 声明有 version 和 standalone 两个特性。特性是由等号分开的名称-数值对,位于

等号左边的是特性名,而其值位于等号的右边,并用双引号括起来。每一个 XML 文档都

以一个 XML 声明开始,用以指明所用的 XML 的版本。在上例中,version 特性表明这个

文档符合 XML 1.0 规范。XML 声明还可以有 standalone 特性,用来告诉我们文档是在这

个文件里还是需要从外部导入文件。在本例及以后的几章中,所有的文档都在一个文件里

完成,因而 standalone 特性的值要设置为 yes。

再看第 2~4 行,总体上说,这 3 行组成了 FOO 元素。<FOO>是开始标记,</FOO> 是

结束标记,Hello XML!是 FOO 元素的内容。读者可能要问,<FOO>标记的含义是什么?

回答是“你让它是什么它就是什么”。除了几百个预定义的标记之外,XML 还允许用户创

建所需的标记。因而<FOO>标记可以具有用户赋予的任何含义。同一个 XML 文档可以用

不同的标记名编写,例如:

<?xml version="1.0" standalone="yes"?>

<GREETING>

Hello XML!

</GREETING>

或者

<?xml version="1.0" standalone="yes"?>

<P>

Hello XML!

</P>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

58

2.6.4 XML 文档的整体结构

XML 文档包括 3 部分:XML 声明、处理指示(可选)、XML 元素。XML 文档的一个

基本要求是文档要形式良好(well formed),一个形式良好的 XML 文档要包含这 3 个部分。

下面是一个完整的 XML 文档。

<?xml version="1.0" encoding="gb2312" ?>

<?xml-stylesheet type="text/xsl" href="mystyle.xsl"?>

<学生花名册>

<学生>

<名字>李四</名字>

<籍贯>河北</籍贯>

<年龄>15</年龄>

<电话号码>62875555</电话号码>

</学生>

<学生>

<名字>张三</名字>

<籍贯>北京</籍贯>

<年龄>14</年龄>

<电话号码>82873425</电话号码>

</学生>

</学生花名册>

2.6.5 XML 文档的实质内容——元素

元素是 XML 文档内容的基本单元。从语法上讲,一个元素包含一个起始标记、一个

结束标记和标记之间的数据内容。其形式如下:

<标记>数据内容</标记>

另外,元素中还可以再嵌套别的元素。比如数据内容可再扩展为

<标记 1>数据内容 1</标记 1>

<标记 2>数据内容 2</标记 2>

......

<标记 n>数据内容 1</标记 n>

元素里还可以再嵌套元素,实现循环嵌套。 外层的元素称为根元素,一个 XML 文

档只能有一个根元素。

CHAPTER 第 2 章 Ajax 基础知识

59

2

2.6.6 字符数据与实体引用

一对标记之间出现的字符数据可以是任何合法的 Unicode 字符,但不能包含字符“<”。

这是因为字符“<”被预留用作标记的开始符。

在 XML 中,起始标记和结束标记之间出现的所有合法字符都被忠实地传给 XML 处理

程序。为了避免字符数据和标记中需要用到的一些特殊符号混淆,XML 还提供了一些有用

的实体引用。实体引用的作用是,当在字符数据中需要使用这些特殊符号时,我们采用它

的实体引用来代替。这些特殊的 XML 实体引用包括:

> >

< <

& &

" "

’ '

这样,如果我们需要在“示例”这个标记中出现文本:

"<姓名>张三</姓名>"

正确的写法应该是:

<示例><姓名>张三</姓名></示例>

容易理解,字符“<”的实体引用是必不可少的,为“>”设立实体引用同样是为了避

免与标记混淆;而字符“&“的实体引用则防止它与实体引用中开头所用的“&”相混淆。

那么,我们什么时候需要用到“"”和“'”两个字符的实体引用呢?在标记中可以为标记设

立属性,而 XML 规定属性值必须用“"”括起来。

2.6.7 标记

正如我们在前面所讲,标记是 XML 语言的精髓。因此,标记在 XML 的元素中乃至整

个 XML 文档中,占据了举足轻重的位置。

XML 的标记和 HTML 的标记在模样上大体相同,除了注释和 CDATA 部分以外,所有

符号“<”和符号“>”之间的内容都称为标记。其基本形式为:

<标记名 (属性名="属性取值")*>

不过,XML 对于标记的语法规定可比 HTML 要严格得多。在 XML 标记中必须注意区

分大小写。在 HTML 中,标记<HELLO>和<hello>是一回事,但在 XML 中,它们是两个截

然不同的标记。

(1)要有正确的结束标记

结束标记除了要和开始标记在拼写和大小写上完全相同外,还必须在前面加上一个斜

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

60

杠“/”。因此,如果开始标记是<HELLO>,结束标记应该写为</HELLO>。XML 严格要求

标记配对,因此,HTML 中的<br>、<hr>的元素形式在 XML 中是不合法的。不过,为了

简便起见,当一对标记之间没有任何文本内容时,可以不写结束标记,而在开始标记的

后加上斜杠“/”来确认,这样的标记称为“空标记”。例如,HTML 中的标记<hr>在 XML

中的使用方式应该是:<hr/>。

(2)标记要正确嵌套

在一个 XML 元素中允许包含其他 XML 元素,但这些元素之间必须满足嵌套性。

(3)有效使用属性

后要指出的是,标记中可以包含任意多个属性。在标记中,属性以名称/取值对出现,

属性名不能重复,名称与取值之间用等号“=”分隔,且取值用引号引起来。例如:

<商品 类型 = "服装" 颜色 = "黄色">

在这个例子中,“类型”和“颜色”是标记“商品”的属性,“服装”是属性“类型”

的取值,“黄色”是属性“颜色”的取值。

需要特别注意的是,在 XML 中属性的取值必须用引号引起来,而在 HTML 中这一点

并不严格要求。

2.6.8 CDATA

正如我们前面所说的,我们可以把 XML 文档中除标记以外的所有内容都看作字符数

据,而把标记中的所有内容都看作标记。不过,也有一个例外,在一个特殊的标记 CDATA

下,所有的标记、实体引用都被忽略,而被 XML 处理程序一视同仁地当作字符数据看待。

CDATA 的形式如下:

<! [CDATA[

文本内容

]]>

聪明的读者可能已经猜出,CDATA 的文本内容中是不能出现字符串“]]〉”的,因为

它代表了 CDATA 数据块的结束标志。

2.6.9 注释

有些时候,希望 XML 处理器能够把你在数据中引入的标记当作普通数据而不是真正

的标记来看待。这时,CDATA 为你助了一臂之力。另外还有些时候,就像在程序中引入注

释一样,你可能希望在 XML 文件中加入一些用作解释的字符数据,并且希望 XML 处理器

不对它们进行任何处理。这种类型的文本称作注释(Comment)文本。

在 HTML 中,注释是用“〈!--“和”--〉“引起来的;在 XML 中,注释方法与其完全

CHAPTER 第 2 章 Ajax 基础知识

61

2

相同。下面是一个合法的 XML(但不是形式良好的)文档。

<!-- 一个 XML的例子 -->

<![CDATA[

<联系人>

<姓名>张三</姓名>

<EMAIL>[email protected]</EMAIL>

</联系人>

]]>

</示例>

不过,在 XML 文档中使用注释时,同样要遵守几个规则。

在注释文本中不能出现字符“-”或字符串“—”,XML 处理器可能把它们和注释

结尾标志“-->”混淆。

不要把注释文本放在标记之中。类似地,不要把注释文本放在实体声明中,也不要

放在 XML 声明之前。记住,永远用 XML 声明作为 XML 文档中的第一行。

注释不能被嵌套。在使用一对注释符号表示注释文本时,要保证其中不再包含另一

对注释符号。例如,下面例子是不合法的:

<!-- 一个 XML的例子

<!--以上是一个注释-->

-->

后再重申一遍:XML 处理器对于注释中的一切内容都会视而不见,注释中出现的标

记也一同被忽略。

2.7 JSON 简介

JSON 是一个轻量级的、基于文本的、语言无关的数据交换格式,易于阅读和编写,同

时也易于机器解析和生成。它是 JavaScript Programming Language, Standard ECMA-262 3rd

Edition - December 1999 的一个子集。JSON 采用完全独立于语言的文本格式,但是也使用

了类似于 C 语言家族的习惯(包括 C,C++,C#,Java、JavaScript、Perl、Python 等),这

些特性使 JSON 成为理想的数据交换语言。JSON 对象示例如图 2-15 所示。

图 2-15 JSON 对象示例

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

62

XML 和 JSON 都利用结构化来标记数据。我们通过一个地址簿的例子来展示它们之间

的不同。

下面是一个地址薄信息,用 XML 表示如下:

<?xml version='1.0' encoding='UTF-8'?>

<card>

<fullname>Sean Kelly</fullname>

<org>SK Consulting</org>

<emailaddrs>

<address type='work'>[email protected]</address>

<address type='home' pref='1'>[email protected]</address>

</emailaddrs>

<telephones>

<tel type='work' pref='1'>+1 214 555 1212</tel>

<tel type='fax'>+1 214 555 1213</tel>

<tel type='mobile'>+1 214 555 1214</tel>

</telephones>

<addresses>

<address type='work' format='us'>1234 Main St

Springfield, TX 78080-1216</address>

<address type='home' format='us'>5678 Main St

Springfield, TX 78080-1316</address>

</addresses>

<urls>

<address type='work'>http://seankelly.biz/ </address>

<address type='home'>http://seankelly.tv/ </address>

</urls>

</card>

在 JSON 中,则表示如下:

{

"fullname": "Sean Kelly",

"org": "SK Consulting",

"emailaddrs": [

{"type": "work", "value": "[email protected]"},

{"type": "home", "pref": 1, "value": "[email protected]"}

],

"telephones": [

{"type": "work", "pref": 1, "value": "+1 214 555 1212"},

{"type": "fax", "value": "+1 214 555 1213"},

CHAPTER 第 2 章 Ajax 基础知识

63

2

{"type": "mobile", "value": "+1 214 555 1214"}

],

"addresses": [

{"type": "work", "format": "us",

"value": "1234 Main StnSpringfield, TX 78080-1216"},

{"type": "home", "format": "us",

"value": "5678 Main StnSpringfield, TX 78080-1316"}

],

"urls": [

{"type": "work", "value": "http://seankelly.biz/"},

{"type": "home", "value": "http://seankelly.tv/"}

]

}

与 XML 相比,JSON 具有简单、无须解析、传输迅速等特点,尤其适合 Ajax 开发应用

中的数据传递。总体而言,XML 适用于要求信息格式的应用,而 JSON 没有内容格式的限制,

因而更适用于不限制内容格式的应用。大量的测试表明,JSON 解析的速度几乎是 XML 解析

的 10 倍,因此,如果应用对数据解析的性能要求较高,则要考虑使用 JSON 格式。

JSON 有两种数据结构:

“‘名称/值’对”的集合(A collection of name/value pairs)。在不同的语言中,它

被理解为对象(object)、纪录(record)、结构(struct)、字典(dictionary)、哈希

表(hash table)、有键列表(keyed list)、或者关联数组(associative array)。

值的有序列表(An ordered list of values)。在大部分语言中,它被理解为数组(array)。

这些都是常见的数据结构。实际上,大部分现代计算机语言都以某种形式支持它们,

这使得一种数据格式在同样基于这些结构的编程语言之间交换成为可能。

JSON 具有以下一些形式:

(1)对象

对象是一个无序的“‘名称/值’对”集合。一个对象以“{”(左括号)开始,“}”

(右括号)结束。每个“名称”后跟一个“:”(冒号);“‘名称/值’对”之间使用“,”

(逗号)分隔,比如:

{"username: "student",age:’20"}

JSON 对象示意图如图 2-16 所示。

图 2-16 JSON 对象示意图

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

64

(2)数组

数组是值(value)的有序集合。一个数组以“[”(左中括号)开始,“]”(右中括号)结

束。值之间使用“,”(逗号)分隔。JSON 的值(value)可以是双引号括起来的字符串(string)、

数值(number)、true、false、null、对象(object)或者数组(array),这些结构可以嵌套。

下面的代码就显示了一个数组结构表示的地址信息。

"addresses": [

{"type": "work", "format": "us","value": "Shangdi 1,Beijing,China"},

{"type": "home", "format": "us","value": "Guanyuan 1,Beijing,China"}

]

JSON 数组示意图如图 2-17 所示。

图 2-17 JSON 数组示意图

关于 JSON 如何在 Client 端和 Server 端传递,请参阅本书的第 9 章,这里不再赘述。

2.8 XMLHttpRequest 对象简介

XMLHttpRequest 对象是当今所有 Ajax 和 Web 2.0 应用程序的技术基础。尽管软件经

销商和开源社团现在都在提供各种 Ajax 框架以进一步简化 XMLHttpRequest 对象的使用;

但是,我们仍然很有必要理解这个对象的详细工作机制。

2.8.1 XMLHttpRequest 对象的属性和事件

XMLHttpRequest 对象拥有各种属性、方法和事件,以便于脚本处理和控制 HTTP 请求

与响应。下面我们将对此展开详细的讨论。

1. readyState 属性

当 XMLHttpRequest 对象把一个 HTTP 请求发送到服务器端时将经历若干种状

态,一直等待直到请求被处理,然后它才接收一个响应。这样,脚本才正确响应各

种状态——XMLHttpRequest 对象有一个描述对象当前状态的 readyState 属性,如表

2-6 所示。

CHAPTER 第 2 章 Ajax 基础知识

65

2

表 2-6 XMLHttpRequest 对象的 readyState 属性值

状 态 状 态 解 释

0 描述一种“未初始化”状态;此时,已经创建一个 XMLHttpRequest 对象,但是还没有初始化

1 描述一种“发送”状态;此时,代码已经调用了 XMLHttpRequest open()方法,并且 XMLHttpRequest

已经准备好把一个请求发送到服务器端

2 描述一种“发送”状态;此时,已经通过 send()方法把一个请求发送到服务器端,但是还没有收到响

3 描述一种“正在接收”状态;此时,已经接收到 HTTP 响应头部信息,但是消息体部分还没有完全接

收结束

4 描述一种“已加载”状态;此时,响应已经被完全接收

2. onreadystatechange 事件

无论 readyState 值何时发生改变,XMLHttpRequest 对象都会激发一个 readystatechange

事件。其中,onreadystatechange 属性接收一个 EventListener 值。

3. responseText 属性

这个 responseText 属性包含客户端接收到的 HTTP 响应的文本内容。当 readyState 值为 0、1

或 2 时,responseText 包含一个空字符串;当 readyState 值为 3(正在接收)时,响应中包含客户

端还未完成的响应信息,当 readyState 值为 4(已加载)时,该 responseText 包含完整的响应信息。

4. responseXML 属性

responseXML 属性用于当接收到完整的 HTTP 响应时(readyState 值为 4)描述响应内

容;此时,Content-Type 头部指定 MIME(媒体)类型为 text/XML、application/XML 或以

+XML 结尾。如果 Content-Type 头部并不包含这些媒体类型之一,那么 responseXML 的值

为 null。无论何时,只要 readyState 值不为 4,那么该 responseXML 的值也为 null。

其实,这个 responseXML 属性值是一个文档接口类型的对象,用来描述被分析的文档。

如果文档不能被分析(例如,如果文档的结构不完整或不支持文档相应的字符编码),那么

responseXML 的值将为 null。

5. status 属性

status 属性描述了 HTTP 状态代码,而且其类型为 short。仅当 readyState 值为 3(正在

接收中)或 4(已加载)时,这个 status 属性才可用。当 readyState 的值小于 3 时,试图存

取 status 的值将引发一个异常。

6. statusText 属性

statusText 属性描述了 HTTP 状态代码文本,并且仅当 readyState 值为 3 或 4 时才可用。

当 readyState 为其他值时,试图存取 statusText 属性将引发一个异常。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

66

2.8.2 XMLHttpRequest 对象的方法

XMLHttpRequest 对象提供了各种方法用于初始化和处理HTTP 请求,下面将逐个详细讨论。

1. abort()方法

可以使用 abort()方法来暂停与一个 XMLHttpRequest 对象相联系的 HTTP 请求,从而

把该对象复位到未初始化状态。

2. open()方法

需要调用 open(DOMString method,DOMString uri,boolean async,DOMString username,

DOMString password)方法初始化一个 XMLHttpRequest 对象。其中,method 参数是必须提供

的,用于指定用来发送请求的 HTTP 方法(GET、POST、PUT、DELETE 或 HEAD)。为了

把数据发送到服务器端,应该使用 POST 方法;为了从服务器端检索数据,应该使用 GET 方

法。另外,uri 参数用于指定 XMLHttpRequest 对象把请求发送到的服务器端相应的 URI。借

助于 window.document.baseURI 属性,该 uri 被解析为一个绝对的 URI;换句话说,你可以使

用相对的 URI-它将使用与浏览器解析相对的 URI 一样的方式解析。async 参数指定请求是否

是异步的,默认值为 true。为了发送一个同步请求,需要把这个参数设置为 false。对于要求

认证的服务器,你可以提供可选的用户名和口令参数。在调用 open()方法后,XMLHttpRequest

对象把它的 readyState 属性设置为 1(打开),并且把 responseText、responseXML、status 和

statusText 属性复位到它们的初始值。另外,它还复位请求头部。注意:如果调用 open()方法,

并且此时 readyState 值为 4,则 XMLHttpRequest 对象将复位这些值。

3. send()方法

在通过调用 open()方法准备好一个请求之后,需要把该请求发送到服务器端。仅当

readyState 值为 1 时,才可以调用 send()方法;否则,XMLHttpRequest 对象将引发一个异

常。当 async 参数为 true 时,send()方法立即返回,从而允许其他客户端脚本继续处理。在

调用 send()方法后,XMLHttpRequest 对象把 readyState 的值设置为 2(发送)。当服务器响

应时,在接收消息体之前,如果存在任何消息体,XMLHttpRequest 对象将把 readyState 的

值设置为 3(正在接收中)。当请求完成加载时,它把 readyState 的值设置为 4(已加载)。

对于一个 HEAD 类型的请求,它将在把 readyState 的值设置为 3 后再立即把它设置为 4。

send()方法使用一个可选的参数,该参数可以包含可变类型的数据。典型地,你可以使

用它并通过POST方法把数据发送到服务器端。另外,你可以显式地使用null参数调用 send()

方法,这与不用参数调用它一样。对于大多数其他的数据类型,在调用 send()方法之前,

应该使用 setRequestHeader()方法(见后面的解释)先设置 Content-Type 头部。如果在

send(data)方法中的 data 参数的类型为 DOMString,那么数据将被编码为 UTF-8;如果数据

CHAPTER 第 2 章 Ajax 基础知识

67

2

是 Document 类型,那么将使用由 data.XMLEncoding 指定的编码串行化该数据。

4. setRequestHeader()方法

setRequestHeader(DOMString header,DOMString value)方法用来设置请求的头部信息。

当 readyState 值为 1 时,你可以在调用 open()方法后调用这个方法;否则,将得到一个异常。

5. getResponseHeader()方法

getResponseHeader(DOMString header,value)方法用于检索响应的头部值。仅当

readyState 的值为 3 或 4(换句话说,在响应头部可用以后)时,才可以调用这个方法;否

则,该方法返回一个空字符串。

6. getAllResponseHeaders()方法

getAllResponseHeaders()方法以一个字符串形式返回所有的响应头部(每一个头部占单

独的一行)。如果 readyState 的值不为 3 或 4,则该方法返回 null。

2.8.3 发送请求和处理请求

在 Ajax 中,许多使用 XMLHttpRequest 的请求都是在一个 HTML 事件(例如,一个调

用 JavaScript 函数的按钮点击事件(onclick)或一个按键事件(onkeypress))中被初始化的。

Ajax 支持包括表单校验在内的各种应用程序。有时,在填充表单的其他内容之前要求校验

一个唯一的表单域。例如,要求使用一个唯一的 UserID 来注册表单。如果不是使用 Ajax

技术来校验这个 UserID 域,那么整个表单都必须被填充和提交;如果该 UserID 不是有效

的,这个表单必须被重新提交,如图 2-18 所示。

图 2-18 XHR 发送和处理请求状态图

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

68

例如:一个要求必须在服务器端进行校验的 Catalog ID 的表单域可能按下列形式指定。

<form name="validationForm" action="validateForm" method="post">

<table>

<tr><td>Catalog Id:</td>

<td>

<input type="text" size="20" id="catalogId"

name="catalogId" autocomplete="off" onkeyup="sendRequest()">

</td>

<td><div id="validationMessage"></div></td>

</tr>

</table>

</form>

</HTML>

前面的 HTML 使用一个 id 为 validationMessage 的 div 来显示输入域 Catalog Id 的一

个校验消息。onkeyup 事件调用 JavaScript 的 sendRequest()函数,这个函数创建一个

XMLHttpRequest 对象。创建一个 XMLHttpRequest 对象的过程因浏览器实现的不同而有

所区别,如果浏览器支持 XMLHttpRequest 对象作为一个窗口属性(所有普通的浏览器

都是这样的,除了 IE 5 和 IE 6 之外),那么可以调用 XMLHttpRequest 的构造器;如果

浏览器把 XMLHttpRequest 对象实现为一个 ActiveXObject 对象(就像在 IE 5 和 IE 6 中

一样),那么可以使用 ActiveXObject 的构造器。

下面的函数将调用一个 init()函数,它负责检查并决定要使用的适当的创建方法——在

创建和返回对象之前。

<script type="text/javascript">

function sendRequest(){

var XMLHttpReq=init();

function init(){

if (window.XMLHttpRequest) {

return new XMLHttpRequest();

}

else if (window.ActiveXObject) {

return new ActiveXObject("Microsoft.XMLHTTP");

}

}

</script>

接下来,需要使用 Open()方法初始化 XMLHttpRequest 对象)——指定 HTTP 方法和

要使用的服务器 URL。

CHAPTER 第 2 章 Ajax 基础知识

69

2

var value document.getElementById("catalogId").value;

var catalogId=encodeURIComponent(value);

XMLHttpReq.open("GET","validateForm?catalogId=" + catalogId,true);

在默认情况下,使用 XMLHttpRequest 发送的 HTTP 请求是异步进行的,对 URL

validateForm 的调用将激活服务器端的一个 Servlet,但是服务器端技术不是根本性的;实际

上,该 URL 可能是一个 ASP、ASP.Net、PHP 页面或一个 Web 服务,只要该页面能够返回

一个响应——指示 CatalogID 值是否是有效的——即可。因为在进行异步调用,所以需要注

册一个 XMLHttpRequest 对象将调用的回调事件处理器,当它的 readyState 值改变时调用。记

住,readyState 值的改变将会激发一个 readystatechange 事件。可以使用 onreadystatechange 属

性来注册该回调事件处理器,显式地把 async 参数设置为 true,如下所示:

XMLHttpReq.onreadystatechange=processRequest;

然后,需要使用 send()方法发送该请求。因为这个请求使用的是 HTTP GET 方法,所

以,可以在不指定参数或使用 null 参数的情况下调用 send()方法。

在这个示例中,因为 HTTP 方法是 GET,所以在服务器端接收 Servlet 将调用一个

doGet()方法,该方法将检索在 URL 中指定的 catalogId 参数值,并且从一个数据库中检

查它的有效性。

本示例中的这个 Servlet 需要构造一个发送到客户端的响应;而且,这个示例返回的是

XML 类型。因此,它把响应的 HTTP 内容类型设置为 text/xmls 并且把 Cache-Control 头部设

置为 no-cache。设置 Cache-Control 头部可以阻止浏览器简单地从缓存中重载页面。

public void doGet(HttpServletRequest request,HttpServletResponse response)

throws ServletException,IOException {

//...其他处理

//...

response.setContentType("text/xml");

response.setHeader("Cache-Control","no-cache");

//...其他处理

}

来自于服务器端的响应是一个 XML DOM 对象,此对象将创建一个 XML 字符串,其

中包含要在客户端进行处理的指令。另外,该 XML 字符串必须有一个根元素。

out.println("<catalogId>valid</catalogId>");

注意:

XMLHttpRequest 对象的设计目的是为了处理由普通文本或 XML 组成的响

应;但是,一个响应也可能是另外一种类型,如果用户代理(UA)支持这种内容类

型的话。

当请求状态改变时,XMLHttpRequest 对象调用使用 onreadystatechange 注册的事件处

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

70

理器。因此,在处理该响应之前,事件处理器应该首先检查 readyState 的值和 HTTP 状态。

当请求完成加载(readyState 值为 4)并且响应已经完成(HTTP 状态为“OK”)时,就可

以调用一个 JavaScript 函数来处理该响应内容。下列脚本负责在响应完成时检查相应的值

并调用一个 processResponse()方法。

function processRequest(){

if(XMLHttpReq.readyState==4){

if(XMLHttpReq.status==200){

processResponse();

}

}

}

该 processResponse() 方 法 使 用 XMLHttpRequest 对 象 的 responseXML 和

responseText 属性来检索 HTTP 响应。如上面所解释的,仅当在响应的媒体类型是

text/XML、application/XML 或以+XML 结尾的时,这个 responseXML 属性才可用,这

个 responseText 属性将以普通文本形式返回响应。对于一个 XML 响应,你可以按如下

方式检索内容:

var msg=XMLHttpReq.responseXML;

借助于存储在 msg 变量中的 XML,你可以使用 DOM 方法 getElementsByTagName()

来检索该元素的值。

var catalogId=msg.getElementsByTagName

("catalogId")[0].firstChild.nodeValue;

后,通过更新Web页面的 validationMessage div中的HTML内容并借助于 innerHTML

属性,你可以测试该元素值以创建一个要显示的消息。

if(catalogId=="valid"){

var validationMessage = document.getElementById("validationMessage");

validationMessage.innerHTML = "Catalog Id is Valid";

}

else

{

var validationMessage = document.getElementById("validationMessage");

validationMessage.innerHTML = "Catalog Id is not Valid";

}

上面展示了利用 XMLHttpRequest 和 Server 端请求的过程,其中的代码段是零散

的,希望读者在阅读完这一节后自行组合成一个可运行的程序,以加深对

XMLHttpRequest 的理解。

CHAPTER 第 2 章 Ajax 基础知识

71

2

2.9 本章小结

本章中,从 HTML 和浏览器的历史开始讲起,让读者明白 HTML 和浏览器之间的关

系;随后详细讲解了 DOM、CSS 和 JavaScript; 后讲解了 XMLHttpRequest 对象的属性

和方法。这些知识是 Web 开发的基础知识,也是组成 Ajax 的基本内容,在掌握好了这些

知识之后,才可以进一步地深入了解 Ajax。

CHAPTER

3

3.1 Prototype

3.1.1 什么是 Prototype

Prototype 是目前应用 为广泛的 Ajax 开发框架之一,它的特点是功能实用而且尺寸较

小,非常适合在中小型的 Web 应用中使用。开发 Ajax 应用需要编写大量的客户端 JavaScript

脚本,而 Prototype 框架可以大大地简化 JavaScript 代码的编写工作。更难得的是,Prototype

具备兼容各个浏览器的优秀特性,使用它可以不必考虑浏览器兼容性问题。

Prototype 对 JavaScript 的内置对象(如 String 对象、Array 对象等)进行了很多有用的

扩展,同时也新增了不少自定义的对象,包括对 Ajax 开发的支持等都是在自定义对象中实

现的。Prototype 可以帮助开发人员实现以下目标:

对字符串进行各种处理;

使用枚举的方式访问集合对象;

以更简单的方式进行常见的 DOM 操作;

使用 CSS 选择符定位页面元素;

发起 Ajax 方式的 HTTP 请求并对响应进行处理;

监听 DOM 事件并对事件进行处理。

第 3 章

Ajax 框架介绍

本章将详细介绍目前在一线开发中获得广泛使用的 Ajax 框

架,读者在阅读完本章后,应该能对主流的 Ajax 框架有清晰的认

识和深入的了解,并能够应用这些框架编写一些简单的应用。当

然,如果想要真正熟练并掌握这些框架,还需要读者在实际应用

中不断练习。

CHAPTER 第 3 章 Ajax 框架介绍

73

3

3.1.2 Prototype 的下载和引入

Prototype 代码可以到 Prototype 的官方网站 www.prototypejs.org 上下载,目前 新的

版本是 1.6。

Prototype 框架只有一个源代码文件 prototype.js,使用十分简单,只需要将该文件引入

即可。引入 prototype.js 文件的示例语句如下所示:

<script type="text/javascript" src="../javascript/prototype.js">

</script>

3.1.3 Prototype 常用函数介绍

使用 Prototype, 方便也 常用的就是它封装的一些函数,熟练使用这些函数,可以

减少开发过程中的代码量,提高开发的速度。

3.1.3.1 $()函数

第 2 章提及的 DOM API 中,有 document.getElementById 这个函数,该函数可以根据

传入的页面元素的 id 返回相应的元素对象。$()函数是 document.getElementById 函数的一

个简化写法,它比 DOM 中的方法功能更加强大,比如$()函数可以接收多个参数,返回满

足条件的 Array 对象。在 Prototype 中$()函数的实现代码如例 3-1 所示。

【例 3-1】 $()函数的实现代码

function $() {

var results = [], element;

for (var i = 0; i < arguments.length; i++) { //传入多个参数

element = arguments[i];

if (typeof element == 'string')

element = document.getElementById(element);

results.push(Element.extend(element)); //加入 results数组

}

// 如果只有一个对象,则返回该对象;如果有多个对象,则返回包含这些对象的数组

return results.length < 2 ? results[0] : results;

}

从例 3-1 中可以看到,当$()只有一个输入参数时,它和 document.getElementById 是等

效的;而当$()拥有多个输入参数时,其返回值是一个 Array 对象,这个数组包含了所有符

合条件的对象。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

74

3.1.3.2 $A()函数

$A()函数可以将一个可枚举的对象转换为一个 Array 对象,它和 Array.from()是等效的。

例 3-2 是 Prototype 中$A()函数的实现代码。

【例 3-2】 $()函数的实现代码

var $A = Array.from = function(iterable) {

if (!iterable) return [];// 如果对象不存在,则返回空数组

if (iterable.toArray) { // 如果有 toArray方法,则返回 toArray()

return iterable.toArray();

} else {

var results = [];

for (var i = 0; i < iterable.length; i++)

results.push(iterable[i]);

return results;//返回对象数组

}

}

可以看到,$A()函数是通过两种方式返回 Array 对象的。第一种方式是如果传入的对

象已经实现了 toArray 方法,则直接调用其 toArray 方法;第二种方式是通过循环的方式返

回对象的数组。使用第二种方式的前提是传入的对象具有 length 属性,并且可以通过序号

索引其内部的所有对象。例 3-3 是$A()函数的应用示例。

【例 3-3】 $A()函数应用示例

<html>

<head>

<title>chapter 3</title>

<script language="javascript" src="prototype.js" type=

"text/javascript"></script>

<script language="javascript" type="text/javascript">

function showOptions() {

// 获取所有 option对象的集合

var someNodeList = $("lstFramework").

getElementsByTagName("option");

// 通过$A方法把 option对象的集合转换为 Array对象

var nodes = $A(someNodeList);

var info = [];

nodes.each (function(node){ // 遍历 nodes

info.push(node.value + ": " + node.innerHTML);

});

CHAPTER 第 3 章 Ajax 框架介绍

75

3

alert(info.join("\r\n"));

}

</script>

</head>

<body>

<form>

<select id="lstFramework" size="10">

<option value="1">Prototype</option>

<option value="2">Script.aculo.us</option>

<option value="3">Dojo</option>

<option value="4">YUI</option>

</select>

<input onclick="showOptions();" type="button" value="Show the

options">

</form>

</body>

</html>

运行结果如图 3-1 所示。

图 3-1 $A()函数应用示例运行结果

例 3-3 是通过单击“Show the options”按钮触发 onclick 事件的响应函数 showOptions,该

函数使用$A 方法将 lstFramework 的 option 对象集合转换为数组输出,相应的输出结果如

图 3-1 所示。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

76

3.1.3.3 $F()函数

$F()是另一个经常使用的快捷方式,它是 Form.Element.getValue()函数的简化写法。$F()

函数用来求得页面中输入控件的输入值,它的输入参数可以是目标控件的 id 值,也可以是

目标控件对象本身。$F()支持的输入控件包括<input>系列(即 type=submit/hidden/

password/text/ checkbox/radio)、下拉列表<select>控件(在多选的情况下,$F()将会返回所

有选中值的 Array 对象)和<textarea>控件。例 3-4 给出了$F()函数的应用示例,该示例通

过单击按钮触发 onclick 事件的响应函数,调用$F()方法获得输入控件 username 的输入值,

然后输出。

【例 3-4】 $F()函数应用示例

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html>

<head>

<title>chapter 3</title>

<script type="text/javascript" language="javascript"

src="prototype.js" ></script>

<script type="text/javascript" language="javascript">

function test() {

alert($F("userName"));

}

</script>

</head>

<body>

<form>

<input type="text" id="userName" value="test username">

<input type="button" value="click" onclick="test()">

</form>

</body>

</html>

3.1.3.4 $H()函数

Prototype 中定义了 Hash 对象,用于模拟 Hash 数据结构,$H()函数的功能是将对象转

换为 Hash 对象。$H()函数将 Hash 对象的方法和属性通过 Object.extend 方法扩展到目标对

象上,这样返回的对象就具有了 Hash 对象的方法,如 keys 方法、values 方法等。例 3-5 是

$H()函数的一个应用示例。

CHAPTER 第 3 章 Ajax 框架介绍

77

3

【例 3-5】 $H()函数应用示例

<script>

function test() {

// 创建一个对象

var obj = {

key1: 1,

key2: 2,

key3: 3

};

// 将其转换为 Hash对象

var hash = $H(obj);

alert(hash.toQueryString());

}

</script>

3.1.3.5 $R()函数

$R()函数是 new ObjectRange(start,end,exclusive)的简化写法,它根据指定的起始边界返

回相应的对象范围。在 Prototype 中$R()函数的定义如下所示。

ObjectRange = Class.create();

Object.extend(ObjectRange.prototype, Enumerable);

Object.extend(ObjectRange.prototype, {

initialize: function(start, end, exclusive) {

this.start = start;

this.end = end;

this.exclusive = exclusive;

},

_each: function(iterator) {

var value = this.start;

do {

iterator(value);

value = value.succ();

} while (this.include(value));

},

// include函数的作用是判断 value是否包含在对象范围之内

include: function(value) {

if (value < this.start)

return false;

if (this.exclusive) // 注意 exclusive参数仅在判断上界是有效的

return value < this.end;

return value <= this.end;

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

78

}

});

var $R = function(start, end, exclusive) {

// 定义$R方法,$R是 new ObjectRange(start, end, exclusive)的一个简化写法或

快捷方式

return new ObjectRange(start, end, exclusive);

}

可以看到,ObjectRange 类是由 Enumerable 类继承而来的,它的初始化参数 start、end

和 exclusive 分别代表 ObjectRange 对象的下界、上界,以及是否排除上界的值。这里需

要注意的是,exclusive 参数仅仅对上界起作用,从 ObjectRange 的 include 方法的实现代

码中不难看出这一点。下面是$R()函数的应用示例,其中比较了 exclusive 参数分别为 true

和 false 时的区别。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html>

<head>

<title>chapter 3</title>

<script type="text/javascript" language="javascript"

src="prototype.js" ></script>

<script>

// 依次输出 1,2,3,4

function test_R1(){

var range = $R(1, 5, true);

range.each(function(value, index){

alert(value);

});

}

// 依次输出 1,2,3,4,5

function test_R2(){

var range = $R(1, 5, false);

range.each(function(value, index){

alert(value);

});

}

</script>

</head>

<body>

<form>

<input type="button" value="click (exclusive = true)"

CHAPTER 第 3 章 Ajax 框架介绍

79

3

onclick="test_R1()" />

<input type="button" value="click (exclusive = false)"

onclick="test_R2()" />

</form>

</body>

</html>

3.1.3.6 $$()函数

$$()函数是 Prototype 1.5 新增的一个快捷方式,它允许开发人员通过 CSS 样式选择页

面中的元素。熟悉 XPath 的读者会发现,CSS 选择符在语法形式上和 XML 文档的 XPath

十分类似,Prototype 支持的 CSS 选择符包括以下几种类型:

元素标签名称,例如:$$("li")。

元素 ID,例如:$$("#fixtures")。

CSS 类名,例如:$$(".first")。

元素是否具有某个属性,例如:$$("h1[class]")。

元素的某个属性是否符合特定的条件,例如:$$('a[href="#"]')、$$('a[class~=

"internal"]')、$$('a[href!=#]')。

上面所有这些 CSS 选择符的类型可以自由组合,形成一个复合的 CSS 选择符,例如:

$$('li#item_3[class][href!="#"]')。

不同的 CSS 选择符(包括复合 CSS 选择符)之间用空格分隔,就组成了一个多层的

CSS 选择符,它通过指定目标元素的父节点甚至更多层父节点的 CSS 样式属性来定位目标

元素。例如:$$('div[style] p[id] strong')。

下面给出了$$()函数的一个测试页面示例,读者可以在该页面中输入不同的 CSS 选择

符表达式,测试结果。

<!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" xml:lang="en" lang="en">

<head>

<title>chapter 3</title>

<style type="text/css" media="screen">

/* <![CDATA[ */

#testcss1 { font-size:11px; color: #f00; }

#testcss2 { font-size:12px; color: #0f0; display: none; }

/* ]]> */

</style>

<script type="text/javascript"language="javascript" src="prototype.js">

</script>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

80

<script>

function test() {

// 根据输入的 CSS选择符,切换相应元素的显示

$$($F('csspath')).each(

function(item) {

Element.toggle(item);

}

);

}

</script>

</head>

<body>

<form>

<div id="fixtures">

<h1 class="title">Some title <span>here</span></h1>

<p id="p" class="first summary">

<strong id="strong">This</strong> is a short blurb

<!-- 该页面元素具备 first和 internal两种 CSS样式-->

<a id="link_1" class="first internal" href="#">with a link

</a> or

<a id="link_2" class="internal highlight" href="#">

<em id="em">two</em>

</a>.

</p>

<ul id="list">

<li id="item_1" class="first">

<a id="link_3" href="#" class="external">

<span id="span">Another link</span>

</a>

</li>

<li id="item_2">Some text</li>

<li id="item_3" xml:lang="es-us" class="">Otra cosa</li>

</ul>

</div>

<input type="text" value="" id="csspath" />

<input type="button" value="click" onclick="test()" />

</form>

</body>

</html>

CHAPTER 第 3 章 Ajax 框架介绍

81

3

例 3-8 的运行页面如图 3-2 所示,在文本框中输入一个 CSS 选择符(例如:“.title”),

单击“click”按钮即可切换相应的页面元素的显示/隐藏状态。

(a)在文本框中输入 CSS 选择符".title" (b)页面元素"Some title here"隐藏

图 3-2 $$()函数应用示例运行页面

3.1.3.7 Try.these()函数

在程序开发的过程中有时会遇到这样的情况:在若干个函数中开发人员不能确定哪一

个会返回正确的结果,只能依次尝试。Prototype 中 Try.these()函数为开发人员提供了一个

很简便的方式来解决类似的问题。Try.these()函数的的定义如下所示。

var Try = {

these: function() {

// 返回结果

var returnValue;

for (var i = 0; i < arguments.length; i++) {

// 输入参数为多个 function对象

var lambda = arguments[i];

try {

// 如果其中一个函数成功返回,则返回该函数的结果

returnValue = lambda();

break;

} catch (e) {}

}

return returnValue;

}

}

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

82

从上述代码中可以看到,Try.these()函数的每一个参数都必须是一个无参的

JavaScript 方法。在 Prototype 框架中,实现 Ajax 对象的 getTransport 方法就用到了

Try.these()函数,getTransport 方法的作用是返回一个 XMLHttpRequest 对象,而在不同

浏览器中创建 XMLHttpRequest 对象的方式是不同的。通过 Try.these()函数可以依次尝

试各种浏览器创建 XMLHttpRequest 对象的方法,直到成功为止。Ajax 对象的

getTransport 方法的实现代码如下:

var Ajax = {

getTransport: function() {

// 尝试以下 3种创建 XMLHttpRequest对象的方法

// 1. Mozilla浏览器中的 XMLHttpRequest对象

// 2. 创建 ActiveX对象"Msxml2.XMLHTTP"

// 3. 创建 ActiveX对象"Microsoft.XMLHTTP"

return Try.these(

function() {return new XMLHttpRequest()},

function() {return new ActiveXObject('Msxml2.XMLHTTP')},

function() {return new ActiveXObject('Microsoft.XMLHTTP')}

) || false;

},

// 当前活动的 Ajax请求计数

activeRequestCount: 0

}

第 2 章介绍了创建 XMLHttpRequest 对象的方法,代码中对浏览器的类型、版本进行

了判断, 终返回 XMLHttpRequest 对象的实例。与第 2 章中使用的方法相比,Prototype

框架中 Ajax 对象的实现代码更加简洁、明了。

3.1.4 Prototype 的 Ajax 功能

作为一个 Ajax 开发框架,Prototype 对 Ajax 开发提供了有力的支持。在 Prototype 中,

与 Ajax 相关的类和对象包括:Ajax、Ajax.Responders、Ajax.Base、Ajax.Request、

Ajax.PeriodicalUpdater 和 Ajax.Updater。下面分别对这些类和对象进行介绍。

3.1.4.1 Ajax 对象

Ajax 对象为其他的 Ajax 功能类提供了 基本的支持,它的实现包括一个方法

getTransport 和一个属性 activeRequestCount。getTransport 方法返回一个 XMLHttpRequest

对象,activeRequestCount 属性代表正在处理中的 Ajax 请求的个数。

CHAPTER 第 3 章 Ajax 框架介绍

83

3

3.1.4.2 Ajax.Base 类

Ajax.Base 类是 Ajax.Request 类和 Ajax.PeriodicalUpdater 类的基类,它提供了 3 个方法:

setOptions:设置 Ajax 操作所使用的选项。

responseIsSuccess:判断 Ajax 操作是否成功。

responseIsFailure:判断 Ajax 操作是否失败(与 responseIsSuccess 相反)。

Ajax.Base 类的主要作用是提取一些公用的方法,其他类通过继承的方式使用这些方

法,实现代码复用。

3.1.4.3 Ajax.Request 类

Ajax.Request 类是 Prototype 中 经常使用的一个 Ajax 相关类。首先给出一个 简单的

Ajax.Request 类的应用示例,如例 3-6 所示。

【例 3-6】 Ajax.Request 类应用示例

Ajax.Request 测试页面:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html>

<head>

<title>chapter 3</title>

<script type="text/javascript" language="javascript"

src="prototype.js" ></script>

<script type="text/javascript" language="javascript">

function test() {

// 创建 Ajax.Request对象,发起一个 Ajax请求

var myAjax = new Ajax.Request(

'data.html', // 请求的 URL

{

method: 'get', // 使用 GET方式发送 HTTP请求

onComplete: showResponse // 指定请求成功完成时需要执行的方法

}

);

}

function showResponse(response) {

$('divResult').innerHTML = response.responseText;

}

</script>

</head>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

84

<body>

<input type="button" value="click" onclick="test()" />

<div id="divResult" />

</body>

</html>

data.html 页面:

<input type="text" id="name" />

<input type="button" value="Click Me" onclick="sayHi()">

Ajax.Request 对象在初始化时需要提供两个参数:第 1 个参数是将要请求页面的

URL,这里使用的 data.html 是一个普通的 HTML 静态页面;第 2 个参数是 Ajax 操作的

选项,在 Prototype 中并没有专门为 Ajax 操作选项定义一个类,通常都是像例 3-11 这

样,通过匿名对象的方式设置 Ajax 操作的参数。在例 3-11 中,Ajax 操作选项具有两个

属性:method 表示 HTTP 请求方式,默认为 POST 方式;onComplete 指定了 Ajax 操作

完成以后(即 XMLHttpRequest 对象的 status 属性为 4 时),页面将要执行的函数。当然,

Ajax 操作还包括很多其他选项,如表 3-1 所示。

表 3-1 Ajax 操作选项属性及其含义

属性名称 含 义

method HTTP 请求方式(POST/GET/HEAD)

parameters 在 HTTP 请求中传入的 URL 格式的值列表,即 URL 串中问号之后的部分

asynchronous 是否做异步 XMLHttpRequest 请求

postBody 在 POST 请求方式下,传入请求体中的内容

requestHeaders 和请求一起被传入的 HTTP 头部列表,这个列表必须含有偶数个项目,因为列表中每两项为一组,

分别代表自定义部分的名称和与之对应的字符串值

onXX

在 HTTP 请求、响应的过程中,当 XMLHttpRequest 对象状态发生变化时调用的响应函数。响应函

数有 5 个:onUninitialized、onLoading、onLoaded、onInteractive 和 onComplete。传入这些函数的参数

可以有 2 个,其中第 1 个参数是执行 HTTP 请求的 XMLHttpRequest 对象;第 2 个参数是包含被执行

的 X-JSON 响应的 HTTP 头

onSuccess Ajax 操作成功完成时调用的响应函数,传入的参数与 onXXXXXXXX 相同

onFailure Ajax 操作请求完成但出现错误时调用的响应函数,传入的参数与 onXXXXXXXX 相同

onException Ajax 操作发生异常情况时调用的响应函数,它可以接收 2 个参数,其中第 1 个参数是执行 HTTP

请求的 XMLHttpRequest 对象;第 2 个参数是异常对象

3.1.4.4 Ajax.Updater 类

例 3-11 使用 Ajax.Request 类实现了页面的局部刷新效果,而这样类似的功能在 Ajax

应用中是经常使用的。因此,为了简化这种工作,Prototype 框架从 Ajax.Request 类中派生

出一个子类——Ajax.Updater。与 Ajax.Request 相比,Ajax.Updater 的初始化多了一个

CHAPTER 第 3 章 Ajax 框架介绍

85

3

container 参数,该参数代表将要更新的页面元素的 id。例 3-7 的功能通过 Ajax.Updater 的

实现会变得更加简单,如例 3-12 所示。

【例 3-7】 Ajax.Updater 类的应用示例

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html>

<head>

<title>chapter 3</title>

<script type="text/javascript" language="javascript"

src="prototype.js" ></script>

<script type="text/javascript" language="javascript">

function test() {

var myAjax = new Ajax.Updater(

'divResult', // 更新的页面元素

'data.html', // 请求的 URL

{

method: 'get'

}

);

}

</script>

</head>

<body>

<input type="button" value="click" onclick="test()" />

<div id="divResult" />

</body>

</html>

此外,Ajax.Updater 类还具有另外一个功能,即:如果请求的页面内容中包括

JavaScript 脚本,Ajax.Updater 类可以执行其中的脚本,只需要在 Ajax 操作选项中增

加属性“evalScripts: true”即可。对例 3-11中的 data.html进行修改,在其中加入 JavaScript

脚本,如例 3-8 所示。

【例 3-8】 data.html 页面

<script language="javascript" type="text/javascript">

sayHi = function() {

alert("Hello, " + $F('name') + "!");

}

</script>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

86

<input type="text" id="name" />

<input type="button" value="Click Me" onclick="sayHi()">

调用 Ajax.Updater 的 JavaScript 脚本修改为:

function test() {

var myAjax = new Ajax.Updater(

'divResult', // 更新的页面元素

'data.html', // 请求的 URL

{

method: 'get',

evalScripts: true

}

);

}

这样就可以使用 data.html 页面的内容更新当前页面中的<div>元素 divResult,并且执

行 data.html 页面中包含的 JavaScript 脚本。

这里需要注意的是例 3-13 中 sayHi 函数的写法,如果写成

function sayHi() {

alert("Hello, " + $F('name') + "!");

}

或者

var sayHi = function() {

alert("Hello, " + $F('name') + "!");

}

程序是不能正常运行的。这是因为 Ajax.Updater 执行脚本是通过 eval 的方式,而不是

将脚本内容引入到当前页面,直接声明 function sayHi 或者用 var 声明 sayHi 函数,其作用

域只是在这段脚本内部,外部的其他脚本不能访问 sayHi 函数。而按照例 3-13 的方式声明

的函数,其作用域是整个 Window 对象。

3.1.4.5 Ajax.PeriodicalUpdater 类

和 Ajax.Request 类相似,Ajax.PeriodicalUpdater 类也继承自 Ajax.Base 类。在一些 Ajax

应用中,需要周期性地更新某些页面元素,例如天气预报、即时新闻等。实现这样的功能

通 常 要 使 用 JavaScript 中 的 定 时 器 函 数 setTimeout 、 clearTimeout 等 , 而 有 了

Ajax.PeriodicalUpdater 类则可以很好地简化这类编码工作。

新建一个 Ajax.PeriodicalUpdater 类的实例需要指定 3 个参数:

container:将要更新的页面元素 id;

CHAPTER 第 3 章 Ajax 框架介绍

87

3

url:请求的 URL 地址;

options:Ajax 操作选项。

和 Ajax.Updater 类相似,Ajax.PeriodicalUpdater 类也支持动态执行 JavaScript 脚本,只

需在 Ajax 操作选项中增加(evalScripts: true)属性值即可。

Ajax.PeriodicalUpdater 类支持两个特殊的 Ajax 操作选项:frequency 和 decay。

frequency 参数很容易理解,既然是定时更新页面元素,或者定时执行脚本,那么多长

时间更新或者执行一次呢?frequency 指的就是两次 Ajax 操作之间的时间间隔,单位

是秒,默认值为 2 秒。

如果仅指定 frequency 参数,程序会按照固定的时间间隔执行 Ajax 操作。这样的更

新策略合理吗?答案取决于请求 URL 中数据的更新频率。如果请求的数据会很有规律

地按照固定频率改变,那么只要设置一个合适的 frequency 值,就可以很有效地实现页

面的定时更新。然而实际应用中的数据往往不会那么理想,例如新闻,可能在一天中只

有特定的一段时间更新频率会很高,而在其他时间则几乎没有变化。经常遇到这样的情

况该怎么办呢?Ajax.PeriodicalUpdater 类支持的 decay 属性就是为了解决这个问题而产

生的。当 option 中带有 decay 属性时,如果请求返回的数据与上次相同,那么下次进行

Ajax 操作的时间间隔会乘以一个 decay 的系数。

为了比较明显地看到 decay 属性的效果,在请求的测试页面中加入记录时间的脚本,

代码如例 3-9 所示。

【例 3-9】 Ajax.PeriodicalUpdater 类应用示例

ex10.html:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html>

<head>

<title>chapter 3</title>

<script type="text/javascript" language="javascript"

src="prototype.js" ></script>

<script type="text/javascript" language="javascript">

var count = 0;

function test() {

var myAjax = new Ajax.PeriodicalUpdater(

'divResult', // 定时更新的页面元素

'script1.html', // 请求的 URL

{

method: 'get', // HTTP请求的方式为 GET

evalScripts: true, // 是否执行请求页面中的脚本

frequency: 1, // 更新的频率

decay: 2 // 衰减系数

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

88

}

);

}

</script>

</head>

<body>

<input type="button" value="click" onclick="test()" />

<div id="divResult" ></div>

<div id="divResult2" ></div>

</body>

</html>

script1.html:

<script language="javascript" type="text/javascript">

// Ajax.PeriodicalUpdater调用函数计数

count++;

// 在<div>元素 divResult2中增加一行结果,并记录当前时间和

// Ajax.PeriodicalUpdater的调用次数

var str = $('divResult2').innerHTML;

$('divResult2').innerHTML=str+"count="+count+":"+new Date()+"<br>";

</script>

例 3-9 的运行结果如图 3-3 所示。

图 3-3 Ajax.PeriodicalUpdater 类应用示例运行结果

可以看到,由于请求返回的数据一直没有发生变化,每次请求时间的间隔是上一次

的 2 倍(decay=2);如果某一次请求返回的数据发生了变化,那么执行请求的时间间隔则

恢复到初始值。

3.1.4.6 Ajax.Responders 对象

Ajax.Responders 对象维护了一个正在运行的 Ajax 对象列表,在需要实现一些全局

CHAPTER 第 3 章 Ajax 框架介绍

89

3

的功能时就可以使用它。例如,在 Ajax 请求发出以后需要提示用户操作正在执行中,而

操作返回以后则取消提示。利用 Ajax.Responders 对象就可以实现这样的功能,如例 3-10

所示。

【例 3-10】 Ajax.Responders 对象应用示例

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html>

<head>

<title>chapter 3</title>

<script type="text/javascript" language="javascript"

src="prototype.js" ></script>

<script type="text/javascript" language="javascript">

function test() {

var myAjax = new Ajax.Request(

'data.html',

{

method: 'get',

onComplete: showResponse

}

);

}

function showResponse(response) {

$('divResult').innerHTML = response.responseText;

}

var handle = {

onCreate: function() {

Element.show('loading'); // 当创建 Ajax请求时,显示 loading

},

onComplete: function() {

// 当请求成功返回时,如果当前没有其他正在运行中的 Ajax请求,隐藏

loading

if (Ajax.activeRequestCount == 0) {

Element.hide('loading');

}

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

90

}

};

// 将 handle注册到全局的 Ajax.Responders对象中,使其生效

Ajax.Responders.register(handle);

</script>

</head>

<body>

<input type="button" value="click" onclick="test()" />

<div id="divResult" ></div>

<div id='loading' style="display:none">

<img src="loading.gif">Loading...

</div>

</body>

</html>

例 3-10 中定义了一个 handle 对象,其中包含 onCreate 和 onComplete 函数。页面中发

出任何一个 Ajax 请求时都会调用 onCreate 方法,而请求完成时都会调用 onComplete 方法。

例 3-10 的运行结果如图 3-4 所示。

图 3-4 Ajax.Responders 对象应用示例运行结果

3.2 script.aculo.us

通过上一节的介绍,我们可以知道,Prototype 框架提供了很多很强大的功能,简化

了 Web 中 JavaScript 的开发,但是同时也应该看到,Prototype 提供的是对 JavaScript 底

层功能的封装和简化,而一些高级的 JavaScript 效果,如动画、拖拽、文本框自动填充

等,Prototype 并没有提供这方面的功能,如果要实现这些功能,我们应该怎么办呢?幸

好,在 Prototype 强大的控制能力的基础上,很多人已经实现了这些功能,并且效果非

常好。

还 是 先 让 我 们 来 看 一 个 小 例 子 吧 ( 如 图 3-5 所 示 ), 这 个 例 子 可 以 在

http://yura.thinkweb2.com/ scripting/contextMenu/下载。图 3-5 中展示了一个单击右键弹出菜

单的例子,可以看出右键效果几乎和 Windows 默认的效果一样,但使用的代码却很少。

CHAPTER 第 3 章 Ajax 框架介绍

91

3

图 3-5 Prototype 扩展效果示例

展示这个效果的代码如下:

document.observe('dom:loaded', function(){

var myMenuItems = [

{

name: 'New', className: 'new',

callback: function(e) {

var tagName = e.element().tagName.toLowerCase(),

x = e.screenX, y = e.screenY;

alert('you clicked on <' + tagName + '> element at x: ' +

x + ',

and y: ' + y);

}

},{ separator: true },{

name: 'Edit', className: 'edit',

callback: function() { alert('Forward function called'); }

},{

name: 'Copy', className: 'copy',

callback: function() { alert('Copy function called'); }

},{

name: 'Delete', disabled: true, className: 'delete'

},{ separator: true},{

name: 'Save', className: 'save',

callback: function() { alert('Saving...'); }

},{separator: true },{

name: 'Save as .xsl', className: 'xsl',

callback: function() { alert('Saving as .xsl'); }

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

92

},{

name: 'Save as .doc', className: 'doc',

callback: function() {alert('Saving as .doc'); }

},{

name: 'Save as .pdf', className: 'pdf',

callback: function() { alert('Saving as .pdf'); }

},{separator: true},{

name: 'Send to...', disabled: true, className: 'send'

}

],

new Proto.Menu({

selector: '#desc',

className: 'menu desktop',

menuItems: myMenuItems

})

})

我们需要在页面上做的就是定义一个 myMenuItems 对象,然后定义一个 Proto.Menu

对象而已,而需要引入的这个 Proto.Menu 对象,大小也仅仅 2KB 多点,因此是一个轻

量级的工具。

更多的 Prototype 扩展库,可以参见 http://scripteka.com/网站。而在这一节中介绍的

script.aculo.us,则是众多 Prototype 扩展库之上的翘楚,提供了全面而方便的特效实现。如果还不

了解 script.aculo.us ,那么先登录苹果(http://www.apple.com/)、美国航天局(http://www.nasa.gov/)、

美国有线电视网(http://www.cnn.com)等网站体验一下吧,其中美国航天局为了纪念成立 60

周年,特意将其网站改版成 Web 2.0 的站点,其中就大量使用了 script.aculo.us ,建议读者登

录这个网站,亲自体验一下这个框架的功能。

3.2.1 script.aculo.us 简介

script.aculo.us 是一个跨浏览器的 JavaScript 用户界面库,提供了包括动画(Animation)、

拖放(Drag/Drop)、Ajax、DOM 操作工具类及单元测试(Unit Testing)等功能,它是 Prototype

JavaScript 框架的一个扩展。script.aculo.us 里面封装了许多控件,常见的有 AotuComplete、

Accordion、Slider 等,很多的动画效果,通过简单的几行代码就可以实现。

顺便提一下,框架的名字有些奇怪,那是因为这个框架的开发组的官方网站的域名是

script.aculo.us,这就是这个框架命名的由来。

CHAPTER 第 3 章 Ajax 框架介绍

93

3

3.2.2 script.aculo.us 的引入和使用

3.2.2.1 script.aculo.us 的引入

下面来看一下 script.aculo.us 的使用。首先自然是到官方网站 http://script.aculo.us 上下

载 script.aculo.us,当前的 新版本是 1.8.1。下载完后解压缩到本地,会发现有 lib、src、test

等目录,其中 lib 目录存放的是它依赖的 Prototype 库,src 目录存放的是它的效果文件,目

前有 builder、effects、dragdrop、controls、slider、sound。需要注意的是,它的效果库文件

是相互分离的,用户可以选择性地使用。test 目录是一些测试文件,我们也可以使用里面提

供的方法对网页进行 JavaScript 测试。

要想使用 script.aculo.us,首先,将上面提到的 lib 目录下的 Prototype 文件和 src 目录下

的所有文件复制到网站的某一个目录下,比如 javascript 目录下。需要注意的是,我们需要

把 lib 目录下的所有 JS 文件都添加进去。

然后,在新建的文件中添加下面代码:

<script src="javascripts/prototype.js" type="text/javascript"></script>

<script src="javascripts/scriptaculous.js" type="text/javascript"/>

在默认情况下,scriptaculous.js 文件自动加载 lib 目录下的其他文件。下面详细分析

scriptaculous 的加载过程。

如下面代码所示,在 scriptaculous.js 中,首先定义了一个 Scriptaculous 对象,这样

就会形成一个全局的 JavaScript 对象,体现了 OOP 的思想。然后运行其 load 方法,在

其中的 load 方法中查找并向网页中添加其他的 JavaScript 库文件,默认会添加所有的文

件(builder、effects、dragdrop、controls、slider、sound)。如果我们只需要某个特性,

那么就可以选择性地加载。例如,在网页中我们可能不需要声音处理,那么可以使用

“scriptaculous.js?load=”指定需要加载的文件即可,如 scriptaculous.js?load=effects,

dragdrop 将只加载效果库和拖拽库。

从下面这段代码中也学习到如何在网页中选择性地加载 JavaScript 文件。这种办法对

于需要在一个页面中加载很多 JavaScript 文件时比较有效,并且在一个地方改动后,对所

有引用都有效。

var Scriptaculous = {

require: function(libraryName) {

// inserting via DOM fails in Safari 2.0, so brute force approach

document.write('<script type="text/javascript" src="'+libraryName+'">

<\/script>');

},

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

94

load: function() {

function convertVersionString(versionString){

var r = versionString.split('.');

return parseInt(r[0])*100000 + parseInt(r[1])*1000 + parseInt

(r[2]);

}

if((typeof Prototype=='undefined') ||

(typeof Element == 'undefined') ||

(typeof Element.Methods=='undefined') ||

(convertVersionString(Prototype.Version) <

convertVersionString(Scriptaculous.REQUIRED_PROTOTYPE)))

throw("script.aculo.us requires the Prototype JavaScript framework

>= " +

Scriptaculous.REQUIRED_PROTOTYPE);

$A(document.getElementsByTagName("script")).findAll( function(s) {

return (s.src && s.src.match(/scriptaculous\.js(\?.*)?$/))

}).each( function(s) {

var path = s.src.replace(/scriptaculous\.js(\?.*)?$/,'');

var includes = s.src.match(/\?.*load=([a-z,]*)/);

(includes ? includes[1] : 'builder,effects,dragdrop,

controls,slider,sound').split(',').each(

function(include) { Scriptaculous.require(path+include+'.js') });

});

}

}

Scriptaculous.load();

3.2.2.2 script.aculo.us 的使用

在引入 scritp.aculo.us 后,就可以在页面上使用它的方法了,不过推荐使用下面的写法:

<script type="text/javascript" language="javascript">

// <![CDATA[

Effect.Appear('element_id');

// ]]>

</script>

通过这种方法就不需要担心“<”和“>”等特殊符号不被网页解析了。

下面,我们来看第一个例子。

首先,在硬盘上建立如图 3-6 所示结构的文件夹。

CHAPTER 第 3 章 Ajax 框架介绍

95

3

图 3-6 文件结构

其中,javascript 文件夹中存放的是所需要的 scritp.aculo.us 库文件和 prototype.js 文件;

css 文件夹中存放一个 css 文件,然后在 effect 目录下建立一个 HTML 文件,命名为

helloworld.html,内容如下:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

<html>

<head>

<title>Script.aculo.us HelloWorld</title>

<meta http-equiv="Content-Type" content="text/html; GBK">

<script type="text/javascript" src="../javascript/prototype.js">

</script>

<script type="text/javascript" src="../javascript/scriptaculous.

js.js">

</script>

<style type="text/css" href="./css/css.css"></style>

</head>

<body>

<div id="logsummary" onclick="new Effect.SwitchOff(this);">

点击,就会隐藏哦

</div>

</body>

</html>

当运行这个 HTML 文件后,效果如图 3-7 所示。

图 3-7 隐藏文字效果

点击边框,这个边框就会逐渐隐藏。实现这个效果,仅仅需要 new Effect.SwitchOff(this)

这一行代码!

如果还觉得不过瘾,那么就向网页上再添加如下代码:

<div id="logsumary" onclick="new Effect.BlindUp(this,{duration:16})">

</div>

当每次点击方框时,这个方框都会向下增大一些。

类似的效果在 script.aculo.us 中实在太多了,在下面的学习中会逐渐掌握这些技巧。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

96

3.2.3 script.aculo.us 的功能

script.aculo.us 主要由以下几个文件组成:

builder,提供了操纵 DOM 的支持;

effects,提供了各种特效;

dragdrop,提供了跨浏览器的拖拽支持;

controls,控制功能;

slider,滑动。

下面讲解这些方面的内容。

3.2.3.1 DOM Builder

关于 DOM 的概念,我们在第 2 章中已经讲过了,在这里主要了解如何操作 DOM 节点。

DOM 操作在 JavaScript 中是 基本也 常用的操作,在 script.aculo.us 中,Builder.js

文件提供了一个 Builder 类,提供了简洁而强大的 DOM 操作功能。

在 script.aculo.us 中,提供了一个 Builder 对象,封装了 DOM 的主要操作。下面来看看

Builder 的语法:

Builder.node( elementName )

Builder.node( elementName, attributes )

Builder.node( elementName, children )

Builder.node( elementName, attributes, children )

其中,elementName 指标签的名称;attributes 指标签的属性,比较常见的属性有 id、

className、style、onclick 等;children 是一个对象数组,即将被作为子对象添加到这个节

点上。需要注意的是,如果要指定节点的 class 属性,则要使用 className 来代替,这是为

了避免在 Firefox 中出现问题,Firefox 在处理 DOM 节点的 class 时有一个缺陷,会使节点

的高度出现问题。另外,还需要注意的是,如果要指定节点的 for 属性(多用在操作 Label

标签时),因为 for 是 JavaScript 的关键字,因此要使用 htmlFor 代替。

下面我们来看一个具体的例子。如何使用 Builder 创建一个 table,这也是在入职笔试

中经常会出现的一个题目,题目会要求使用 JavaScript 和 DOM 动态创建一个 table。在这

里,我们同时用 DOM 和 script.aculo.us 提供的 Builder 来实现。

用 JavaScript 动态创建 table,需要使用 document 对象的 createElement 方法,函数

如下:

oElement = document.createElement(sTag)

参数:

sTag:一个对象的 tag 名称,比如:table、tr、td、div 等。

返回值:返回这个对象的引用。

CHAPTER 第 3 章 Ajax 框架介绍

97

3

我们的目标是在一个 div 中创建一个 table,下面是用 DOM 方法实现的代码。

//获取 body标签

var container = document.getElementsById("divCat");

// 创建一个<table>元素和一个<tbody>元素

oTable= document.createElement("table");

oTableBody = document.createElement("tbody");

// 创建一个<tr>元素

oRow = document.createElement("tr");

// 创建一个<td>元素

oTd = document.createElement("td");

//创建一个文本节点

oText = document.createTextNode("单元格内容");

// 将创建的文本节点添加到<td>里

oText.appendChild(currenttext);

// 将列<td>添加到行<tr>

oRow.appendChild(oText);

// 将行<tr>添加到<tbody>

oTableBody.appendChild(oRow);

// 将<tbody>添加到<table>

oTable.appendChild(oTableBody);

// 将表格 mytable的 border属性设置为 2

oTable.setAttribute("border", "2");

//将<table>添加到<body>

container.appendChild(oTable);

上面的代码就不多讲了,主要是几个 API 的大量重复使用,造成代码繁复冗长,降低

了代码的质量和美感。下面我们来看看使用 Builder 是如何实现的。

//创建 table

table = Builder.node('table', {width:'100%',cellpadding:'2',

cellspacing:'0',border:'2'});

//创建 tbody

tbody = Builder.node('tbody');

//创建 tr

tr = Builder.node('tr',{className:'header'});

//创建 td

td = Builder.node('td',[ Builder.node('strong', '单元格内容')]);

//绑定关系

tr.appendChild(td);

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

98

tbody.appendChild(tr);

table.appendChild(tbody);

$('divCat').appendChild(table);

同样的功能,我们仅仅用了 12 行代码,并且还多加了 cellpadding 和 cellspacing 这两

个属性,比之纯粹 DOM 操作,优势十分明显。

3.2.3.2 动画效果

script.aculo.us 的动画效果是非常有吸引力的,如果还没有领略的话,可以先到

http://wiki.script.aculo.us/scriptaculous/show/CombinationEffectsDemo 体验一下。

script.aculo.us 中所有的动画效果都是基于时间,而不是通过在网页中内嵌 frame 来

实现的,这就意味着我们可以精确地控制动画效果到某一秒。script.aculo.us 会根据指定

的时间来实现动画效果,尽管在不同的浏览器上展现的速度可能不尽相同。 重要的是,

这些效果都是跨浏览器的,不管底层运行的是 Internet Explorer、Firefox 还是 Safari,都

会有一致的效果。

script.aculo.us 的效果库可以分为可视化效果库和效果队列两种,可视化效果库主要是

单个的效果;效果队列则是让这些效果排队,依次施加到某一个对象中。

首先我们来看一下可视化效果库。可视化效果库又分为核心效果库和综合效果库,核

心效果库包括 Effect.Opacity、Effect.Scale、Effect.Morph、Effect.Move、Effect.Highlight 和

Effect.Parallel。其中 Effect.Opacity 用来改变对象的透明度,可以设置成半透明到完全透明

不等;Effect.Scale 可以调整对象的长度和高度;Effect.Morph 可以改变对象的 css 属性;

Effect.Move 实现对象的移动动画效果;Effect.Highlight 通过变换对象的背景颜色,形成闪

烁的效果,一般用来实现通知用户页面某一部分已经发生改变;Effect.Parallel 比较特别,

这个动画把多个 effect 集成在一起,形成一个多个动画效果并列执行的效果。

下面看一下核心效果库的语法。

new Effect.EffectName( element, required-params, [options] );

而 Effect.Parallel 的语法如下:

new Effect.Parallel([array of subeffects], [options]);

EffectName 指 script.aculo.us 提供的效果名称,目前主要有下面几种,见表 3-2。

表 3-2 script.aculo.us 效果汇总

效果集合 效 果 相反效果 备 注

In & Out 效果

Appear:节点淡入 Fade:节点淡出 淡入淡出

BlindUp:从底向上逐步卷起消失 BlindDown:自顶向下逐步消失 卷帘效果

SlideUp:自底向上移动消失 SlideDown:自顶向下移动消失 上下出入

Grow:增大 Shrink:缩小 大小变化

CHAPTER 第 3 章 Ajax 框架介绍

99

3

续表

效果集合 效 果 相反效果 备 注

Out 效果

DropOut:下拉消失 Fold:底部向上折叠后消失 —

Puff:膨胀后消失 Squish:挤压后消失 体积变化

SwitchOff:上下对折后消失 — —

Attention 效果

Highlight:高亮 — —

Shake:左右晃动 — —

Pulsate:震动 — —

Scale:改变大小 — —

element 既可以是页面对象的 id,也可以是 JavaScript 中的 DOM 对象,如 new

Effect.DropOut(document.getElementById('demo-all'))就是先通过 getElementById 取

得 DOM 对象,然后在其上添加 DropOut 效果。

required-params 的添加取决于使用的效果,大部分 effect 是不需要这个参数的。

options 参数给 effect 添加自定义的参数,在使用时外面要用大括号,每个参数用

name:value 表示,各个参数用逗号分隔开。比如:

new Effect.Opacity('my_element', { duration: 2.0, transition: Effect.

Transitions.linear,from: 1.0, to: 0.5 });

可选参数可以分为公共参数和效果特定参数,公共参数就是对每个核心效果库都适用

的参数。下面是核心效果库的公共参数,见表 3-3。

表 3-3 script.aculo.us 核心效果库的公共参数

选 项 版 本 说 明

duration V1.0 效果的持续时间,以秒为单位,浮点数表示,默认为 1.0 秒

fps V1.0 动画时每秒帧数,默认为 25,不可以超过 100

transition V1.0

设置动画的过渡效果,值在 0~1 之间,可能的取值如下: Effect.Transitions.

sinoidal (默认 )、Effect.Transitions.linear、Effect.Transitions.reverse、Effect.

Transitions.wobble、Effect.Transitions.flicker

from V1.0 设置过渡的起点,是 0.0 和 1.0 之间的一个浮点数。默认为 0.0

to V1.0 设置过渡的终点,是 0.0 和 1.0 之间的一个浮点数。默认为 0.0

sync V1.0 设置是否重新绘制新的帧,如果设置为 true,就需要手工调用效果的

render()方法,默认为 false。主要在 Effect.Parallel()方法中使用

queue V1.5 设置效果队列

delay V1.5 设置动画开始前的延迟时间,默认为 0.0 秒

direction — 设置动画方向,可选项为“ top-left”、“ top-right”、“ bottom-left”、

“bottom-right”或“center”(默认)。只有在 Grow and Shrink 效果中有用

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

100

除了上面的公共参数外,还可以在公共参数中添加回调函数,也就是在动画开始运行

时,可以使用 JavaScript 响应各种事件。这些回调函数有一个动画对象的引用作为参数,

例如,下面的例子中会列出所有的动画对象的 element 属性。

function callback(obj){

for(var i in obj.effects){

alert(obj.effects[i]['element'].id);

}

}

这些事件包括如下回调函数,见表 3-4。

表 3-4 script.aculo.us 效果事件

回调函数名称 版 本 说 明

beforeStart V1.0 效果展现之前调用

beforeUpdate V1.0 在效果展示的循环中,每次帧重绘之前调用

afterUpdate V1.0 在效果展示的循环中,每次帧重绘之后调用

afterFinish V1.0 效果展现结束后调用

在回调函数中,除了 effect 对象之外,还可以访问如下对象,见表 3-5。

表 3-5 script.aculo.us 效果回调函数中的辅助对象

变 量 版 本 说 明

effect.element V1.0 动画添加的目标

effect.options V1.0 动画的可选参数

effect.currentFrame V1.0 当前帧的上一帧的顺序号

effect.startOn, effect.finishOn V1.0 动画已经开始的时间及剩余时间,单位为 ms

effect.effects[] V1.0 取得 Effect.Parallel 效果中的效果数组 effects[]

下面是一个使用回调函数的例子,在动画开始和结束时取得正在展示动画对象的信息。

function myCallBackOnFinish(obj){

alert("the Element's id the effect was applied to is :" + obj.element.id);

}

function myCallBackOnStart(obj){

alert("the Element object the effect will be applied to is :"+obj.element);

}

//开始创建效果

new Effect.Highlight(myObject,

{ startcolor:'#ffffff',

endcolor:'#ffffcc',

CHAPTER 第 3 章 Ajax 框架介绍

101

3

duration: 0.5,

afterFinish: myCallBackOnFinish,

BeforeStart: myCallBackOnStart

});

下面是 Effect.Parallel 的例子。

new Effect.Parallel(

[new Effect.Move(element,{sync: true, x: 20, y: -30, mode: 'relative'}),

new Effect.Opacity(element, {sync: true, to: 0.0, from: 1.0 } ) ],

{ duration: 0.5,

afterFinish: function(effect) { Element.hide(effect.effects[0].

this.parentNode); }

}

);

3.2.3.3 Ajax 功能

script.aculo.us 提供了非常强大的 Ajax 功能,主要有 Autocompletion 和 In Place Editing

两个部分,我们在这一节中将研究这两个功能的实现。

1. Autocompletion

类似于 Google Suggest,script.aculo.us 提供了非常方便的使用 Ajax 调用服务器端自动

填充文本框功能,当用户在输入框中输入一些字符时,页面会根据用户的当前输入形成一

个匹配列表供用户选择,从而减少用户的输入量,提高用户体验。

这种效果在查找用户名、查找有规律字符串方面特别有用。下面是某航空公司网

上购票时选择城市的情景(如图 3-8 所示),当在出发城市中输入 bei 这几个字符时,

输入框下面就会自动弹出以 bei 开头的城市供用户选择,可以看到,这种效果对用户而

言是非常方便的。

图 3-8 自动匹配效果示例

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

102

下面介绍 script.aculo.us 提供的这种自动匹配功能。

自动匹配的语法如下:

new Ajax.Autocompleter(element, update, url, options);

其中第一个参数是文本框的标识符,使用 id 或者 name 都可以;第二个参数是当服务

器响应时更新的对象,一般会用一个 div 来显示;第三个参数是请求的地址;第四个参数

指定自动填充时的参数,这些参数及说明见表 3-6。

表 3-6 script.aculo.us 自动匹配语法参数及说明

选 项 默 认 值 说 明

paramName 文本框的 name 属性 向服务器发送的参数的名称,默认用文本框的 name 表示

Tokens [] 当指定的 token 出现时,就发起一次自动匹配过程,可以设置一个,如

tokens: ',',也可以设置多个,如 tokens: [',', '\n']

frequency 0.4 自动匹配的频率,单位为 ms(毫秒)

minChars 1 发动自动匹配时用户输入字数的 小长度

indicator null 当调用 Ajax 发起文本匹配时,将显示这个对象,文本匹配结束后关闭显示

updateElement null

回调函数,当用户选择匹配列表中的值后,将调用该函数而不调用默认

的函数填充文本框。这个函数只有一个参数,也就是用户在匹配列表中选

择的对象

afterUpdateElement null

回调函数,在用户选择匹配列表中的一项,内建的函数更新了匹配文本

框的值之后执行。这个函数接收两个参数,分别是自动填充文本框和用户

选择的匹配项

callback null

回调函数,如果设置这个值,那么在发送 Ajax 请求之前,会先调用这

个函数来改变 Ajax 请求中的参数值。这个函数接收当前的自动填充文本

框和已经生成的参数值,返回需要发送到服务器端的参数

parameters null

如果除了当前输入的值之外,还想发送更多的参数值,就可以通过这个

参数设定,格式是:

{field: 'value',another: 'value'}或者 'field=value&another=value'

下面我们来看一个实际的例子:当用户在页面上输入多于一个字符的名字时,返回一

个名字列表供用户选择。

首先,在页面上添加一个文本输入框作为自动匹配框,还要添加一个当服务器返回匹

配结果时显示的列表区域。代码如下:

<input type="text" id="autocomplete" name="autocomplete_parameter"/>

<div id="autocomplete_choices" class="autocomplete"></div>

其中第一个是自动匹配框,第二个是用户可以选择的匹配列表。

然后,在上面代码的下面添加一个 script.aculo.us 定义的自动匹配器。

CHAPTER 第 3 章 Ajax 框架介绍

103

3

new Ajax.Autocompleter("autocomplete", "autocomplete_choices",

"/url/on/server", {});

接着,需要定义 CSS,以便当匹配列表在页面上出现时显示不同的颜色。

div.autocomplete {

position:absolute;

width:250px;

background-color:white;

border:1px solid #888;

margin:0px;

padding:0px;

}

div.autocomplete ul {

list-style-type:none;

margin:0px;

padding:0px;

}

div.autocomplete ul li.selected { background-color: #ffb;}

div.autocomplete ul li {

list-style-type:none;

display:block;

margin:0;

padding:2px;

height:32px;

cursor:pointer;

}

后,编写服务器端的代码,处理用户匹配请求的参数,我们这里不讨论具体的服务

器端技术,只需要保证返回如下代码即可:

<ul>

<li>your mom</li>

<li>yodel</li>

</ul>

这样,一个简单的文本匹配器就形成了。

如果需要添加一个指示器,那么把输入框的代码换成下面的就可以了:

<span id="indicator1" style="display: none">

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

104

<img src="/images/spinner.gif" alt="Working..." />

<div id="autocomplete_choices" class="autocomplete"></div>

相应地,要修改自动匹配器:

new Ajax.Autocompleter("autocomplete","autocomplete_choices","/url/on/server",

{paramName: "value", minChars: 2, updateElement: addItemToList,

indicator:'indicator1'}

);

2. In Place Editing

In Place Editing 在中文里有“就地编辑”的意思,在页面表现上可能就是一段提示性

文字(Label),鼠标悬停在上面时会有边框出现。当用户点击这段文字(Label)时,切换

到 Input 状态。用户输入完毕鼠标移开,又会切换回 Label 状态,提供了丰富的用户视觉体

验,同时也节约了页面空间。

先来看下面的例子(见图 3-9),在页面上有这么一段文字:

图 3-9 In Place Editing 示例(一)

当把鼠标移动到这段文字上面的时候,就会出现下面的提示效果(见图 3-10)。

图 3-10 In Place Editing 示例(二)

当在这段文字上点击鼠标时,就会出现一个输入框让你编辑(见图 3-11)。

图 3-11 In Place Editing 示例(三)

当然,除了文本输入框之外,还可以任意选择其他的控件,如下拉列表等。这些都

是 script.aculo.us 提供的 Ajax.InPlaceEditor 具有的功能,下面我们来学习如何使用这个

组件。

首先来看一下语法:

new Ajax.InPlaceEditor( element, url, [options]);

与以前的控件一样,element 表示将这个功能添加到页面的哪个组件上;url 是将修改

的值发送到服务器中的地址;options 表示可选项。

目前的可选项有如下几个,见表 3-7。

CHAPTER 第 3 章 Ajax 框架介绍

105

3

表 3-7 InPlaceEditor 参数可选项

可 选 项 版 本 默 认 值 说 明

okButton V1.6 true 当出现编辑状态时是否出现提交按钮 (true,false)

okText V1.5 ok 提交按钮的显示文字

cancelLink V1.6 true 当出现编辑状态时是否出现取消链接 (true,false)

cancelText V1.5 cancel 取消链接中的显示文字

savingText V1.5 Saving… 当发送到服务器端时显示的文字

clickToEditText V1.6 Click to edit 当鼠标移动到可编辑文字上时显示的提示

formId V1.5 参数 element 的 id 属性加上

InPlaceForm 参数 element 的 id

externalControl V1.5 null

页面对象 id,用来设置是否进入编辑状态的外部控

制器。当控件进入编辑状态时这个对象会被设置为隐

藏,当退出编辑状态时重新显示

rows V1.5 1 当进入编辑状态时输入框的高度,如果设置大于 1,

则会显示为多行文本输入框

onComplete V1.6

function(transport, element) {new

Effect.Highlight(element,

{startcolor:

this.options.highlightcolor});}

回调函数,当当前输入框的值在服务器端更新成功

后调用。默认调用 Effect.Highlight 效果

onFailure V1.6

function(transport) {alert("Error

communicating with the server: " +

transport.responseText.stripTags());}

回调函数,当当前输入框的值在服务器端更新失败

后调用。默认显示服务器端错误代码

cols V1.5 none 文本框的列数,对单行和多行文本框都起作用

size V1.5 none 与 cols 同义

highlightcolor ? Ajax.InPlaceEditor.defaultHighlig

htColor 高亮时的颜色

highlightendcolor ? "#FFFFFF" 高亮效果消退时的颜色

savingClassName V1.5 "inplaceeditor-saving" 当向服务器端更新时显示的文本的 css 类

formClassName V1.5 "inplaceeditor-form" 设置当前输入框所在 form 的 class 属性

hoverClassName ? None 设置当鼠标移动到输入框时的 class 属性

loadTextURL V1.5 Null 进入编辑模式后,将从这个地址获取显示在编辑框

中的文字

loadingText V1.5 "Loading…" 当从服务器端加载文字时,页面上显示的提示性文

callback V1.5 function(form)

{Form.serialize(form)}

回调函数,在服务器端发送 Ajax 请求前调用,可以修

改发送到服务器端的请求中的参数,接收两个参数,一

个是当前文本框所在的 form;另一个是当前文本框的值

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

106

在学习完语法之后,我们来看一个例子。

首先,在页面上添加如下代码:

<p id="editme">Click me, click me!</p>

<script type="text/javascript">

new Ajax.InPlaceEditor('editme', '/demoinplaceeditservlet');

</script>

在上面这个例子中,我们只需要指定一个显示文字的节点(editme)和一个服务器端

处理 url(/demoinplaceeditservlet,处理 Ajax 请求的一个 Servlet),即可完成一个简单的就

地编辑,script.aculo.us 提供的功能实在是太强大、太好用了!

下面是一个多行文本框的例子,只需要指定行数和列数即可。

<p id="editme2">Click me to edit this nice long text.</p>

<script type="text/javascript">

new Ajax.InPlaceEditor('editme2', '/demoajaxreturn.html',

{rows:15,cols:40});

</script>

下面的例子演示了在线 WiKi 的功能,用户如果对内容感到不满意,可以随时修改网

页的内容(见图 3-12)。

图 3-12 script.aculo.us In Place Editing 例子:在线 WiKi 示例

将鼠标移动到这段内容上之后的效果如图 3-13 所示。

CHAPTER 第 3 章 Ajax 框架介绍

107

3

图 3-13 script.aculo.us In-place Editing 例子:WiKi 显示

用户在文字上点击鼠标,进入修改状态,如图 3-14 所示。

图 3-14 script.aculo.us In Place Editing 例子:WiKi 编辑

3.3 jQuery

jQuery 是一个 JavaScript 库,它有助于简化 Ajax 编程。jQuery 是 John Resig 在 2006

年年初创建的,jQuery 具有独特的基本原理,可以简洁地表示常见的复杂代码。对于任何

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

108

使用 JavaScript 代码的程序员来说,它是一个非常有用的 JavaScript 库。学习 jQuery 基

本原理,探索其特性和功能,执行一些常见的 Ajax 任务并掌握如何使用插件扩展 jQuery,

是本节中的任务。

3.3.1 jQuery 简介

jQuery 是一个快速、简练的 JavaScript 工具箱,它能够让你以简单的方式来操作 HTML

元素、处理事件、实现特效并为 Web 页面添加 Ajax 交互。在 jQuery 的网站上还特意强调:

“jQuery 能够改变你编写 JavaScript 的方式。”

那么 jQuery 究竟有什么魔力,能够信誓旦旦地说改变人们开发 JavaScript 的方式呢?

先来看一个例子。

例子中有一些 class 属性等于 surprise 的段落(<p>),这些段落现在是隐藏不可见的,现

在要给这些段落添加一个 class 属性,值为 ohmy,添加完后缓慢地显示这些段落。

如果用 W3C 的 DOM 操作来实现,我们应该怎么做呢?做法如下:

Var paras = document.getElementsByTag(‘p’);

For (var p in paras){

If(p.class=’surprise’){

p.setAttribute(‘class’,’surprise ohmy’);

p.show();

}

}

Function show(){

…//显示代码省略

}

在上面的代码中,首先选择了所有符合条件的段,然后添加属性,再显示,用了

10 多行代码,并且缓慢显示的效果还没有达到(因为这是一个动画效果,DOM 中没有

提供太多的支持)。那么来看看 jQuery 是如何实现的吧:

$("p.surprise").addClass("ohmy").show("slow");

令人惊奇的是,jQuery 只用了一行代码,就实现了所有的代码!这个例子展示了 jQuery

强大的一面:简化 DOM 操作,并且拥有 RoR 风格的简约之美,而这正是 jQuery 在首页

上宣称的:The quick and dirty。

3.3.2 jQuery 的使用

3.3.2.1 jQuery 的引入

首先,到 http://jquery.com 下载 新版本,截至成书时,jQuery 的 新版本是 1.2.3。首

CHAPTER 第 3 章 Ajax 框架介绍

109

3

页上有 3 个版本:一个是开启 Gzip 压缩的 mini 版本;一个是经过压缩变换处理过的版本;

还有一个是未压缩的版本。一般说来,3 个文件都要下载,在开发时可以使用未压缩的版

本,在应用发布时再根据需要换上压缩版本或者压缩变换的版本。

3.3.2.2 简单的代码简化

下面是一个简单示例,它说明了 jQuery 对代码的影响。要执行一些真正简单和常见

的任务,比如为页面的某一区域中的每个链接附加一个单击(click)事件,我们可以使用

纯 JavaScript 代码和 DOM 脚本来实现。

var external_links = document.getElementById('external_links');

var links = external_links.getElementsByTagName('a');

for (var i=0;i < links.length;i++) {

var link = links.item(i);

link.onclick = function() {

return confirm('You are going to visit: ' + this.href);

};

}

下面的代码表示使用 jQuery 实现了相同的功能。

$('#external_links a').click(function() {

return confirm('You are going to visit: ' + this.href);

});

是不是很神奇? 使用 jQuery,你可以把握问题的要点,只让代码实现自己想要的功

能,而省去了一些烦琐的过程。无须对元素进行循环,click() 函数将完成这些操作。同样

也不需要进行多个 DOM 脚本调用,你只需要使用一个简短的字符串对所需的元素进行定

义即可。

理解这一代码的工作原理可能会有一点复杂。首先,使用了 $() 函数——jQuery 中

功能 强大的函数。通常,我们都使用这个函数从文档中选择元素。在本例中,一个包

含有一些层叠样式表语法的字符串被传递给函数,然后 jQuery 尽可能高效地把这些元

素找出来。

如果你具备 CSS 选择器的基本知识,那么应该很熟悉这些语法。在上面的代码中,

#external_links 用于检索 id 为 external_links 的元素。#external_links 后面紧跟着用空格隔开

的 a,表示 jQuery 需要检索 external_links 元素中的所有 <a> 元素。这个选择用语言描述

起来非常绕口,甚至在 DOM 脚本中也是这样,但是在 CSS 中却再简单不过了。

$() 函数返回一个含有与 CSS 选择器匹配的所有元素的 jQuery 对象。jQuery 对象

类似于数组,但是它附带有大量特殊的 jQuery 函数。比如,我们可以通过调用 click 函

数把 click 处理函数指定给 jQuery 对象中的所有元素。

还可以向 $() 函数传递一个元素或者一个元素数组,该函数将把这些元素封装在一个

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

110

jQuery 对象中。我们可能会想要使用这个功能将 jQuery 函数用于一些对象,比如 window

对象。例如,我们通常会像下面这样把函数分配给加载事件。

window.onload = function() {

// do this stuff when the page is done loading

};

使用 jQuery 编写的功能相同的代码如下:

$(window).load(function() {

// run this when the whole page has been downloaded

});

我们可能有所体会,等待窗口加载的过程是非常缓慢而且令人痛苦的,这是因为必须

等待整个页面加载完所有的内容,包括页面上所有的图片。有的时候,我们希望首先完成

图片加载,但是在大多数情况下,只需加载 HTML 就可以了。通过在文档中创建特殊的

ready 事件,jQuery 解决了这个问题,方法如下:

$(document).ready(function() {

// 这个方法中可以执行各种需要等待 HTML加载完毕的操作

});

这个代码围绕 document 元素创建了一个 jQuery 对象,然后建立一个函数,用于在

HTML DOM 文档就绪时调用实例。可以根据需要任意地调用这个函数,并且能够以真正

的 jQuery 格式,使用快捷方式调用这个函数。这很简单,只需向 $() 函数传递一个函数

就可以了。

<html>

<head>

<script type="text/javascript" src="./javascript/jquery-1.2.3.js">

</script>

<script type="text/javascript">

$(document).ready(function(){//在 HTML加载完成时执行此函数

$('a').click(function(){//添加链接的处理函数

alert("Thanks for visiting!");

$('a').addClass('test').show('slow')

.html("This is a Hello World in jQuery way.");

return false;

});

});

</script>

<style type="text/css">

a.test { font-weight: bold; }

CHAPTER 第 3 章 Ajax 框架介绍

111

3

</style>

</head>

<body>

<a href="#">jQuery</a>

<br/>

</body>

</html>

下面是代码的运行情况,当页面显示时,页面上只显示一个 jQuery 链接;当用户单击

这个链接时,就会执行链接上注册的事件监听器,显示一个弹出框,并在链接上添加一个

css 属性,以缓慢的方式显示这个链接,并改变链接内的文字(见图 3-15)。

图 3-15 jQuery 的$()函数应用示例

至此,已经向大家介绍了 $() 函数的 3 种用法。第 4 种用法是可以使用字符串来创建

元素,结果会产生一个包含该元素的 jQuery 对象。下面的代码中展示了如何在页面中添

加了一个段落:

创建和附加一个简单的段落:

$('<p></p>') .html('Hey World!').css('background', 'yellow').appendTo ("body");

除了$()函数外,jQuery 还有一个强大的函数 each(fn),这个函数的作用是;以每一个

匹配的元素作为上下文来执行一个函数。这意味着,每次执行传递进来的函数时,函数中

的 this 关键字都指向一个不同的元素(每次都是一个不同的匹配元素)。而且,在每次执行

函数时,都会给函数传递一个表示作为执行环境的元素在匹配的元素集合中所处位置的数

字值作为参数。

还是看一个例子吧。先看 HTML 代码:

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

112

<img/><img/>

jQuery 代码如下:

$("img").each(function(i){ this.src = "test" + i + ".jpg"; });

运行结果如下:

<img src="test0.jpg"/><img src="test1.jpg"/>

在这个例子中,通过$("img")选中图片元素后,再通过 each 函数对每个图片元素运行

内嵌的函数,并设置 src 属性。同时我们也能看出,each 函数中含有一个参数 i,表示当前

操作的对象在选择的对象中的位置,这对于有些情况是十分有用的。

在上面几个例子中你可能已经注意到,jQuery 中的另一个功能强大的特性就是方法链

接(Method Chaining)。每次对 jQuery 对象调用方法时,都会返回相同的 jQuery 对象。

这意味着如果需要对 jQuery 对象调用多个方法,那么不必重新键入选择器就可以实现这一

目的。

$('#message').css('background', 'yellow').html('Hello!').show();

通过方法链接,还可以在当前选中的对象上添加或者删除一些对象,也可以修改选择

的对象,然后恢复以前的选择对象。下面是一个更加复杂的例子。

$("a").filter(".clickme")

.click(function(){//选中链接 class为 clickme的链接,添加 click的处理函数

alert("You are now leaving the site.");

}).end()//结束当前的选择对象

.filter(".hideme").click(function(){//重新选择并添加 click处理函数

$(this).hide();

return false;

}).next().click(function(){//选择当前对象的下一个兄弟对象

Alert(“I am a normal Link.”);

}).end();

下面是代码所要操作的页面元素:

<a href="http://google.com/" class="clickme">

I give a message when you leave</a>

<a href="http://yahoo.com/" class="hideme">Click me to hide!</a>

<a href="http://jquery.com/">I'm a normal link</a>

在上面的代码中,我们用 filter 来过滤想要选择的对象,用 end 方法来结束当前的选择,

用 next 方法选择当前元素的下一个元素。

CHAPTER 第 3 章 Ajax 框架介绍

113

3

3.3.2.3 DOM 脚本和事件处理

或许 jQuery 擅长的就是简化 DOM 脚本和事件处理。遍历和处理 DOM 非常简

单,附加、移除和调用事件也十分容易,并且不像手动操作那样容易出错。

从本质上说,jQuery 可以使 DOM 脚本中的常用操作变得更加容易。我们可以创建

元素并且使用 append() 函数把它们与其他的元素链接到一起,使用 clone()函数复制元

素,使用 html()函数设置内容,使用 empty() 函数删除内容,使用 remove() 函数删除所

有的元素。

通过遍历 DOM,一些函数可以用于更改 jQuery 对象本身的内容;可以获得元素所

有的 siblings()、parents() 和 children();还可以选择 next() 和 prev() 兄弟元素。find() 函

数或许是功能 强大的函数,它允许使用 jQuery 选择器搜索 jQuery 对象中元素的后代

元素。

如果结合使用 end() 函数,那么这些函数将变得更加强大。这个函数的功能类似于

undo 函数,用于返回到调用 find() 或 parents() 函数(或者其他遍历函数)之前的 jQuery

对象。

如果配合方法链接(Method Chaining)一起使用,这些函数可以使复杂的操作看上去

非常简单。下面的代码示例包含有一个登录表单,并处理了一些与之有关的元素。

$('form#login') // 隐藏 form中所有 class为 optional的 label标签

.find('label.optional').hide().end()

//对所有的密码框添加红色边框

.find('input:password').css('border', '1px solid red').end()

// 添加 form的事件处理器

.submit(function(){

return confirm('Are you sure you want to submit?');

});

这个代码只有一行,首先,选择登录表单。然后,发现其中含有可选标签,隐藏它们,

并调用 end() 返回表单。接着,创建密码字段,将其边界变为红色,再次调用 end() 返回

表单。 后,在表单中添加一个提交事件处理程序。其中尤为有趣的就是(除了其简洁性

以外),jQuery 完全优化了所有的查询操作,确保将所有内容很好地链接在一起后,不需

要对一个元素执行两次查询。

处理常见事件就像调用函数(比如 click()、submit() 或 mouseover())和为其传递事件

处理函数一样简单。此外,还可以使用 bind('eventname', function(){})指定自定义的事件

处理程序;使用 unbind('eventname') 删除某些事件或者使用 unbind() 删除所有的事件。

上面的例子展示了 jQuery 的 DOM 操作的 API,这些 API 只是 jQuery 强大功能中极小

的一部分。jQuery 的 API 分为 Core、DOM 操作、JavaScript 辅助增强、动画特效、事件增

强、Ajax 支持和插件等部分,其中的 Core 操作主要包括$()函数和 each()函数,上面已经介

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

114

绍过了。下面简要说明一下 DOM API,如表 3-8 所示。

表 3-8 jQuery DOM API 说明

功 能 方 法 返 回 说 明

过滤

eq(index) jQuery 当前选中集合的第 index 个元素,index 从 0 开始,以集合的长度减一

结束

hasClass(class) Boolean 判断当前选择元素的 class 属性和传入的 class 是否相等,如果有一项

相同则返回 true

filter(expr) jQuery 将当前选中元素中不符合匹配表达式的元素除去

filter(fn) jQuery 将当前选中元素中不符合匹配函数的元素除去

is(expr) Boolean 判断当前选中元素和匹配表达式是否相符,如果当前选中元素中有一

项相符合则返回 true

map(callback) jQuery 将当前 jQuery 对象中的元素转换成数组,并将 callback 函数应用到数

组的每一个对象中

not(expr) jQuery 从当前选择对象中去除所有与匹配表达式相同的对象

slice(start, end) jQuery 从当前选中元素中取子集

查找

add(expr) jQuery 把与表达式匹配的元素添加到 jQuery 对象中。这个函数可以用于连

接分别与两个表达式匹配的元素结果集

children(expr) jQuery 取得一个包含匹配的元素集合中每一个元素的所有唯一子元素的元

素集合

contents() jQuery 把元素集合筛选为包含指定文本(区分大小写)的元素

find(expr) jQuery 搜索所有与指定表达式匹配的元素。这个函数是找出正在处理的元素

的后代元素的好方法

next(expr) jQuery

取得一个包含匹配的元素集合中每一个元素紧邻的后面同辈元素的

元素集合。这个函数只返回后面那个紧邻的同辈元素,而不是后面所有

的同辈元素。可以用一个可选的表达式进行筛选

parent(expr) jQuery 取得一个包含着所有匹配元素的唯一父元素的元素集合。可以通过一

个可选的表达式进行筛选

查找

parents(expr) jQuery 取得一个包含着所有匹配元素的唯一祖先元素的元素集合(不包含根

元素)。可以通过一个可选的表达式进行筛选

prev(expr) jQuery

取得一个包含匹配的元素集合中每一个元素紧邻的前一个同辈元素

的元素集合。可以用一个可选的表达式进行筛选

这个函数只返回前一个紧邻的同辈元素,而不是前面所有的同辈元

siblings(expr) jQuery 取得一个包含匹配的元素集合中每一个元素的所有唯一同辈元素的

元素集合。可以用可选的表达式进行筛选

链接 end() jQuery

结束 近的“破坏性”操作,把匹配的元素列表恢复到前一个状态。

在调用 end 函数后,匹配的元素列表会恢复到上一个操作之前的匹配元

素列表状态

CHAPTER 第 3 章 Ajax 框架介绍

115

3

在具体使用这些 API 时,可以参考 jQuery 中文网站的文档说明,网址为:

http://jquery.org.cn/ visual/c n/index.xml。

3.3.2.4 Ajax 功能

使用 jQuery 将使 Ajax 变得极其简单。jQuery 提供一些函数,可以使简单的工作变

得更加简单,复杂的工作变得不再复杂。

Ajax 常见的用法就是把一段 HTML 代码加载到页面的某个区域中。为此,

只需简单地选择所需的元素,然后使用 load() 函数即可。下面是一个更新统计信息

的示例。

$('#stats').load('stats.html');

通常,我们只需简单地把一些参数传递给服务器中的某个页面即可。使用 jQuery

实现这一操作非常简单,可以使用 $.post()或$.get(),这由所需的方法决定。如果需要

的话,还可以传递一个可选的数据对象和回调函数。下面为一个发送数据和使用回调

函数的简单示例。

$.post('save.cgi', {

text: 'my string',

number: 23

}, function() {

alert('Your data has been saved.');

});

如果确实需要编写一些复杂的 Ajax 脚本,那么需要用到 $.ajax() 函数。我们可以

指定 xml、script、html 或 json,jQuery 将自动为回调函数准备合适的结果,这样我们

便可以立即使用该结果。还可以指定 beforeSend、error、success 或 complete 回调函

数,向用户提供更多有关 Ajax 体验的反馈。此外,还有其他的一些参数可供使用,比

如可以使用它们设置 Ajax 请求的超时,也可以设置页面 “ 近一次修改”的状态。下

面为一个参数检索 XML 文档的示例。

$.ajax({

url: 'document.xml',

type: 'GET',

dataType: 'xml',

timeout: 1000,

error: function(){

alert('Error loading XML document');

},

success: function(xml){

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

116

//暂不处理响应结果

}

});

当 success 回调函数返回 XML 文档后,我们可以使用 jQuery 检索这个 XML 文档,

其方式与检索 HTML 文档是一样的。这样使得处理 XML 文档变得相当容易,并且把内

容和数据集成到了 Web 站点中。下面的代码为 success 函数的一个扩展,它为 XML 中的

每个 <item> 元素都添加了一个列表项到 Web 页面中。

success: function(xml){

$(xml).find('item').each(function(){

var item_text = $(this).text();

$('<li></li>')

.html(item_text)

.appendTo('ol');

});

}

3.3.2.5 动画效果

可以使用 jQuery 处理基本的动画和显示效果。animate() 函数是动画代码的核心,它

用于更改任何随时间变化的数值型的 CSS 样式值。比如,可以变化高度、宽度、不透明

度和位置;还可以指定动画的速度,设定单位为毫秒的数值或者预定义的速度:慢速、中

速或快速。

下面是一个同时变化某个元素高度和宽度的示例。请注意,这些参数没有开始值,只

有 终值,开始值取自元素的当前尺寸。同时也附加了一个回调函数。

$('#grow').animate({ height: 500, width: 500 }, "slow", function(){

alert('The element is done growing!');

});

使用 jQuery 的内置函数,可以很容易地完成很多常见的动画。可以使用 show() 和

hide() 函数,立即显示或者以特定的速度显示;还可以通过使用 fadeIn() 和 fadeOut()或者

slideDown() 和 slideUp()函数显示和隐藏元素,这取决于所需要的显示效果。下面的示例

定义了一个下滑的导航菜单。

$('#nav').slideDown('slow');

3.3.2.6 jQuery 选择器

我们经常会使用 ID 如 #myid 来选择元素,或者通过类名如 div.myclass 来选择元素。

然而,jQuery 提供了更为复杂和完整的选择器语法,允许我们在单个选择器中选择几乎所

有的元素组合。

CHAPTER 第 3 章 Ajax 框架介绍

117

3

jQuery 的选择器语法主要是基于 CSS 3 和 XPath 的。对 CSS 3 和 XPath 了解得越

多,使用 jQuery 时就越得心应手。有关 jQuery 选择器的完整列表,包括 CSS 和 XPath,

请参阅参考资料[1]。

CSS 3 包含一些并不是所有浏览器都支持的语法,因此很少被使用。然而,我们仍

然可以在 jQuery 中使用 CSS 3 选择元素,因为 jQuery 具备自己的自定义选择器引

擎。比如,要在表格的每一个空列中都添加一个横杠,可以使用“:empty 伪选择器”

(pseudo-selector):

$('td:empty').html('-');

如果需要找出所有不含特定类的元素呢? CSS 3 同样提供了一个语法可以实现这个

目的,使用 :not 伪选择器。如下代码表示如何隐藏所有不含 required 类的输入内容。

$('input:not(.required)').hide();

与在 CSS 中一样,可以使用逗号将多个选择器连接成一个。下面是一个同时隐藏页

面上所有类型列表的简单示例。

$(‘ul, ol, dl’).hide();

XPath 是一种功能强大的语法,用于在文档中搜寻元素。它与 CSS 稍有区别,不过

它能实现的功能略多于 CSS。要在所有复选框的父元素中添加一个边框,可以使用 XPath

的 /.. 语法:

$("input:checkbox/..").css('border', '1px solid #777');

jQuery 中也加入了一些 CSS 和 XPath 中没有的选择器。比如,要使一个表更具有可

读性,通常可以在表格的奇数行或偶数行中附加一个不同的类名——也可以称做“把表分

段”(striping)。使用 jQuery 不费吹灰之力就可以做到这点,这主要归功于 odd 伪选择器。

下面这个例子使用 striped 类改变了表格中所有奇数行的背景颜色。

$('table.striped > tr:odd').css('background', '#999999');

通过上面的介绍,我们可以看到强大的 jQuery 选择器是如何简化代码的。不论想处

理什么样的元素,也不管这个元素是具体的还是模糊的,都有可能使用一个 jQuery 选择器

对它们进行定义。

3.3.2.7 插件扩展 jQuery

与大多数软件不同,使用一个复杂的 API 为 jQuery 编写插件并不是非常困难的。事

实上,jQuery 插件非常易于编写,我们甚至希望编写一些插件来使代码更加简单。下面是

可以编写的 基本的 jQuery 插件。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

118

$.fn.donothing = function(){

return this;

};

虽然非常简单,但是还是需要对这个插件进行一些解释。第一,如果要为每一个 jQuery

对象添加一个函数,就必须把该函数指派给 $.fn。第二,这个函数必须要返回一个 this

(jQuery 对象),这样才不至于打断方法链接(Method Chaining)。

可以轻松地在这个示例上构建插件。比如,要编写一个更换背景颜色的插件,以替代

使用 css('background'),可以使用下面的代码:

$.fn.background = function(bg){

return this.css('background', bg);

};

请注意,可以只从 css() 返回值,因为已经返回了 jQuery 对象。因此,方法链接

(Method Chaining)仍然运作良好。

笔者建议在需要重复工作的时候使用 jQuery 插件。比如,如果需要使用 each() 函数

反复执行相同的操作,那么可以使用一个插件来完成。

由于 jQuery 插件相当易于编写,所以有上百种可供选择使用。jQuery 提供的插件可

用于制表、圆角、滑动显示、工具提示、日期选择器,以及我们可以想到的一切效果。有

关插件的完整列表,请参阅参考资料[2]。

为复杂、使用 为广泛的插件要属界面(Interface),它是一种动画插件,用于处理

排序、拖放功能、复杂效果,以及其他有趣和复杂的用户界面(User Interface,UI)。界面

对于 jQuery 来说,就如 Scriptaculous 对于 Prototype 一样。

下面介绍一个比较表格排序的插件 Tablesorter,这个插件是 UI 插件中的一种,目前的

版本是 2.0,提供了方便的表格排序功能,用户可以通过点击表头来对表格进行排序,效果

如图 3-16 所示。

图 3-16 表格排序示例效果(一)

当用户再次点击 FirstName 栏时,就会出现倒序排列效果,如图 3-17 所示。

CHAPTER 第 3 章 Ajax 框架介绍

119

3

图 3-17 表格排序示例效果(二)

下面我们来看看这个效果是如何实现的。

首先,要加载这个插件,到 http://tablesorter.com/docs/网站上下载 新的插件,本

书成书时 新的插件版本是 2.0。需要下载 Full Release zip 包,然后解压缩到本地即可

使用。

然后,在硬盘上建立一个 sort 目录,在 sort 目录下建立一个 css 目录和 javascript 目录,

并将解压缩的文件释放到这里面。

接下来,建立一个 index.html 文件,内容如下:

<head>

<meta http-equiv="Content-Type" content="text/html; charset=GB18030">

<title>Table Sort Sample</title>

<script type="text/javascript" src="./javascript/jquery-1.2.3.js">

</script>

<script type="text/javascript" src="./javascript/jquery.tablesorter.js">

</script>

<script type="text/javascript" src="./javascript/jquery.metadata.js">

</script>

<script type="text/javascript"

src="./javascript/jquery.tablesorter.pager.js"> </script>

<link rel="stylesheet" href="./css/blue/style.css" type="text/css">

<script>

$(document).ready(function(){

//在这里对表格执行排序任务

$("#example").tablesorter({sortList:[[0,0],[2,1]], widgets:

['zebra']});

});

</script>

</head>

<body>

<table id="example" class="tablesorter" border="0" cellpadding="0"

cellspacing="1">

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

120

<thead>

<tr>

<th id="th1" nowrap="nowrap" align="center">Position<br />

Feb 2008</th>

<th nowrap="nowrap" align="center">Programming Language</th>

<th nowrap="nowrap" align="center">Delta in Position</th>

<th nowrap="nowrap" align="center">Ratings<br />

Feb 2008</th>

<th nowrap="nowrap" align="center">Delta <br />

Feb 2007</th>

<th nowrap="nowrap" align="center">Status</th>

</tr>

</thead>

<tbody>

</table>

</body>

</html>

后,打开页面,我们就会看到如图 3-18 所示的效果。

图 3-18 jQuery 插件实现自定义表格排序

在上面的代码中,当 document 加载完毕时,对 example 对象执行排序任务,也就是使

CHAPTER 第 3 章 Ajax 框架介绍

121

3

用代码$("#example").tablesorter 来指定对 example 表格执行排序任务;同时指定了表格初始

化时要排序的列,这是通过 sortList 来指定的,其中[0,0]表示第 1 列第 1 行,[2,0]表示 3 列

第 1 行,并且使用 zebra 指定渲染时的风格。这样,通过简单的一行代码,就完成了所有

的表格排序任务,是不是很简单啊!

$(document).ready(function(){

$("#example").tablesorter({sortList:[[0,0],[2,1]], widgets: ['zebra']});

});

3.4 Sarissa

相对于前面介绍的几个 JavaScript 框架,Sarissa 可能没有那么大的名气,几乎不为

开发人员所知,但是在 JavaScript 框架开发中,这个框架却是鼎鼎大名的。它是

ECMAScript 的一个库,是一个提供了丰富的 XML 操作 API 的跨浏览器的 JavaScript

库,它提供了包括 Document 实例化、XML 加载、XSLT 转换、XPath 查询等各种 XML

相关的操作,可以说在 Ajax 开发中,如果数据表示要使用 XML 的话,Sarissa 是一个

非常好的工具。JSF 中著名的 RichFaces/A4J JSF,其核心的 XML 操作就是通过调用

Sarissa 实现的,著名的内容管理系统 Plone 中也有。

在这一节里,只对 Sarissa 的功能做简单的介绍,如果读者对它有更大的兴趣,可以登

录 http://dev.abiss.gr/sarissa 做更深入的了解。

3.4.1 Sarissa 介绍

Sarissa 是封装了原生 XML 应用程序编程接口(API)的一套跨浏览器的

ECMAScript 链接库,它提供了与 XML 相关的各种功能,比如文件实体化(Document

instantiation)、从 URL 或字符串中加载 XML、XSLT 转换、XPath 查询等,以及提供进

行 Ajax 开发的便利性。

与其他 Ajax 框架相比,Sarissa 为独特的就是以一种独立于浏览器的方式对 XML API

提供了包装支持。Sarissa目前支持Mozilla Firefox系列产品、Internet Explorer 与MSXML 3.0

或更新的版本、Konqueror (KDE 3.3+确定支持)、Safari 及 Opera。到目前为止,Konqueror、

Safari 及 Opera 等浏览器并不提供 XSLT/XPath 脚本语言编程的支持,但 Sarissa 在这些浏

览器上却提供了非常好的支持。

Sarissa 发行遵循 GNU GPL version 2 或更新的版本,或者 GNU LGPL version 2.1 或更

新的版本。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

122

3.4.2 Sarissa 的使用

3.4.2.1 Sarissa 的引入

Sarissa 有两种发行版本:第一种版本就是普通的 JavaScript 文件及相关文档,我们可

以从 http://sourceforge.net/projects/sarissa/网站上得到 新的下载;第二种版本提供了 Maven

的支持,是作为 Maven 的依赖项存在的。这里主要介绍第一种。

对于不使用 Maven 管理的应用,仅需要下载 Sarissa 的 JavaScript 文件包即可。下载并

解压后会发现有下面几个文件,见表 3-9。

表 3-9 Sarissa 文件结构

文 件 说 明

/gr/abiss/js/sarissa/sarissa.js Sarissa 主程序

/gr/abiss/js/sarissa/sarissa_ieemu_xpath.js 在 Mozilla 浏览器上模拟 IE 的 SelectNodes 功能和 SelectSingleNode 功能

/gr/abiss/js/sarissa/sarissa-compressed.js Sarissa 的压缩版本,正式应用中可以使用

/gr/abiss/js/sarissa/sarissa-table-utils.js Sarissa 提供的工具类,可以对 table 进行按列排序

/gr/abiss/js/sarissa/sarissa-table-utils-compressed.js 工具类的压缩版

下面详细说明如何在页面中使用 Sarissa。在页面中引入 Sarissa,只要在页面上添加如

下代码即可。

<script type="text/javascript"src="js/gr/abiss/js/sarissa/sarissa.js"/>

3.4.2.2 XML API

Sarissa 的重要功能在于 XML 的操作,在这一节中我们主要来学习如何使用这些 API

来加快开发速度。

在第 2 章中我们已经知道,可扩展标记语言 (XML) 是 Web 上的数据通用语言,它

使开发人员能够将结构化数据从许多不同的应用程序传递到桌面,进行本地计算和演示。

XML 允许为特定应用程序创建唯一的数据格式,它还是在服务器之间传输结构化数据的

理想格式。但是不同的语言提供 XML 操作的 API 并不相同,如果想在浏览器中进行 XML

操作,就要依靠浏览器提供的对 XML 的支持了。

MSXML(Microsoft XML Core Services) 是 Microsoft 提供的核心 XML 服务组件,

它基于 XML 1.0 规范,允许用户创建基于 XML 的高性能、与其他应用高度互操作的应用。

新版本的 Microsoft 核心 XML 服务提供下面 4 种不同的功能:

基于文档对象模型 (DOM) 的分析器——这是一个标准的应用编程接口(API)库,

允许自由访问和操纵 XML 文档。

SAX(Simple API for XML)分析器——它针对处理大型文档和高吞吐量的情况进

CHAPTER 第 3 章 Ajax 框架介绍

123

3

行了优化。SAX 是基于事件的分析器,它读取文档并将分析事件(例如元素的开始

和结尾)直接报告给应用程序。用户创建的应用程序实现了处理不同事件的处理程

序,这非常类似于处理图形用户界面 (GUI) 中的事件。

XSLT 处理器——读取 XSLT 文件,并将可扩展样式表转换语言 (XSLT) 文件的指

令应用到 XML 文件,以产生某些类型的输出。除了创建 XML 结构之外,XSLT 处

理器还可以在得到的 XSLT 过滤器上执行一定量的优化。另外,从技术角度看,它

更像是一种编译器。

验证分析器——读取文档类型定义 (DTD) 或者 XML 架构,然后检验实际得到

的文档的格式是否正确,以及是否不包含与架构冲突的数据。请注意,仅对架构

而言,验证架构将返回架构本身作为对象,可以在以后的 HTML 列表框中创建选

项时引用这个对象。

所有的这 4 种功能都包含在同一个 MSXML 库软件包中,在过去几年中,XML 经

历了许多反复,所以存在不同版本的 Microsoft XML 分析器,目前的版本有 MSXML

3.0、MSXML 4.0、MSXML 5.0 和 MSXML 6.0。MSXML 3.0 是目前应用 为广泛的版

本,这是一个在 Windows 2000 之后随系统安装的一个 XML 引擎,提供了很好的向后

兼容性和对遗留应用的支持,因此不需要额外安装。MSXML 4.0 在 MSXML 3.0 的基础

上增加了很多特性,在性能上也有不少的提升,从 MSXML 4.0 起由于采用 Side-by-Side

的安装方式,MSXML 4.0 和 MSXML 3.0 可以并存。但是在 MSXML 6.0 推出后,MSXML

4.0 已经在 2006 年 12 月 31 日停止开发,并在 2008 年 12 月 31 日后取消支持。MSXML

5.0 是随着 MS Office 2003 一起发布的,是专门针对 Microsoft Office 产品及相关应用设

计的。MSXML 6.0是随着 SQL Server 2005出现的, 新的单独安装版随着 Windows Vista

一起发布,6.0 版在安全、性能、可靠性,以及在 XSD 1.0、XML 1.0 遵循方面都有很大

的进步,是目前 新和 好的一个版本。如果 MSXML 没有安装,那么 MSXML 3.0 则

是一个很好的顶替。

与 Mozilla 的其他方面一样,Mozilla 提供的 XML DOM 版本要比 IE 的更加标准。

Mozilla 中的 XML DOM 实际上是它的 JavaScript 实现,也就是说,它不仅与浏览器一起

衍化,同时它能可靠地在 Mozilla 支持的所有平台上使用。因此,Mozilla 的 XML DOM

支持跨越了平台的界限。另外,Mozilla 的 XML DOM 实现了支持 DOM Level 2 的功能,

而微软的 IE 浏览器仅支持 DOM Level 1。

在了解了上面这些知识之后,我们可以看出,由于不同浏览器的差异,给使用 JavaScript

操作 XML 带来了很大的不便。幸好,Sarissa 完美地解决了这个问题。

下面我们来逐一学习 Sarissa 提供的功能。

1. 获取与创建 DOM

Sarissa 提供了一个 Sarissa 对象,这个 JavaScript 对象封装了很多常用的 XML 操作,

用起来也非常简单。例如,使用 Sarissa 获得一个 DOM Document 对象非常简单,只需要调

用 Sarissa 提供的一个工厂方法即可。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

124

// 得到一个特定浏览器的 DOM Document 对象

var oDomDoc = Sarissa.getDomDocument();

除此之外,我们还可以创建更加一般的 DOM 节点,只要给上面的方法添加额外的参

数就可以了。

var oDomDoc = Sarissa.getDomDocument("http://abc.org/ns/uri","test");

这样,我们就会在内存中得到如下的一个节点对象:

<foo xmlns="http://foo.org/ns/uri"></foo>

除了这个方法之外,Sarissa 对象还提供了相关的 DOM 方法,见表 3-10。

表 3-10 Sarissa DOM 方法

方 法 说 明

clearChildNodes(oNode) 删除节点下的所有子节点

copyChildNodes(nodeFrom, nodeTo, bPreserveExisting) 把一个节点的所有子节点复制到另一个节点下,

bPreserveExisting 表示是否保留目的节点的原有子节点

escape(sXml) 转义给定字符串

formToQueryString(oForm) 把给定的 HTML Form 对象中的数值转换成查询字符串

getParseErrorText(oDoc) 将转换错误转换成适合人们阅读的描述,用于调试。当

表现时, 好在外面添加<pre>元素

getText(oNode, deep) 将给定节点下的所有字符串节点值连接后返回。deep 指

示是否递归执行子节点

moveChildNodes(nodeFrom, nodeTo, bPreserveExisting) 将一个节点下的所有子节点移动到另一个元素下。

bPreserveExisting 同 copyChildNodes 方法

stripTags(s) 从给定的字符串中去掉标签

unescape(sXml) 取消转义

updateContentFromForm(oForm, oTargetElement, xsltproc,

callback) 根据指定 form 发起异步调用,更新目标元素内容

updateContentFromNode(oNode, oTargetElement, xsltproc) 根据指定节点发起异步调用,更新目标元素内容

updateContentFromURI(sFromUrl, oTargetElement, xsltproc,

callback, skipCache) 根据指定 URI 发起异步调用,更新节点内容

xmlize(anyObject, objectName) 将一个对象(不能为DOM对象)序列化为一个XML字符串

Sarissa 除了提供一个公共的 Sarissa 对象外,还提供了一个额外的 XMLSerializer 类,

我们可以利用这个类来序列化 DOM 对象。XMLSerializer 类只有一个方法:

serializeToString(oNode)

用来将 DOM 节点序列化为字符串。

下面的例子使用这个方法将一个 XML 对象序列化为一个 DOM 节点。

CHAPTER 第 3 章 Ajax 框架介绍

125

3

var xmlString = new XMLSerializer().serializeToString(someXmlDomNode);

那么如何将一个字符串如"<root>my xml!</root>",构造成一个 DOM 节点呢?这就需

要 Sarissa 提供的另外一个类 DOMParser 了,DOMParser 提供了一个 parseFromString 方法,

可以方便地把一个类似上面的字符串转换为 DOM 节点。

//声明 XML字符串

var oDomDoc = Sarissa.getDomDocument();

var xmlString = "<root>my xml!</root>";

//解析

oDomDoc = (new DOMParser()).parseFromString(xmlString, "text/xml");

alert(new XMLSerializer().serializeToString(oDomDoc));

2. XMLHttpRequest

在 Sarissa 中创建 XMLHttpRequest 是非常容易的,只需要下面一行代码:

var xmlhttp = new XMLHttpRequest();

下面的代码完整地展示了如何向服务器发送 Ajax 请求,并且将结果打印出来。

var xmlhttp = new XMLHttpRequest();

xmlhttp.open("GET", "http://foo.org/someDocument.xml", false);

// 如果需要设置头部信息,则使用 setRequestHeader方法

xmlhttp.send('');

alert(new XMLSerializer().serializeToString(xmlhttp.responseXML));

在实际的情况中,一般会使用 form 接收用户的输入,在用户单击“确定”按钮后发起

一次 Ajax 请求,在发送完 Ajax 请求后,一般需要把 Server 端返回的数据显示在页面上。

如果是我们自己手写 Ajax,还需要将 form 中的数据转换为查询参数(也就是以“?

paramName=value&…”的方式),Sarissa 已经为这种方式做好了准备:

<div id="targetId"> this content will be updated</div>

<form action="/my/form/handler" method="post"

onbeforesubmit="return Sarissa.updateContentFromForm(this,

document.getElementById('targetId'));">

//表单组件

</form>

如果 JavaScript 支持的话,那么上面代码中的 form 将不会被提交,而是通过 Sarissa

提供的 updateContentFromForm 方法,将 form 提交转换为一个 Ajax 请求,将 form 的数

据转换为 Ajax 请求参数,发送到 Web Server 端,然后再将 Server 端的数据显示在指定的

Node 上。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

126

3.5 本章小结

本章详细介绍了目前广为使用的多个 Ajax 框架:Prototype、script.aculo.us、jQuery 和

Sarissa,了解这几个框架后,就可以在 Web 界面应用中开发各种特殊的 Ajax 效果。读者对

于这几个框架不一定需要全部熟练,只需要熟练掌握其中的一种即可。

参考资料

[1] jQuery 选择器列表及示例:http://docs.jquery.com/selectors

[2] jQuery 插件库:http://plugins.jquery.com/

第Ⅱ部分 JSF 应用开发

CHAPTER

4

4.1 什么是 JSF

4.1.1 JSF 简介

JSF(Java Server Faces)是由 Java 社区进程(Java Community Process,JCP)制定的

一个 Web 应用框架标准,也是首个 Web 应用程序开发的标准框架。它的目的是结束当

前 Web 应用开发中框架众多混乱的局面,简化 Web 应用程序的开发。JSF 提供了一个

基于组件的架构(框架)用于管理和构建 Web 应用程序接口,它也提供了丰富的功能

集包含事件处理机制(Event Handling)、页面导航(Page Navigation)、验证客户端数据

(Input Validation)和数据转换(Conversion)等;它同样也提供了基于组件的插件化体

系来开发和展现用户界面组件,开发者可随时开发他们的自定义 UI 组件并很容易地将

其应用到框架之中。JSF 框架 主要的特色之一就是它并不仅仅针对于一种类型的客户

端,这意味着除了用于桌面的 HTML 浏览器客户端外,它甚至还可能支持用于移动电

话的 WML 浏览器客户端。另外,因为 JSF 是由 JCP 制定的 Java 标准,所以得到各

大开发工具供应商的支持,为 Java Server Faces 提供了很多易于使用的、高效的可视

化开发环境。

在详细了解 JSF 之前,让我们先来回顾一下 Java 中 Web 开发的历史。

第 4 章

JSF 介绍

CHAPTER 第 4 章 JSF 介绍

129

4

在 Java 社区里,Web 应用的开发一直存在开发难的问题,从开始的 简单的 CGI 程序,

到 JSP/Servlet。虽然说 JSP/Servlet 的出现大大减轻了开发的难度,但稍微复杂点的 Web 应

用,就会产生大量重复代码,后来为了解决这些问题,先后出现了诸如 Struts、Spring 等优

秀的框架,并得到了广泛推广。

在 JSF 出现之前,Servlet 和 JSP 是构成 Web 应用开发的核心组件中 主要的部分。我

们来看看在一个传统的 Web 应用程序中所发生的交互作用,它的实现仅仅是由遵循 MVC-2

架构模型的 Servlet 和 JSP 组件来构建的。客户端(通常是一个 HTML 浏览器)发送请求

(Request)到服务器,Web 服务器收到请求,并封装 Request,然后通过来自客户端的各

种参数值组装这个 Request 对象并将其发送给 Servlet。Servlet 担当控制器(Controller),它

解析接收到的请求(Request),然后与要执行各种各样的业务逻辑的模型(Java Beans)相

结合, 后选择一个要展现给用户的视图(View)。

MVC 架构示意图如图 4-1 所示。

图 4-1 MVC 架构示意图

从本质上说,Model 2 体系结构是一种用于 Web 应用程序的简化版 MVC。在 Model

2 体系结构中,控制器由 Servlet 表示,显示由 JSP 页面负责。Struts (Apache 推出的一

个 Web 开发框架)是一个简化的 Model 2 实现,其中用 Actions 替代了 Servlet。在 Struts

中,应用程序的控制器逻辑与它的数据(由 ActionForms 表示)分隔开。对 Struts 的主要

批评意见是它的处理具有很强的过程性,不够面向对象(它被戏称为 “COBOL for the

Web”)。WebWork 和 Spring MVC 是另外两种 Model 2 体系结构实现,它们降低了过程性,

但是没有像 Struts 那样得到广泛应用。与 JSP 技术一样,大多数 Model 2 框架允许非常

容易地在 HTML 布局和格式化代码中混合 GUI 定制标记,这使代码具备组件那样的松散

性,但它们不是有状态的。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

130

大多数 Model 2 框架的真正问题是其内置的事件模型太过简单,没有提供有状态的

GUI 组件,把太多工作留给了开发人员。为了简便地创建大多数用户期望的交互方式,需要

一个更丰富的组件和事件模型。同时,这些开发框架对 Web 中出现的验证、转换、可重用组

件等问题都没有恰当地处理。另外,与微软 Visual Studio.NET 程序开发有可视化工具的支持

相比,人们在开发基于 Java 的 Web 用户界面时缺乏优秀的可视化集成开发环境(IDE)的支

持。例如,需要手工书写大量的标签,难以实现拖拽式的 Web 开发,因此开发难度比较大,

效率较低,重用性差。

在这些基础上,Java 社区进程(JCP),包括 Sun、Oracle、Borland、BEA、IBM

及一群业内知名的 Java 和 Web 专家,开始制作 JSF 规范请求, 初的 Java 规范请求

(JSR 127)制作开始于 2001 年中期,并于 2004 年 3 月正式公开发布了 JSF 规范和参

考实现。

前面提及,页面状态保存、事件处理不灵活,缺少可视化集成开发环境等,都是 Java Web

开发中的硬伤,Java Server Faces(JSF)技术正是为了解决这些问题应运而生的。就像 Swing

和 AWT 一样,JSF 提供了一个基于组件的体系结构用于开发可重用的 UI 组件,使得我们

不需要去重复一些往往会发生在 Model 2 体系结构的 View 部分的复杂工作。JSF GUI 组件

包括标准的 HTML 表单控件(如按钮)、布局组件,以及更复杂的组件,如数据表。此外,

第三方还可以扩展规范中定义的基本类,来开发额外的 GUI 组件,使得 JSF 开发不断有更

新、更好的组件出现。

这个框架不仅用于开发自定义的 UI 组件,同时也支持多种高级特性,如之前提到的事

件处理机制、验证来自客户端的用户输入数据和页面导航机制等。

JSF 引人注目的特性之一是,它是一个经过 JCP 指定的规范,其标记语言、协议、

客户端设备无关,这样,利用 JSF 提供的可重用、可扩展、基于组件的用户界面框架,在

快速开发工具 RAD 的支持下就可以实现可视化开发。现在 JSF 技术已经得到了许多大厂商

的支持,如 Sun 公司的 JSF Web UI、IBM 公司的 JSF Extension 及 Oracle 的 ADF Faces 等,

许多开源项目(典型的 Apache MyFaces)也提供对 JSF 技术的支持。同时 Oracle、Sun、Borland

和 IBM 等公司都为 JSF 提供了开发环境。

基于此,在众多的 J2EE 表现层框架技术中,JSF 表现出其旺盛的生命力。尽管 JSF

技术还有不少问题,但随着 JSF 技术的不断成熟和版本更新,其必将获得越来越多的

应用。

4.1.2 JSF 的体系结构

JSF 的休系结构图如图 4-2 所示。

CHAPTER 第 4 章 JSF 介绍

131

4

图 4-2 JSF 的体系结构图

在 JSF 应用中, 重要的几个组件是:

UI 组件:有状态的对象,由服务器维护,提供与用户的特定交互功能。UI 组件是

具有属性、方法和事件的 Java Bean,它们被组织进一个视图,而该视图通常显示

为一个页面的组件树。

托管 Bean(Managed Bean);

验证器;

转换器;

事件和监听器;

页面导航;

呈现器。

以上所述的所有组件都可以在单独的 API 包中找到。例如, UI 组件在

javax.faces.component 包中,Validation API 在 javax.faces.validator 包中等。

4.1.2.1 UI 组件

如果 Java Swing 的 UI 组件的展现是用于桌面 Java 应用程序,那么 JSF UI 组件则是用

于 Web 应用程序。像 Swing 组件一样,它们构建在 Java Bean 规范和标准之上。这意味着

JSF 组件具有属性、方法和事件,如同传统的 Java Bean 中所具有的一样。JSF UI 组件的特

点之一就是它们可以管理组件的状态,更特别的是它们驻留在服务器端而不是客户端,这

是针对 Web 应用的约束做出的一些特殊设计。很多内置的 UI 组件与 JSF API 绑定,如常

用的 Label、Text-Field、Form、Check-Box 等。JSF 同时也提供了一套框架用于创建自定义

的 UI 组件。

值得注意的是,JSF UI 组件仅表达一个组件的属性、行为和事件,而并非实际的显示。

例如,对于一个文本框(Text-Field),它的属性可以是文本框的值、可输入的 大字符数等。

修改文本框的现有值或设置它的 大字符数将产生行为;改变文本框的值可能使得文本框

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

132

触发某种事件的发生。这种形式即显示在客户端的文本框本身与 UI 数据呈现是相分离的。

这意味着有一个单独的组件,被称做渲染器(Renderer),由它来负责 UI 组件在不同客户

端层面的显示,可能是运行在 PC 上的 HTML 浏览器,也可能是运行在移动电话内部的

WML(Wireless Markup Language,无线标记语言)浏览器。每个 JSF 组件都由一个唯一标

识符来标识。

4.1.2.2 托管 Bean(Managed Bean)

托管 Bean 其实是一些遵循 Java Bean 规范的标准的 Java 类。一般地,托管 Bean 被用

来表达用户输入,它们甚至可能担当监听器(Listener),也可以处理适当的 Action。

实际上,托管 Bean 也是一个真正的 IoC(Inversion of Control,控制反转)容器。当然,

谈到 IoC,很多人肯定会想到目前比较流行的 Spring 框架,但还有另外两个实现了 IoC 的

框架:HiveMind 和 PicoContainer,这是目前的三大 IoC。关于 IoC 就不作详细介绍了,但

在这里列出它的几个核心方面。

控制对象实例化并通过设置(setter)注入托管对象;

依赖处理;

支持对象生命周期管理;

对于配置管理的支持。

接下来,根据以上几点来解释 JSF 托管 Bean 同样也是一个“IoC”容器。

4.1.2.3 实例化控制

JSF 的托管 Bean 工具提供了很好的解决方案来创建和实例化托管 Bean。

在 JSF 之前,要在页面中处理 Java Bean,一般使用 JSP 的<jsp:useBean>标签,它允许

我们在一个指定的范围中实例化 Java Bean。JSF 对 JSP 的这个特性做了很大的改进。使用

JSF,可以将任何一个含有无参构造函数的 Java 类(Plain Old Java Object,POJO),通过将

其引用到一个 XML 配置文件 faces-config.xml 中注册为一个托管 Bean。例如,一个 Java

类,名为“User”并有 name、phone 和 email 三个字段。

public class User {

String name;

String phone;

String email;

public User() {

}

public void setName(String name) {

this.name = name;

}

public String getName() {

CHAPTER 第 4 章 JSF 介绍

133

4

return name;

}

public void setPhone(String phone) {

this.phone = phone;

}

public String getPhone() {

return phone;

}

public void setEmail(String email) {

this.email = email;

}

public String getEmail() {

return email;

}

}

下面将这个 Bean 注册到 faces-config.xml 中:

<managed-bean>

<managed-bean-name>userBean</managed-bean-name>

<managed-bean-class>com.jsf.User</managed-bean-class>

<managed-bean-scope>request</managed-bean-scope>

</managed-bean>

现在可以随时在 JSF 应用中使用这个叫做“userBean”的 Bean,这是通过 JSF EL 表达

式来完成的。例如,将一个 JSF UI 组件绑定到 userBean 的一个属性上,可以使用表达式“#”

作为一个 JSF UI 组件的参数值。现在,将 userBean 的 name 属性绑定到一个 JSF inputText UI

组件上:

<h:input value=“#{userBean.name}”/>

当组件的值改变时,userBean.name 属性也跟着改变;如果 userBean.name 先改变也是

一样。两者将自动保持同步,这是 JSF 的关键特性。

对于 IoC 必不可少的一点是对处理生命周期的支持。实际上,JSF 的托管 Bean 工具对

处理生命周期的支持非常好,因为它为 HTTP Request 定义一个明确的生命周期范围,而在

其他 IoC 容器中,对于这一点的处理则更一般。与在 JSP useBean 标签中指定的范围参数类

似,JSF 有以下几种类型的生命周期范围:

none:这种类型的托管 Bean 不会被存储在任何地方,只有当需要它的时候才被创建。

request:这种类型的 Bean 只会将它的值存储在一个单独请求的整段时期内,对于在

同一对象上的并发请求,会实例化一个新的版本。

session:这种类型的 Bean 会被存储在 session 中,这意味着这个 Bean 的属性在多个

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

134

请求期间会一直存在。对象存储在 session 中无疑是线程安全的,并且如果不被应用

程序显式清除,当 session 过期后就会终止。

application:一个对象被创建为 application 范围,将在容器的整个生命周期内一直存

在。

在 JSF 中,page 范围是不可用的。这样做的目的是为了使托管 Bean 不依赖于指定的

页面。

4.1.3 为什么要使用 JSF

4.1.3.1 UI 组件(UI Component)

UI 组件(UI Component)一直是桌面程序的专利,在 Web 程序中,虽然 HTML 定义

了基本的 UI 标签,但要使这些 UI 标签像 UI 组件那样工作,还需要很多代码片段来处理

数据及其表现形式,而且有效地组织这些代码片段使其协调一致也是一件烦琐的工作。JSF

的 UI 组件是真正意义上的 UI 组件,能极大地简化程序员的工作。例如,在页面上放置一

个文本输入框,这个输入框立即具备了数据填充、界面更新、事件侦听、动作触发、有效

性检查和类型转换的功能。重要的是,程序员只需根据业务逻辑编写核心业务代码,JSF

会保证代码在合适的时候被执行,完全不用考虑代码与代码之间该如何来配合。

更为重要的是,JSF 的组件标准是开放的,允许开发人员自定义组件,并且很多 IT 公

司都不断推出新的组件,因而 JSF 的组件种类十分丰富,图 4-3 至图 4-6 显示了一些 JSF

组件。

图 4-3 国内厂商金蝶提供的 JSF 组件

CHAPTER 第 4 章 JSF 介绍

135

4

图 4-4 MyFaces 提供的 JSF 组件

图 4-5 RichFaces 提供的 JSF 组件

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

136

图 4-6 IBM 提供的 JSF 扩展组件

充分利用这些组件,可以简化 Web 用户界面的开发,提高 Web 开发的质量和速度。

4.1.3.2 完善的事件处理机制

事件是面向对象方法的重要组成部分,对象之间通过事件进行沟通和交流,使得一个

或多个对象能够对另一个对象的行为作出响应,共同合作去完成一项业务逻辑。通常,编

写 Web 程序时,程序员要为对象之间的沟通设计机制,编写代码。虽然沟通的内容属于业

务逻辑,但沟通的机制显然与业务没有太大关系,程序员因此为业务逻辑之外的功能浪费

了时间。

JSF 改变了这种状况。JSF 的事件和侦听模式与大家熟悉的 Java Bean 的事件模式类

似,有 Java 基础的程序员并不需要学习任何新的东西。JSF 的 UI 组件可以产生事件,

例如,当页面上一个文本输入框的内容被修改时,会发出一个“值改变事件”。另一个对

象如果对“值改变事件”感兴趣,只需注册为该对象的侦听者,并编写处理例程,即可命

令 JSF 在事件发生时自动调用处理例程。JSF 做了所有该做的事,留给程序员的只有业

务逻辑代码的编写。

CHAPTER 第 4 章 JSF 介绍

137

4

4.1.3.3 用户界面到业务逻辑的直接映射

举个例子:表单提交是 Web 编程 常见的任务,也是 复杂的任务之一。当用户在网

页上单击“确定”按钮时,浏览器将生成一个 HTTP 请求,发往服务器端的某个 Servlet,

执行该 Servlet 的 service 方法。在 service 方法中,HTTP 请求需要经历解码、类型转换、

有效性验证、状态保存、数据更新等环节,处理这些环节的所有细节,对程序员来说是沉

重的负担。

在 JSF 下,这些工作的很大一部分都由框架承担了,在程序员看来,这个过程是透明

的,用户界面端的 HTTP 请求可以直接映射到后端的一个事件处理例程上,JSF 起到了承

前启后的作用。

4.1.3.4 多种角色的合理分工

在 JSP 中,程序员和网页设计人员的工作有时候是互相交织、无法区分的。这是因为

JSP 页面中掺入了网页设计人员所不熟悉的一些 JSP 标签,甚至是晦涩的 Java 代码。要求

网页设计人员理解这些标签和代码是不现实的,不符合分工合作的原则。在 JSF 中,框架

为网页设计人员提供了一套标准的 UI 组件,在工具的支持下,可以通过拖放简单地添加到

网页上,然后设置某些显示属性来满足视觉要求。

网页设计人员不需要知道 UI 组件背后的复杂代码,那是程序员的事;而程序员

也不需要再处理任何与视觉相关的细节,程序员所做的只是给 UI 组件绑定类的属性

或方法。虽然程序员和网页设计人员需要修改同一份文件,但他们各司其职,各得其

所,互不干扰。程序员和网页设计人员工作的明确划分,是 JSF 在易用性方面迈出的

一大步。

4.1.3.5 请求处理生命周期的多阶段划分

虽然都是建立在 Servlet 基础之上,但 JSF 的生命周期要比 JSP 复杂得多。JSP 的

生命周期非常简单,页面被执行时,HTML 标记立即被生成了,生命周期随即结束。

而一个完整的 JSF 请求-处理生命周期被精心规划为 6 个阶段,典型的 JSF 请求需要经

历所有阶段,某些特殊的请求也可以跳过一些阶段。阶段的细分,显然引入了更多的

处理,但 JSF 框架会管理这一切,所以,程序员在获得更多控制能力的同时,工作量

并没有增加。

4.1.3.6 卓越的可视化工具支持

JSF 带来了 Web 编程的巨大变革,变革的强烈程度超出了很多工具厂商的预料,以至

于现在可供 JSF 使用的工具非常缺乏。缺乏工具支持的 JSF 只会令人敬而远之,因此,JSF

在设计之初就为工具厂商预留了用武之地。目前,很多厂商为 JSF 开发提供了方便的可视

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

138

化工具支持,如 Sun 的 Java Studio Creator,Oracle 的 JDeveloper,IBM 的 WebSphere

Integration Developer,BEA Workshop Studio JSF Tooling 开发环境,都为 JSF 开发提供了便

利的工具支持。

在这些可视化工具的支持下,JSF 应用的开发正变得越来越容易,真正达到了拖拽式

开发的目标。与桌面应用程序的开发过程相差无几,如果你有 ASP.NET 开发或者桌面应用

程序开发的经历,就会对这种开发模式深有体会。

4.1.3.7 全面的用户自定义支持

前面提到,JSF 将极大地简化 Web 程序的开发,作为一个相对复杂的框架,JSF 是如

何做到这点的呢?原来 JSF 为程序员提供了很多默认的组件和类,通常情况下,JSF 的这

些默认组件和类足以满足 Web 开发的需要了。但是,考虑到在某些应用场合,框架的默认行

为也许不符合业务的要求,JSF 特别允许程序员编写自己的组件和类,来满足客户的特殊需

求。例如,程序员可以编写自己的 UI 组件,甚至可以创建自己的 EL 解释器,来支持非标准

的 EL(表达式语言)。

4.1.3.8 Web 开发的官方标准之一

JSF 的 1.0 版发布于 2004 年 2 月,当时是作为一项独立的 Web 技术推出的。经过

1.1 版到现在 新的 2.0 版,短短的两年多时间,JSF 终于在 2006 年成为 Java EE 5 的组

成部分,上升为 Web 开发的官方标准之一。Java EE 5 重要的使命就是简化 Java 的开

发,而 JSF 无疑为这一使命立下了汗马功劳。在 Web 框架层出不穷甚至有些泛滥成灾

的今天,Sun 以 JSF 来树立标准,对 Java 的发展是有益的。Sun 在 Java 领域的领袖地位

不容动摇,对于 Java 程序员来说,始终追随业界领袖的步伐,也许是避免技术落伍的

好方法。

4.2 使用条件

(1)支持的 JDK 版本

官方文档中要求 JDK 的版本为 5.0 或者以上,但经笔者测试发现 JSF 完全可以运行在

JDK 1.4.2 平台之上,但需要额外的 el-api.jar 和 el-ri.jar 包。

(2)支持 JSF 的实现

目前比较流行的 JSF 实现包括:

Sun JSF RI 1.1~1.2

My Faces 1.1.1 ~1.2

CHAPTER 第 4 章 JSF 介绍

139

4

Facelets JSF 1.1.1 ~ 1.2

Seam 1.2 ~ 2.0

IBM JSF1.1 ~ 1.2

(3)支持的服务器

大多数的服务器都支持,包括:

Apache tomcat 4.1 ~ 6.0

IBM Websphere 5.1 ~ 6.1

BEA Weblogic 8.1 ~ 9.0

Oracle AS/OC4J 10.1.3

Resion 3.0

Jetty 5.1.X

Sun Application Server 8 (J2EE 1.4)

Glassfish (J2EE 5)

JBoss 3.2 ~ 4.2.X

Sybase EA Server 6.0.1

(4)支持的浏览器

IE 6.0 ~ 7.0

Firefox 1.5 ~ 2.0

Opera 8.5 ~ 9.0

Netscape 7.0

Safari 2.0

4.3 配置 JSF

下面,我们以 Sun 的 JSF 实现为例,说明如何使用 JSF 进行 Web 开发。

4.3.1 下载

首先,到 http://java.sun.com/javaee/javaserverfaces/download.html 下载 新的 JSF,下载

时选择单独的 JAR 文件实现,本书成书时 新的 JSF 版本是 1.2.4。下载完成后,解压到本

地硬盘,目录结构如图 4-7 所示。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

140

图 4-7 JSF 目录结构

其中 lib 目录下存放的是 JSF 的实现,jsf-api.jar 和 jsf-impl.jar 就是所需要的 JSF 实

现,在建立新的 Web 项目时需要将这两个文件复制到对应的 lib 目录下,JSF 应用共需

要如下几个文件:

jsf-impl.jar

jsf-api.jar

commons-digester.jar

commons-collections.jar

commons-beanutils.jar

jstl.jar

standard.jar

4.3.2 安装配置

在 Web 项目中配置 JSF 相当简单,请遵循下列步骤操作。

将 jsf-api.jar 和 jsf-impl.jar 拷贝到 WEB-INF/lib 下。

把下列代码拷贝到 web.xml 中。

<?xml version="1.0" encoding="UTF-8"?>

<web-app id="WebApp_ID" version="2.4"

xmlns="http://java.sun.com/xml/ns/j2ee"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.

sun.com/xml/ns/j2ee/web-app_2_4.xsd">

<display-name>jsf demo</display-name>

<servlet>

<servlet-name>Faces Servlet</servlet-name>

CHAPTER 第 4 章 JSF 介绍

141

4

<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>

<load-on-startup>-1</load-on-startup>

</servlet>

<servlet-mapping>

<servlet-name>Faces Servlet</servlet-name>

<url-pattern>/faces/*</url-pattern>

</servlet-mapping>

<servlet-mapping>

<servlet-name>Faces Servlet</servlet-name>

<url-pattern>*.faces</url-pattern>

</servlet-mapping>

</web-app>

在使用 JSF 的页面中引入以下 taglib。

<%@taglib uri="http://java.sun.com/jsf/core" prefix="f"%>

<%@taglib uri="http://java.sun.com/jsf/html" prefix="h"%>

大功告成,现在可以开发 JSF 应用了。下面我们来做一个 JSF 的 HelloWorld 应

用。

4.3.3 HelloWorld

本节中,将带领大家做一个 HelloWorld 的小例子。这个例子的目标效果是,当用户在

文本框内输入文本时,页面输出对应的文本,前面加上“Hello”字符串;如果用户没有输

入任何字符串,页面将输出“Hello,World”。

笔者所使用的开发环境是 Eclipse 3.4.0、Apache Tomcat 6.0.14、Sun JSF RI 1.2。

在 Eclipse 中创建动态 Web 项目。

在 Eclipse 中创建 Web 项目的详细步骤,略。

按照上一节中所述配置 JSF。

完整的 web.xml 内容如下所示:

<?xml version="1.0" encoding="UTF-8"?>

<web-app id="WebApp_ID" version="2.4"

xmlns="http://java.sun.com/xml/ns/j2ee"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.

sun.com/xml/ns/j2ee/web-app_2_4.xsd">

<display-name>jsf demo</display-name>

<servlet>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

142

<servlet-name>Faces Servlet</servlet-name>

<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>

<load-on-startup>-1</load-on-startup>

</servlet>

<servlet-mapping>

<servlet-name>Faces Servlet</servlet-name>

<url-pattern>/faces/*</url-pattern>

</servlet-mapping>

<servlet-mapping>

<servlet-name>Faces Servlet</servlet-name>

<url-pattern>*.faces</url-pattern>

</servlet-mapping>

</web-app>

创建一个 HelloBean 类,这是一个 POJO(Plain Old Java Object),有两个属性

name 和 message,其中 name 用来存储用户输入的名字,message 用于显示向用户展示

的信息。

package org.bluelight.ch04;

public class HelloBean {

private String name;

private String message;

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getMessage() {

return message;

}

public void setMessage(String message) {

this.message = message;

}

}

创建 JSF 页面。JSF 页面和 JSP 页面大致是一样的,除了页面上多了几个 JSF 的标

签(tag)。

在 WebContent 文件夹下建立一个名为 HelloWorld.jsp 的文件。完成上面 4 步以后的工

程结构如图 4-8 所示。

CHAPTER 第 4 章 JSF 介绍

143

4

图 4-8 HelloWorld 工程结构

HelloWorld.jsp 文件的内容如下:

<%@ page language="java" contentType="text/html; charset=GB18030"

pageEncoding="GB18030"%>

<%@taglib uri="http://java.sun.com/jsf/html" prefix="h"%>

<%@taglib uri="http://java.sun.com/jsf/core" prefix="f"%>

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=GB18030">

<title>Hello JSF</title>

</head>

<f:view>

<body>

<h:form id="form1" styleClass="form">

<h:panelGrid columns="2">

<h:outputLabel value="请输入你的名字" id="label1" styleClass=

"outputLabel" for="input1">

</h:outputLabel>

<h:inputText id="input1" styleClass="inputText">

</h:inputText>

<h:commandButton type="submit" value="提交"

id="button1"styleClass="commandButton"/><h:commandButton type="reset"

value="重置" id="button2" styleClass="commandButton"/>

</h:panelGrid>

①JSP 标签库

②包围所有 JSF 标签的标签

③HtmlForm 组件

④HtmlPanel 组件

⑤Label 组件

⑦commandButton 组件

⑥input 组件

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

144

</h:form>

</body>

</f:view>

在这个页面中:

① 首先导入核心 JSF(Java Server Faces)标签库,该库提供诸如校验和事件处理之类

的基本任务的定制标签。接着,导入了基本 HTML 标签库,它提供诸如文本框、输出标签

和表单之类的 UI 组件。(前缀“f”和“h”是建议的)

② <f:view> 标签必须将所有其他 Faces 相关的标签扩起来(包括来自于核心标签和

基本 HTML 标签库中的标签)。

③ <h:form> 标签表示一个 HtmlForm 组件,它是其他组件的容器,用于将信息提

交给服务器。在一个页面内可以有多个 HtmlForm,但输入组件必须嵌入<h:form> 标签

中。

④ HtmlPanel 组件用<h:panelGrid> 标签来表示。HtmlPanelGrid 表现为其他组件的一

个可配置容器,将显示为一个 HTML 表格。

⑤ <h:outputLabel>标签标识一个 HtmlLabel 组件,在页面上显示为一个<label>标签,

用于显示输入框的信息。这个组件有一个 for 属性,指向与它关联的<h:inputText>组件,这

个属性是必需的。

⑥ <h:inputText>标签用于创建一个 HtmlInputText 组件,该组件接收用户的文本输入。

inputText 有一个 value 属性,格式为“#{backbean.property}”,这是一个用 JSF 表达式语言

(EL)写成的表达式。JSF EL 是 JSF 引入的基于 JSP 2.0 的表达式语言,用“#{}”表示,在

{}之间可以引用 JSF 中的 ManagedBean 及表达式。在 inputText 组件中,引用了 backBean 中

的属性,这样,组件的 value 属性和 backBean 的 property 属性是同步的,如果一个变更了,

另一个也要修改(除非 HtmlInputText 组件中的文本无效)。

输入组件有一个 required 属性,它决定该字段是否必须有一个值。这样,如果 required

属性被设置为 true,则组件将只能接收非空输入;如果用户输入了一个空值,页面将重新

显示。

JSF 也支持校验器,它负责确保用户的输入是可接受的值。每个输入控件都可以和一

个或者多个校验器相关联。

⑦ <h:commandButton> 指示一个 HtmlCommandButton 组件,它显示为一个 HTML

表单按钮。HtmlCommandButton 在被用户点击后会将活动事件发送到服务器中的应用上。

这个标签有一个 type 属性,表示这个标签的类型,可以有 summit、reset 和 plain 三种,其

中 summit 表示在页面上生成“提交”按钮,reset 表示在页面上生成“重置”按钮,plain

表示生成普通的按钮,即既不提交,也不重置 form 中的值。

如图 4-9 所示为这个页面在 IDE 中页面的设计视图。

CHAPTER 第 4 章 JSF 介绍

145

4

图 4-9 HelloWorld 页面的设计视图

使用了<f:view>标签,这个标签的作用是标识页面为 JSF 页面。当 JSF 的前端控制器

FacesContext 解析页面时,如果遇到这个标签,就会开始 JSF 页面的处理过程;如果没有

这个标签,则 JSF 不会处理这个页面,页面就像普通的 JSP 页面一样。紧跟着这个标签之

后的是<h:form>标签,这个标签的作用是在页面上添加一个 form 标签,action 属性指示当

form 提交时应该提交给本页处理。然后是一个<h:panelGrid>标签,这个标签类似于

Swing/AWT 中容器的概念,用于存放其他的 JSF 组件,columns=2 表示每行 2 个组件。以

后的几个组件和桌面开发中的组件非常类似,比如使用<h:label>来显示文字,使用

<h:inputText>表示输入框等。

在这一步里,我们完成了一个基本的页面,相当于桌面程序中 GUI 的绘制,这时的页

面仅有一个输入框和两个按钮,当用户输入姓名后单击“提交”按钮,系统就会转向另外

一个页面显示一些消息。下面是另一个 JSF 页面 Result.jsp 代码。

<%@ page language="java" contentType="text/html; charset=GB18030"

pageEncoding="GB18030"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://

www.w3.org/TR/html4/loose.dtd">

<%@taglib uri="http://java.sun.com/jsf/html" prefix="h"%>

<%@taglib uri="http://java.sun.com/jsf/core" prefix="f"%>

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=GB18030">

<title>Say Hello</title>

<link rel="stylesheet" type="text/css" href="theme/stylesheet.css"

title="Style">

</head>

<f:view>

<body>

<h:outputText id="text1" styleClass="outputText" value="">

</h:outputText>

</body>

</f:view>

</html>

这个页面更为简单,只有一个输出框,显示用户的欢迎信息。现在完成了两个页面,

相当于桌面应用程序中完成了 GUI 的绘制工作。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

146

完成这两个页面后,下面来设置页面的跳转。我们的目标是用户在单击 HelloWorld 页

面的“提交”按钮时,页面跳转到 Result.jsp 页面上,因此需要设置页面之间的跳转。在 JSF

中设置页面跳转非常容易,它是通过配置 faces-config.xml 实现的,在 faces-config.xml 中添

加如下代码:

<navigation-rule>

<from-view-id>/HelloWorld.jsp</from-view-id>

<navigation-case>

<from-outcome>sayHello</from-outcome>

<to-view-id>/Result.jsp</to-view-id>

</navigation-case>

</navigation-rule>

navigation-rule 表示导航规则,也就是在 JSF 中定义页面应该如何跳转。<from-view-id>

表示跳转是从哪个页面开始的,因为一个页面可以跳转到很多页面中,因此使用

navigation-case 表示各种跳转情况。在 navigation-case 中有一个跳转标志(sayHello)和跳

转的目的地(/Result.jsp)。上面的整段代码表示,当在页面中的某一个控制标签中出现跳

转标识(如 sayHello)时,页面就会从源页面(/HelloWorld.jsp)导航到目的页面(/Result.jsp)。

JSF 中只有具有控制页面行为的标签如 commandButton、commandLink 等,才可以设置跳

转标识。为此,我们在 HelloWorld 页面的 commandButton 上设置导航标识,这是通过设置

action 属性实现的,action 属性定义点击这个标签时的行为,在此我们让它执行导航,当然

也可以用来执行代码:

<h:commandButton type="submit" value="提交" id="button1"styleClass="

commandButton" action="sayHello"></h:commandButton>

如果这时运行页面,在 HelloWorld 页面上单击“提交”按钮,页面就会跳转到 Result.jsp

上,但是这时 Result.jsp 页面上一片空白,因为我们没有定义任何业务逻辑。

绑定业务逻辑。在这一步中设置业务逻辑,当用户输入名字,单击“提交”后,在

页面上显示“Hello,xxx!”字样。

为此,首先要将第 3 步中定义的 HelloBean 设置为 ManagedBean,也就是让 JSF 管理

这个类的生命周期,当用户发送请求时自动创建一个 HelloBean 实例。设置 ManagedBean,

是通过配置 faces-config.xml 文件实现的。

配置 faces-config.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>

<faces-config xmlns="http://java.sun.com/xml/ns/javaee"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://java.sun.com/xml/ns/javaee

CHAPTER 第 4 章 JSF 介绍

147

4

http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd"

version="1.2">

<managed-bean>

<managed-bean-name>helloBean</managed-bean-name>

<managed-bean-class>org.bluelight.ch04.HelloBean</managed-bean-class>

<managed-bean-scope>session</managed-bean-scope>

</managed-bean>

</faces-config>

配置相当简单,可以利用 WTP 的可视化编辑器生成该文件。

将 HelloBean 设置为 ManagedBean 后,有一个 name 属性 helloBean,可以理解为

helloBean 是 HelloBean 类的实例化,在 JSF 中可以直接使用,这个实例化的过程是由 JSF

来完成的。然后在 JSF 的页面上,为 HelloWorld.jsp 页面中的输入框添加一个 value 属性,

值为“#{helloBean.name}”,完成后的页面代码如下:

<h:inputText id="input1" styleClass="inputText" value="#{helloBean.name}">

</h:inputText>

同样,将 Result.jsp 页面中输出框的值设置为“#{helloBean.message}”。

经过这样的设置后,当用户在页面上输入姓名,单击“提交”按钮后,姓名就会被封装

到 helloBean 对象的 name 属性中,然后跳转到 Result.jsp 页面,显示 helloBean 的 message 的

值。但是现在 message 并没有值,因此我们需要在得到 name 值的同时,设置 message 的值。

在 HelloBean 类上添加如下方法:

// process business logic

public String sayHello() {

// navigation target

String result = "sayHello";

if (this.name != null)

this.message = "Hello," + this.name + "!";

else

this.message = "Hello,World!";

return result;

}

然后,修改HelloWorld.jsp 页面的 commandButton 的 action 属性为“#{helloBean.sayHello}”,

作用就是当单击这个按钮的时候,执行 HelloBean类上的 sayHello 方法,这个方法给 message

赋值。在 JSF 中这就是定义事件(点击按钮)的监听器(当事件发生时执行 sayHello 方法),

这种事件监听器的机制与 Swing 类桌面应用程序一样。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

148

现在可以查看 JSF 的效果了。打开浏览器,在浏览器的地址栏中输入:

http://localhost:8080/HelloWorld/HelloWorld.faces,会出现左边的页面,当在页面上的输入框

中输入一个名字后单击“提交”按钮,在右边的页面上就会显示出相应的信息,如图 4-10

所示。

图 4-10 JSF HelloWord 示例运行结果(一)

如果在输入框中什么也不输入,单击“提交”按钮,那么就会出现默认的消息(如图

4-11 所示)。

图 4-11 JSF HelloWord 示例运行结果(二)

至此,第一个例子就完成了,在这个例子中学习了 JSF 基本的页面跳转和值绑定。

在下面的几节中我们将更深入地了解 JSF。

CHAPTER 第 4 章 JSF 介绍

149

4

4.4 JSF 的元素

通过上面的例子,我们初步了解了 JSF,在本节中将深入讲解 JSF 的各个部分。

4.4.1 UI 组件

UI 组件是 JSF 的中心部件,可以作为积木创建从简单到复杂的各种用户界面。JSF 组

件与 Swing 组件及其他组件一样,具备可组合性,可以将几个组件组合在一起形成一个更

大的组件,从而在页面上形成一棵组件树。在这一节中我们将详细地探究 JSF 的 UI 组件,

以及为了支持它们而存在的其他组件类型。

你可能已经熟悉 Swing 用户界面组件或者来自其他语言或开发环境(如 Visual Basic、

ASP.NET、Delphi 等)的 UI 组件,和这些语言为胖 UI 客户端所做的一样,JSF 为 Web 提

供了标准的用户界面组件框架。这样的标准化,为 Web 应用及复杂的用户界面组件库(例

如日历、树和表格)提供了更加强大和更加可视化开发环境的支持。

JSF 还提供了其他许多标准组件类型,它们的角色是支持 UI 组件,包括转换器、验证

器、渲染器等。和 UI 组件一样,这些组件是可互换的,并且贯穿同一应用和其他许多应用

都可以重用,这使得在使用 JSF 应用时,可以使用不同厂家提供的 JSF 组件。

那么,用户组件界面是由什么构成的呢?JSF 中的每个用户界面组件都实现了

javax.faces.component.UIComponent 接口,这个广泛的接口定义了若干方法来遍历组件树,

与后台数据 Model 交互,并管理所支持的关注点(例如组件验证、数据转换、渲染等)。

javax.faces.component.UIComponentBase 是一个便于使用的基类,它实现了 UIComponent

接口,为每个方法提供了默认实现。组件开发可以通过继承来定制组件的行为,创建新的

组件。

标准的 JSF 组件提供了建立 Web 应用时所需的许多基本组件。在多数情况下,

可以使用这些标准组件或者来自第三方类库的组件,因此通常不必担心 UIComponent

或 UIComponentBase。不过有时候,如果希望扩充现有组件,这时就需要定制新的组

件了。

组件的功能通常围绕着两个动作:解码和编码数据。解码是把进入的请求参数转换

成组件的值的过程;编码是把组件的当前值转换成对应的标记(也就是 HTML)的过

程。当用户请求一个 JSF 页面时,JSF 实现会把一个 JSF 页面上的所有 JSF 组件编码成

适合对方浏览器浏览的代码;如果请求者使用的是 HTML 协议,则会编码成 HTML 代

码并发送到客户端。当用户单击按钮或链接提交页面后,JSF 则开始解码过程,将进入

的请求参数转换成对应组件的值。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

150

JSF 框架提供了两个选项用于编码和解码数据。使用直接实现方式,组件自己实现解

码和编码;使用委托实现方式,组件委托渲染器进行编码和解码。如果选择委托实现,则

可以把组件与不同的渲染器关联,在页面上以不同的方式渲染组件,例如多选列表框和一

列复选框。

因此,JSF 组件由两部分构成:组件和渲染器。JSF 组件包含 UI 组件的状态和行为;

渲染器定义如何从请求中读取组件,如何显示组件——通常通过 HTML 渲染,渲染器把组

件的值转换成适当的标记。事件排队和性能验证发生在组件内部。

4.4.1.1 标准 UI 组件

JSF 的用户界面组件,在页面上表现为标签,而在服务器端;用户界面组件其实是 Java

类。表 4-1 概括了每个组件的后台组件。关于每个组件更详细的描述,可以在 JSF(Java Server

Faces)规范中找到。

表 4-1 JSF 组件

组 件 标 签 描 述

UIColumn <h:column> UIColumn 表示的是父组件 UIData 中的一列,遍历父组件 UIData

的每一行时,UIColumn 组件及子组件都会被处理一次

UICommand <h:commandButton>

<h:commandLink>

UICommand 表示诸如按钮、超链接和菜单项的 UI 组件,这些组

件被触发时会发送一个 ActionEvent ,后者可以被注册的

ActionListener 处理

UIData <h:dataTable>

UIData 表示数据集合,每个数据项由 DataModel(标准 JSF

UIComponent Model Bean)的一个实例封装,该组件通常被渲染成

表格、列表和树

UIForm <h:form> 表示用户输入表单,用做其他输入组件的容器

UIGraphic <h:graphicImage> 表示不可改变的图像或图形

UIMessage <h:messages>

<h:message> 显示错误信息

UIOutput <h:outputLabel>

<h:outputText> 表示诸如标签、错误信息及其他文字性数据

UIPanel <h:panelGrid> 组件的容器,一般用来管理布局

UIParameter <f:param> 向父组件提供附加的参数,这些参数在提交时发送

UISelectBoolean <h:selectBooleanCheckbox> 表示一个布尔数据项,在页面上通常渲染为复选框,在选择或取

消选择时会触发一个 ValueChangedEvent

UISelectItem <f:selectItem> 表示选择列表中的单独一项

CHAPTER 第 4 章 JSF 介绍

151

4

续表

组 件 标 签 描 述

UISelectItems <f:selectItems> 表示下拉列表中的多项

UISelectMany

<h:selectManyCheckbox>

<h:selectManyListbox>

<h:selectManyMenu>

表示诸如组合框、列表框、多组复选框等组件,允许选择多个条

目。当用户选择或取消选择时会发送 ValueChangeEvent 事件

UISelectOne <h:selectOneRadio> 类似 UISelectMany,但只允许选择一项

UIViewRoot <f:view> 表示组件树的根,在页面上没有渲染

表 4-1 也显示了 UI 组件的另一方面:它们是被组织成组件族(Family)的。组件

族是一组相关的 UI 组件,具有相似行为。按组件族组织主要是用于幕后处理以帮助进

行呈现;然而,因为同一族中的组件具有相似功能,因此将同一族组件放到一起讨论是

有益的。

这些组件之间的关系如图 4-12 所示。

图 4-12 标准 JSF 组件之间的关系

这些组件中的大部分都具有特别针对 HTML 的属性——例如,HtmlPanelGrid 组件

有 cellpadding 属性,因为它对应于 HTML 表格。在可视化的 IDE 中,可以直接使用 JSP

或者 Java 代码来操作这些属性。然而,这些组件都是未针对特定目标客户端特定属性

的通用组件的子类。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

152

所有这些组件都漂亮而华丽,但是为什么 UI 组件如此重要呢?上面已经讨论过——它

们提供有状态的,对与用户的交互进行封装的功能。例如,HtmlTextarea 组件向用户显示

一个 HTML <textarea>元素,根据用户的响应更新其值,并且在请求之间记住该值,其值还

可以直接关联到后台 Bean 或者模型对象。UI 组件也产生可以连接到服务器端代码的事件,

HtmlTextarea 将在用户输入新值时产生值改变事件。

UI 组件也大量地使用值绑定表达式,大多数情况下,任何组件属性都可以关联到一个

值绑定表达式。这意味着可以将组件的所有属性,即从组件的值到组件的诸如大小和标题

之类的所有属性,指定到完全不同的数据源中。

1. 标识符

在 JSF 页面中,会有很多 JSF 组件嵌套在一起使用,为了区别每个组件,JSF 通过组

件标识符(Component Identifier)来解决这个问题。在每个组件中都有一个 componentId 属

性,可以通过 getComponentId 来获取当前组件标识符,通过 setComponentId 方法设置它。

不过,大多数开发者都是在页面中设置的,如下面的例子:

<h:form id="form1" styleClass="form">

<h:panelGrid columns="2">

<h:outputLabel value="请输入你的名字" id="label1" for="input1">

</h:outputLabel>

<h:inputText id="input1" styleClass="inputText" value="#{helloBean.name}">

</h:inputText>

<h:commandButton type="submit" value="提交" id="button1" styleClass=

"commandButton" action="#{helloBean.sayHello}"></h:commandButton>

<h:commandButton type="reset" value="重置" id="button2" styleClass=

"commandButton"></h:commandButton>

</h:panelGrid>

</h:form>

不过,上面设置的 id 与生成的页面代码中的 id 并不一致,在生成页面时,JSF 会把父

组件的 id 与本组件的 id 联合在一起,形成一个新的 id。对于上面的代码,编译成的 HTML

代码如下:

<form id="form1" method="post" action="/JSFHelloWorld/HelloWorld.faces"

enctype="application/x-www-form-urlencoded">

<table>

<tbody>

<tr>

<td><label id="form1:label1" for="form1:input1"

CHAPTER 第 4 章 JSF 介绍

153

4

class="outputLabel"> 请输入你的名字</label></td>

<td><input id="form1:input1" type="text" name="form1:

input1" class="inputText" /></td>

</tr>

<tr>

<td><input id="form1:button1" type="submit" name="form1:

button1" value="&#25552;&#20132;" class="commandButton" />

</td>

<td><input id="form1:button2" type="reset" name="form1:button2

"value="&#37325;&#32622;" />

</td>

</tr>

</tbody>

</table>

</form>

从上面的代码中可以看出,在生成的 终代码中,都是以“父组件:子组件”的方式命

名组件 id 的。因此,当我们需要手工标识 JSF 组件的 id 时,要设置得尽量短一些,来限制

JSF 所产生响应的大小。

2. UI 组件树

前面曾经提及,在 JSF 中有一个组件树的概念,在每一个具体的 JSF 页面中,都会

有一棵组件树,这棵组件树的根就是<f:view>代表的 UIViewRoot 组件,每一棵树的节

点就是每一个 JSF 组件的标识符。当 JSF 页面提交时,在服务器端会以这个 JSF 的文件

名为名字,重新构建这棵 JSF 组件树,并对这棵树上的每一个节点递归调用解码(decode)

方法,开始 JSF 的生命周期过程。比如,上一节 HelloWorld 中的页面代码,将会形成如

下的组件树,如图 4-13 所示。

图 4-13 JSF 组件树

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

154

在 UIComponent 中定义了很多方法,可以通过这些方法访问组件的父组件和子组件。

这些方法多数都是望文知义的,通过 getChildren 方法返回的 List,可以对组件的子组件进

行遍历;通过调用 getParent()将返回父组件等。也可以在表单、面板或其他容器组件上调

用 findComponent()完成此需求。

现在应该很清楚,页面中的所有组件都表示在一棵树中,而这棵树则称为视图。

UIViewRoot 是此树的根组件,如果没有 UIViewRoot,就没有视图。这个组件很特别,它

并不显示任何东西,大部分工具都会自动将其添加到 JSP 中(通常不能从组件选项板中

拖出它)。而且,不能将它直接通过 binding 属性绑定到后台 bean,因为它不对 HTML 做

任何事情,它位于核心标签库。

关于 UIViewRoot,总结在表 4-2 中。

表 4-2 UIViewRoot 是整个视图的容器

组件 UIViewRoot

组件族 javax.faces.ViewRoot

可能的 IDE 显示名称 N/A

显示行为 持有视图中的所有子组件。不显示任何内容

标签库 Core

JSP 标签 <f:view>

直通属性 None

属性 类型 默认值 必需 说明

locale java.util.Locale 用户的当前场所 否 当前视图的场所

至此,已经学习了关于 UIViewRoot 的例子——它是由<f:view> 标签表示的,

UIViewRoot 必须封装同一页面中的所有 UI 组件,如下面的代码所示:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://

www.w3.org/TR/html4/loose.dtd">

<%@taglib uri="http://java.sun.com/jsf/html" prefix="h"%>

<%@taglib uri="http://java.sun.com/jsf/core" prefix="f"%>

<html>

<head>

<title>UIViewRoot</title>

</head>

<body>

<h1>UIViewRoot example</h1>

<f:view>

<h:panelGrid border="1" columns="2">

<h:outputText value="item1"></h:outputText>

<h:graphicImage url="images/hello.jpg"></h:graphicImage>

CHAPTER 第 4 章 JSF 介绍

155

4

</h:panelGrid>

</f:view>

</body>

</html>

在上面的代码中,UIViewRoot 被用在模板文本中(纯粹的 HTML),而其他所有

UI 组件都被嵌套其中(HtmlPanelGrid 及其子组件 HtmlOutputText 和 HtmlGraphicImage

组件);它还有两个常规 HTML 标记嵌套其中。这是一个很重要的用法:可以在<f:view>

标签内与 JSF 组件一起混合使用模板文本(如果想要在其他组件中使用模板文本,可

以使用<f:verbatim>标签)。然而,除非嵌套在<f:view>标签中,否则不能直接使用 JSF

组件。

UIViewRoot 的组件标签的唯一属性是 locale,允许指定当前页面支持的语言。假定想

要确保特定的页面总是以西班牙语显示,则写法如下:

<f:view locale="es">

……

</f:view>

字符串“es”是西班牙语的场所代码,所以上述代码将总是以西班牙语显示。每个页

面仅可以有一个视图,所以整个页面必须使用单一语言。

3. Facet

在 JSF 的各个组件中,组件之间除了父子关系外,还有一种与父子关系无关或正交的

从属组件角色,这个角色就是 Facet。Facet 通过<f:facet>定义,有一个 name 属性是必需的,

<f:facet>组件之间可以包括更多的组件,从而使得在容器组件中定义一个命名块(named

section)。Facet 组件的主要功能是,一个功能强大的组件中会有很多增强功能,这些增强

功能可以定义成很多小的 Facet 组件,这样可以从组件的一个主要功能中分解开,降低组

件内部功能的耦合性,同时也可以作为组件的插件机制来扩展。

要理解 Facet 的作用,请看下面的代码:

<d:pane_tabbed id="tabcontrol"

paneClass="tabbed-pane"

contentClass="tabbed-content"

selectedClass="tabbed-selected"

unselectedClass="tabbed-unselected">

<d:pane_tab id="first">

<f:facet name="label">

<d:pane_tablabel label="T a b 1" commandName="first" />

</f:facet>

...

</d:pane_tab>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

156

<d:pane_tab id="second">

<f:facet name="label">

<d:pane_tablabel image="images/duke.gif" commandName="second"/>

</f:facet>

...

</d:pane_tab>

...

在本例中,一个叫做 label 的 Facet 来标识 tab 组件中每个 tab 的标签。可以看出,这里

使用 Facet 从逻辑上表示了在各 tab 中的名字,从而将 tab 的名称和 tab 的内容分开,体现

了 tab 的主要作用,即将次要功能和主要功能分开。

在组件中,如果要访问组件内部的 Facet,则可以通过调用 UIComponent 提供的

getFacets()方法,这个方法返回 java.lang.Map;也可以在具体的组件上调用 getFacet()方法,

这个方法要提供 Facet 的名称作为参数。

此外,一些组件在设计时预定义了 Facet 的名称,例如,<h:dataTable>组件提供了预制

的“header”和“footer”,用来定义一个表格的表头和表尾。此外,<h:column>中也使用

header 来表示一个单元格的头信息。具体例子如下:

<h:dataTable id="reportTable" value="#{reportBean.dailyReport}"

var="item">

<h:column>

<f:facet name="header">

<h:outputText value="Daily Report" />

</f:facet>

<h:outputText value="#{item}" />

</h:column>

<f:facet name="footer">

<h:panelGroup id="group2" styleClass="panelGroup"></h:panelGroup>

</f:facet>

<f:facet name="header">

<h:panelGroup id="group1" ></h:panelGroup>

</f:facet>

</h:dataTable>

本例中就是通过 Facet 分别定义了表格的头尾信息,以及每个单元格的头信息。

4. 使用 HTML 属性

所有的标准 HTML 组件都支持基本 HTML 4.01 属性(attribute)的属性(property),

这些属性会被直接传递给浏览器,也称为直通(pass-through)属性。当在事件监听器或者

组件标签中操作组件时,都可以使用这些属性。

CHAPTER 第 4 章 JSF 介绍

157

4

在某些情况下,组件标签直接对应于 HTML 元素,所以使用直通属性是很符合逻辑的。

例如,这个组件标签:

<h:inputText value="hello" size="30" maxlength="40" accesskey="T"

tabindex="0"></h:inputText>

就对应于这段 HTML 代码:

<input type="text" name="_id1:_id2" value="hello" accesskey="T"

maxlength="40" size="30" tabindex="0"/>

黑体标出的那些属性被直接传递给浏览器,即便 value 属性看起来也是直通给浏览器

的,但实际上它是经过了合法性检查的。这引出了一个重要问题:直通属性是根本不经过

合法性检查的。

直通属性规则的一个微妙之处是 class 属性,它用来关联到 HTML 元素的 CSS 的类名

称。因为 JSP 中的技术限制,不能使用名称“class”。作为解决方法,大部分 UI 组件都有

一个 styleClass 属性。当声明一个组件时,可以使用空格分隔指定多个 CSS 类:

<h:myComponent styleClass="style1 style2 style3"/>

这样就为假定的 myComponent 指定了 3 个 CSS 样式类。某些 IDE 允许我们直接从项

目中的样式表中选择样式类。

大部分组件也支持 CSS style 属性,所以也可以不使用类而直接指定它们的样式。如

果你是 CSS 专家,则可以简单地通过 style 和 styleClass 属性手动将每个组件与样式表(或

样式)进行集成。某些 IDE 可以通过基本的 CSS 编辑器来简化这个过程,这样我们就可

以修改组件的外观而无须了解 CSS。一旦选择了显示属性(font color、background color、

border、alignment 等),IDE 将自动创建正确的 CSS 样式。

5. 数据 Model

JSF 有两种数据 Model:第一种 Model 与用户界面打交道,管理后者的状态;第二

种 Model 与服务器端的应用级 Model 关联。用户界面组件 Model 通常是由应用 Model

的数据快照(snap)来填充的,而对组件 Model 的改变则会通过应用事件传递回应用

Model。

组件 Model 通常通过 JSF 的“值绑定”机制(value-binding)与组件相关联。正如

在 HelloWorld 例子中看到的 ManagedBean,我们使用一个 ManagedBean 来保存输入框

中的数据,HelloBean 在 JSF 的应用配置文件中被注册为一个 ManagedBean,并且通过

一个唯一的标识符来被 UI 组件使用,要把 UI 组件绑定到 ManagedBean 上,我们使用

了 value 属性:

<h:inputText id="input1" styleClass="inputText" value="#{helloBean.name}">

</h:inputText>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

158

value 属性是通过 HelloBean 这个标识符引用这个 ManangedBean,再绑定到它上面的。

应用 Model 表示业务数据,目前有很多种方式来表示和持久化这些数据。

JavaBean(当不需要持久化时);

结合 JDBC 使用 JavaBean;

使用 ORM(如 Hibernate、JPA 等);

使用 EJB。

6. UI 组件的标准属性

本章所涉及的所有组件都有一些通用属性,比如 id 和 value。前面在讨论某个具体组

件时并未描述这些属性,这里我们将它们全部列于表 4-3 中。

表 4-3 UI 组件的通用属性

通用属性 类 型 默认值 必 需 说 明

id String 无 是 组件标识符

value Object 无 是 组件的本地值。可以是字面值或者值绑定表达式

rendered boolean true 否 控制组件是否可见

converter 转换器实例(值绑定表达

式或者转换器标识符) 无 否 设置用于值和显示字符串之间转换的转换器

styleClass String 无 否 CSS 样式类的名称。呈现为 HTML class 属性,可以

设置多个类名,用空格分隔

binding String 无 否 值绑定表达式,用来将该组件关联到后台 bean 属性

4.4.1.2 HtmlDataTable 组件

至此,我们所讨论的所有组件都是相对简单的——它们输出字符串、收集数据、允许

用户从列表中选择项目或者提交表单。如果有一个任何 UI 框架都需要的更加复杂的标准组

件,那一定是数据表格。对于桌面应用来说,不管是在 Swing、.NET 还是 Delphi 中进行开

发,都可以找到很多这种风格的变体(对于基于Web的UI框架也是如此,比如ASP.NET Web

Forms)。在 JSF 中,HtmlDataTable 就是一个标准的数据表格。

从技术上讲,HtmlDataTable 对应于 HTML <table>,这有点像 HtmlPanelGrid。关键的

不同之处在于,它设计为可以使用后台数据集,比如数据库结果集、数组或者列表。事实

上,如果没有动态数据源,根本无法使用它(当然,它也可以通过由受管 bean 创建工具所

配置的列表来使用)。

我们可以使用这个组件来简化在表格中显示数据的任务,或者使用输入控件来编辑数

据源中的数据;也可以滚动数据集或者一次显示特定数量的行。

注意:

这个组件具有比其他大多数组件都多的 Java-Only 属性,利用这些属性可以在

事件监听器中更好地操作这个组件。

CHAPTER 第 4 章 JSF 介绍

159

4

总结 HtmlDataTable,见表 4-4。

表 4-4 HtmlDataTable 总结

组件 HtmlDataTable

组件族 javax.faces.Data

显示行为

显示 HTML <table>元素。表格列通过 UIColumn 子组件指定。如果指定了 header Facet,将以 header

的内容显示<thead>元素;如果 UIColumn 组件有 header Facet,它们也将显示在<thead>元素中。对于

每行,都用 UIColumn 组件作为每列的模板。如果指定了 first 属性,将从 first 行开始显示;如果指定

了 row 属性,将只显示 row 行(从 first 开始)。如果指定了 footer Facet(对这个组件或者 UIColumn

子组件),将以 footer 的内容显示<tfoot>元素

IDE 显示名称 Data Grid,Data Table

标签库 HTML

JSP 标签 <h:dataTable>

直通属性 针对<table>元素的 HTML 属性

通用属性 id、rendered、styleClass、binding

通用属性 类 型 默认值 必 需 说 明

first int 0 否 要显示的数据集中第 1 行的数。改变这个值将开始显示新的行集

rows int 0 否 一次显示的行数。如果设置为 0(默认),所有的行都会被显示

value Object 无

组件的当前本地值,必须是引用数组、列表、JDBC ResultSet、

JSTL ResultSet 或者其他类型对象的值绑定表达式(其他对象被表

示为一行)

var String 无 否

请求作用域变量的名称,将在其下保存当前行的对象。需要引用当前

行的组件,可以在 JSF EL 表达式中使用这个值(例如,如果 var 设置为

“currentUser”,则子组件可以使用表达式"#{currentUser.name}")

headerClass String 无 否 用作 header Facet 的 CSS 样式的类名

footerClass String 无 否 用作 footer Facet 的 CSS 样式的类名

rowClasses String 无 否

用作行的、以逗号分隔的 CSS 样式类的列表。可以对一行指定多个

样式,其间以空格分隔。在每个样式应用完后,将重复使用。例如,

如果指定了两个样式类(style1 和 style2),则第 1 行使用 style1,第 2

行使用 style2,第 3 行又使用 style1,第 4 行又使用 style2,依此类推

columnClasses String 无 否

用作列的、以逗号分隔的 CSS 样式类的列表。可以对一列指定多个样

式,其间以空格分隔。在每个样式应用完后,将重复使用。例如,如果

指定了两个样式类(style1 和 style2),则第 1 列使用 style1,第 2 列类使

用 style2,第 3 列又使用 style1,第 4 列又使用 style2,依此类推

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

160

续表

Java-Only 属性

rowCount int 无 否 返回有效行数的总数(只读);如果总数未知,则返回-1

rowIndex int 无 否 返回当前选定行的索引(如果没有选定行,则为-1)

rowData Object 无 否 如果有效,则返回当前选择的行(只读)

rowAvailable boolean 无 否 如果 rowDate 当前的 rowIndex 有效,则返回 true(只读)

Facet 说 明

header 显示表格表头的子组件

footer 显示表格表尾的子组件

表 4-4 也说明了 HtmlDataTable 中的列是通过 UIColumn 子组件配置的。每个 UIColumn

扮演了特定列的模板,其内容则对将要显示的每一行进行重复。UIColumn 组件也有表头和

表尾的 Facet,总结在表 4-5 中。

表 4-5 UIColumn 总结

组件 UIColumn

组件族 javax.faces.Column

显示行为 无(用来定义 HtmlDataTable 中的列)

可能的 IDE 显示名称 —

标签库 HTML

JSP 标签 <h:column>

直通属性 无

通用属性 id、rendered、binding

Facet 说 明

header 显示表格表头的子组件

footer 显示表格表尾的子组件

UIColumn 仅有 3 个属性,虽然它也可以用在第三方组件中,但使用它的唯一标准组件

是 HtmlDataTable。

HtmlDataTable 是 强大的标准组件,可以以多种方式使用它。我们从一个简单的例子

开始,如表 4-6 所示。

CHAPTER 第 4 章 JSF 介绍

161

4

表 4-6 HtmlDataTable 示例:创建只读表格

HTML

<table id="form1:tableEx1" class="dataTableEx" border="1" cellpadding="2" cellspacing="0" ><thead ><tr >

<th class="headerClass" ><span id="form1:tableEx1:text1" class="outputText">Zone Id</span></th><th

class="headerClass" ><span id="form1:tableEx1:tt2" class="outputTt">Distance(KM)</span></th><th

class="headerClass" ><span id="form1:tableEx1:tt3" class="outputTt">Amount ($)</span></th></tr>

</thead><tbody >

<tr class="rowClass1" ><td class="columnClass1" >Zone 0</td>

<td class="columnClass1" >49.77032</td>

<td class="columnClass1" >36.5645</td>

</tr>

<tr class="rowClass2" ><td class="columnClass1" >Zone 1</td>

<td class="columnClass1" >55.99161</td>

<td class="columnClass1" >41.788</td>

</tr>

<tr class="rowClass1" ><td class="columnClass1" >Zone 2</td>

<td class="columnClass1" >62.212902</td>

<td class="columnClass1" >47.011497</td>

</tr>

<tr class="rowClass2" ><td class="columnClass1" >Zone 3</td>

<td class="columnClass1" >68.43419</td>

HTML

<td class="columnClass1" >52.234997</td>

</tr>

<tr class="rowClass1" ><td class="columnClass1" >Zone 4</td>

<td class="columnClass1" >74.65548</td>

<td class="columnClass1" >57.458496</td>

</tr>

<tr class="rowClass2" ><td class="columnClass1" >Zone 5</td>

<td class="columnClass1" >80.87677</td>

<td class="columnClass1" >62.682</td>

</tr>

<tr class="rowClass1" ><td class="columnClass1" >Zone 6</td>

<td class="columnClass1" >87.09806</td>

<td class="columnClass1" >67.905495</td>

</tr>

<tr class="rowClass2" ><td class="columnClass1" >Zone 7</td>

<td class="columnClass1" >93.31935</td>

<td class="columnClass1" >73.129</td>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

162

续表

HTML

</tr>

<tr class="rowClass1" ><td class="columnClass1" >Zone 8</td>

<td class="columnClass1" >99.54064</td>

<td class="columnClass1" >78.35249</td>

</tr>

<tr class="rowClass2" ><td class="columnClass1" >Zone 9</td>

<td class="columnClass1" >105.76193</td>

<td class="columnClass1" >83.576</td>

</tr>

</tbody>

</table>

组件标签

<h:dataTable id="tableEx1" value="#{pc_Test.zoneList}"

var="varzoneList" styleClass="dataTableEx" headerClass="headerClass" footerClass="footerClass"

rowClasses="rowClass1, rowClass2" columnClasses="columnClass1" border="1" cellpadding="2"

cellspacing="0">

<h:column id="columnEx1"><f:facet name="header">

<h:outputText styleClass="outputText" value="Zone Id"></h:outputText>

</f:facet><h:outputText value="#{varzoneList.zoneId}"></h:outputText>

</h:column>

组件标签

<h:column id="column2"><f:facet name="header"><h:outputText styleClass="outputTt"

value="Distance(km)"></h:outputText>

</f:facet><h:outputText value="#{varzoneList.distance}"></h:outputText>

</h:column>

<h:column id="column3"><f:facet name="header"><h:outputText styleClass="outputTt" value="Amount

$)"></h:outputText></f:facet><h:outputText value="#{varzoneList.amount}"></h:outputText>

</h:column>

</h:dataTable>

浏览器显示

这里,显示了具有 Zone Id、Distancec(KM)和 Amount($)列的简单 HTML 表格。在

CHAPTER 第 4 章 JSF 介绍

163

4

HtmlDataTable 的声明中,UIColumn 子组件用来定义每一列在输出中的模板。所以每个

UIColumn 的 header Facet 将显示在表格的表头中,并且每个 UIColumn 的内容将根据列表

中的行进行重复。

HtmlDataTable 和其 UIColumn 子组件输出表格结构,并且通过列表中的行进行迭代。

HtmlDataTable 的 cellpadding 属性和 border 属性是直通的。然而,var 属性通常用做可被子

组件(这里是 HtmlOutputText 组件)使用的请求作用域变量的名称。这也是子组件可以引

用名为 User 的 bean 的原因,HtmlDataTable 已经将当前行保存在该名称下了。

4.4.2 JSF 生命周期

在了解了 JSF 的 UI 组件后,在认识 JSF 其他特性之前,我们来了解一下 JSF 的生命周

期。首先介绍在每个阶段中会发生什么,以及这些阶段是如何相互连接在一起的,然后使

用一个示例程序来展示实际的生命周期。

JSF 生命周期的 6 个阶段如下:

恢复视图(Restore View);

应用请求值(Apply Request Values);

处理验证(Process Validations);

更新模型值(Update Model Values);

调用程序(Invoke Application);

进行响应(Render Response)。

这 6 个阶段显示了 JSF 请求处理的顺序,在每个生命周期中都要产生一些事件,在这

个周期执行结束后会分别调用这些事件的监听器,进行事件处理。在事件处理完成后会进

入下一个阶段执行。虽然上面列出了每个阶段中事件处理的可能执行顺序,但是 JSF 的生

命周期很难是固定的、一成不变的。如图 4-19 中所示,如果在某阶段中调用 Response

Complete 方法或者 Render Response 方法,或者如果在某阶段出现异常,如 FacesContext

中出现 JSF Message,JSF 都会跳过其他阶段直接进入 后一个阶段。

下面,我们从源码的角度来看一下 JSF 生命周期,了解 JSF 生命周期是如何逐个阶段

运行的。

首先来看一下在 web.xml 中配置的 Faces Servlet。当发起一个 JSF 请求时,例如,在

HelloWord 示例中请求 HelloWorld.faces 页面时,Faces Servlet 会首先接收到这个请求,从

而开始一个 JSF 生命周期,我们从下面的 Faces Servlet 的源代码(Sun 的 JSF 实现)中可以

看出这一点。

// Execute the request processing lifecycle for this request

try{

lifecycle.execute(context);

lifecycle.render(context); 第二步

第一步

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

164

}catch(FacesExceptione){

}

Faces Servlet 是一个 Servlet,因而有 init 和 service 两个方法会被执行。在 init 方法中,

Faces Servlet 会首先查找 JSF 实现提供的生命周期实现(LifeCycle 是一个接口,JSF 实现

会提供一个 LifeCycle 接口的实现),并将之实例化;在 service 方法中分两步处理 JSF 请求,

首先会创建一个唯一的 FacesContext 实例(注意这个 FacesContext 实例在一次 JSF 请求中

是唯一的,该请求的所有相关变量都会存储在这个对象中),然后分别执行 LifeCycle 的

execute 方法和 render 方法。在 execute 方法中执行 JSF 生命周期中的 1~5 个周期;再执行

LifeCycle 的 render 方法,也就是上面生命周期中的第 6 个阶段:Render Response 方法。因

此这两步联合在一起,就构成了 JSF 生命周期的 6 个阶段。

前面我们已经提到,这 6 个阶段的执行顺序不是一成不变的,如果这是第一次 JSF 请

求(也就是在请求中没有任何参数),那么 lifecycle.execute 方法将直接跳过 JSF 的前 5 个

阶段,进入渲染阶段;否则就会依次进行各个阶段的处理。如果在各阶段的执行过程中,

设置 ResponseComplete 或者 RenderResponse 为 true(RenderResponse 和 ResponseComplete

是 FacesContext 中存储的标识符,标志 JSF 请求的前 5 个阶段是否完成,可以在各个阶段

中取值或设置),或者当判断当前页面的请求是由单击 reload 刷新页面时,JSF 都会直接跳

过其他几个阶段,直接进入渲染阶段。

关于 ResponseComplete 和 RenderResponse 这两个变量还要额外说明一下。这两个变

量可以认为表示 JSF 的请求是否完成,下面的代码也说明这一点;但是还是有区别的,

ResponseComplete 标志整个 JSF 的请求已经完成,包括 JSF 的页面输出都已经完成,这

时 JSF 实现应该立刻结束当前生命周期并将执行权交由 FacesServlet 处理;而

RenderResponse 则标识在当前阶段执行完成后,应该立刻转入第 6 个阶段,渲染产生 JSF

的输出。

private Phase[] phases = {

null, // ANY_PHASE placeholder, not a real Phase

new RestoreViewPhase(),

new ApplyRequestValuesPhase(),

new ProcessValidationsPhase(),

new UpdateModelValuesPhase(),

new InvokeApplicationPhase()

};

……

public void execute(FacesContext context) throws FacesException {

……

for (int i = 1; i < phases.length; i++) {

CHAPTER 第 4 章 JSF 介绍

165

4

if(context.getRenderResponse() ||

context.getResponseComplete()) {

break;

}

PhaseId phaseId = (PhaseId) PhaseId.VALUES.get(i);

phase(phaseId, phases[i], context);

if (reload(phaseId, context)) {

context.renderResponse();

}

}

}

从代码上看,进入 JSF 生命周期之前,Faces Servlet 首先执行 lifecycle.execute 方法,

将控制权交由 LifeCycle 实例来处理。LifeCycle 的 execute 方法如上所示,首先在一个数组

中存储 JSF 的前 5 个阶段的实例,在判断当前的 JSF 请求没有开始渲染之后,对数组中的

每个阶段执行 phase 方法。那么 phase 方法进行什么动作呢?

// Execute the specified phase, calling all listeners as well

private void phase(PhaseId phaseId,Phase phase,FacesContext context)

throws FacesException {

//设置请求的语言编码

if (PhaseId.RESTORE_VIEW.equals(phaseId)) {

Util.getViewHandler(context).initView(context);

}

ListIterator<PhaseListener> listenersIterator = listeners.

listIterator();

try {

if (listenersIterator.hasNext()) {

PhaseEvent event = new PhaseEvent(context,phaseId,this);

while (listenersIterator.hasNext()) {

PhaseListener listener = listenersIterator.next();

//每个阶段触发前,首先处理相关阶段事件

listener.beforePhase(event);

}

} catch (Exception e) {

……

}

try {

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

166

//判断是否要跳过该阶段

if (!skipping(phaseId,context)) {

//对每个阶段执行其 execute方法

phase.execute(context);

}

} catch (Exception e) {

……

} finally {

if (listenersIterator.hasPrevious()) {

PhaseEvent event = new PhaseEvent(context,phaseId, this);

while (listenersIterator.hasPrevious()) {

//在每个阶段执行后,调用每个阶段的阶段后事件处理器

listener.afterPhase(event);

}

}

从代码中可以看出,对于每个阶段,在执行前首先要设置该请求的语言编码;然

后调用阶段前的事件监听器,处理阶段前的相关事件,再执行阶段的 execute 方法,

对每个阶段中的相关事务进行处理。 后,执行阶段执行后的事件监听器,处理阶段

后事件。

这就是 JSF 生命周期中每个阶段的详细处理过程。下面我们将结合源码及 HelloWorld

例子,进一步深入介绍 JSF 生命周期内部的各个阶段。

4.4.2.1 阶段 1:恢复视图

在 JSF 生命周期的第 1 个阶段——恢复视图中,会有一个转向 Faces Servlet 控

制器的请求。Faces Servlet 控制器会对请求进行考查,并提取出视图的 ID,这个 ID

是由 JSP 页面的名字来确定的。例如在 HelloWorld 例子中,当我们在浏览器的地址

栏中输入 http://localhost:8080/HelloWorld/HelloWorld.faces 时,Faces Servlet 控制器就

会接收到这个 JSF 的请求,并产生一个 HelloWorld 的视图 ID。JSF 框架控制器使用

这个视图 ID 来为当前的视图查找组件。如果这个视图尚未存在,那么 JSF 控制器

就会创建它;如果这个视图早已存在,那么 JSF 控制器就会使用它。这个视图包含

了所有的 GUI 组件。

生命周期的这个阶段表示为 3 个视图实例:新视图、原始视图和后视图,每个视图的

处理方式都不相同。在新视图的情况中,JSF 会构建 Faces 页面的视图,并将事件处理程

序和验证程序绑定到组件上。这个视图被保存在一个 FacesContext 对象中。

CHAPTER 第 4 章 JSF 介绍

167

4

我们需要了解一下 FacesContext 对象。FacesContext 对象是一个针对当前请求的全局唯

一类,包含了 JSF 用来管理当前会话中当前请求的 GUI 组件状态和需要的所有状态信息。

FacesContext 将视图保存在自己的 viewRoot 属性中;viewRoot 包含了当前视图 ID 的所

有 JSF 组件。

在原始视图的情况中(第一次加载的是一个页面),JSF 会创建一个空视图,这个空视

图会在用户事件产生时进行填充。JSF 可以直接从原始视图过渡到进行响应的阶段。

在后视图的情况中(用户返回之前访问过的页面),包含页面的视图早已经存在了,因

此只需要进行恢复就可以了。在这种情况中,JSF 就使用现有视图的状态信息来重构状态。

后视图的下一个阶段是应用请求值。

4.4.2.2 阶段 2:应用请求值

恢复了视图之后的下一阶段——应用请求值阶段,所做的工作是对进入的请求值或信

息名称—值对进行处理。视图层次结构中的每个用户界面组件节点,现在都能得到客户端

发送过来的更新值。

在幕后,JSF 运行时在用户界面组件树的视图(或 UIViewRoot)上调用高级方法

(processDecodes()),把请求值应用到用户界面组件,这导致所有子组件都递归地调用它们

的processDecodes()方法。用户界面组件的processDecodes()方法或者更具体的decode()方法,

允许组件对进入的请求名称—值对进行“解码”,并把匹配的新进入值应用到用户界面组件

的 value 属性。

应当指出,只有能够容纳值的用户界面组件(例如输入字段)才有新值应用到它

们。一般来说,有两类组件:一类是有值的组件,例如文本字段、复选框和标签;另

一类是引起动作的组件,例如按钮和链接。所有具有 value 属性的组件都实现

ValueHolder 接口;所有表单元素类型的组件,如果它的值可以由用户编辑,那么就都

实现 EditableValueHolder 接口;所有引起动作的组件(按钮或链接)都实现 ActionSource

接口。

例如,按钮(UICommand 或其他实现 ActionSource 的组件)在表单提交期间,不用新

值更新,它只需记录自己是否被单击,如果单击了,就引起称为动作事件(ActionEvent)

的事件进入队列。稍后我们就会看到动作事件到底是什么,以及它如何允许与按钮或链接

单击相对应的定制代码的执行。

虽然请求处理生命周期用连贯的方式处理不同阶段,但各阶段的执行顺序可以为特殊

情况而变化。例如,向表单中添加一个 Cancel 按钮,在单击时,它会跳过所有验证,不处

理表单的值,而是直接导航到另一个页面。要改变请求处理生命周期的处理顺序,只需设

置组件的 immediate 属性即可。本章后面将介绍,在不同的组件上设置 immediate 属性会有

不同的效果。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

168

应用请求值阶段的目的是让每个组件检索自己当前的状态信息,这些组件必须首先通

过 FacesContext 对象进行检索或创建(使用其值)。虽然组件值也可以从 cookie 或头文件

中进行检索,但是它们通常是通过请求参数进行检索的。

如果一个组件的即时事件处理属性(immediate)没有设置为 true,那么就会对这些值

进行转换。因此,如果域被绑定到一个 Integer 属性上,那么该值就会被转换为一个 Integer

类型。如果值的转换失败了,就会生成一个错误消息,并在 FacesContext 中进行排队,在

产生响应的阶段会显示其中的消息,同时还会显示所有的验证错误。

如果一个组件的即时事件处理属性的确被设置为 true( immediate=true),那么

这些值就会被转换为适当的类型,并进行有效性验证,然后转换后的值会被保存到

组件中。如果值转换或值的有效性验证失败了,就会生成一个错误消息,并在

FacesContext 中进行排队,在产生响应的阶段会显示其中的消息,同时还会显示所

有的验证错误。

4.4.2.3 阶段 3:处理验证

应用请求值阶段完成之后,就要执行对进入数据进行转换和验证的处理验证阶段。生

命周期中的第一个事件处理发生在应用请求值阶段之后。在这个阶段中,每个组件都有一

些值需要根据应用程序的验证规则进行有效性验证,这些验证规则可以是预先进行定义的

(JSF 中提供的),也可以由开发者进行定义。用户所输入的值会与这些验证规则进行比较,

如果输入的值无效,就会向 FacesContext 中添加一个错误消息(Faces Message),并且该

组件会被表示为无效的。如果一个组件被表示为无效的,那么 JSF 就会转到产生响应的阶

段,在这个阶段中会显示当前的视图,以及验证错误消息。如果没有有效性验证错误,那

么 JSF 就会转到更新模型值阶段。

JSF 运行时调用主 processValidators()方法(在 UIViewRoot 实例上),启动这个阶段。

processValidators()与 processDecodes()方法类似,也是递归地进入组件树,调用每个组件的

processValidators()方法。在调用每个组件的 processValidators()方法时,与组件相关的转换

器或验证器就会被调用。

验证器和转换器可以用若干种方式与用户界面组件关联。验证请求可以通过设置组件

本身的属性(例如,把 inputText 组件的 required 属性设置为“true”)与组件关联,也可以

通过注册定制验证代码(例如,通过设置组件的 validator 属性,向组件附加电子邮件验证

方法)与组件关联。通过插入 convertDateTime 转换器标签作为输入组件的子组件,与“出

生日期”(UIInput)组件关联。

验证(或转换)失败的组件的 valid 属性会设置成“false”,并把 Faces Message 消息放

进 FacesContext 队列。然后,当响应被渲染回用户(在渲染响应阶段)时,可以用 Faces

的 Message 或 Messages 组件显示消息,以便用户纠正错误或重新提交。

CHAPTER 第 4 章 JSF 介绍

169

4

4.4.2.4 阶段 4:更新模型值

假设进入的数据已经通过验证和转换,现在要把数据分配给值绑定到用户界面组件的

模型对象。回忆一下本章中的第一个示例,在这个示例中,我们创建了一个 Java类UserBean,

把它注册成托管 Bean,并用 JSF 表达式语言把属性绑定到页面上的不同用户界面组件。正

是在更新模型值阶段,实际的托管 Bean 或模型对象的属性,被它们绑定的用户界面组件的

新值更新。

这个操作背后实际的机制与其他阶段相似:在 UIViewRoot 实例上调用

processUpdates()方法,初始化 processUpdates()组件方法的逐级调用,processUpdates()

方法又对每个类型为 UIInput 或者从它扩展的组件调用 updateModel()方法。这么做是合

理的,因为 UIInput 类型的组件(例如输入字段、选择菜单)是能够把用户输入值传递给

模型属性的唯一组件类型。如图 4-14 所示,在这个阶段末尾,模型对象(托管 Bean)

的值绑定属性都被来自组件的新值更新。这个阶段揭示了 JSF(Java Server Faces)的一

部分秘密:只要把 JavaBean 属性绑定到一组 JSF 用户界面组件,不需要手动编码,它

们就能自动更新。

图 4-14 在更新模型值阶段更新模型对象属性

4.4.2.5 阶段 5:调用程序

现在,已经清楚了 JSF 请求处理生命周期如何执行从 Web 请求获得进入数据、验

证数据、把数据转换成合适的服务器端数据类型,以及把数据分配给模型对象等工作。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

170

对 Web 开发人员来说,这只是 Web 应用程序编写工作的一半。另一半是:取得进入数

据,真正对数据进行操作,例如调用处理数据的外部方法。这正是调用应用程序阶段的

作用。

回忆本章前面所述内容,用户界面组件既可以容纳值(实现 EditableValueHolder),也

可以是 ActionEvent 来源(实现 ActionSource)(例如在单击按钮(UICommand)时)。定制

动作代码,又称为动作方法或动作侦听器方法,就是在调用应用程序阶段调用的。

在调用应用程序阶段中,UIViewRoot 的 processApplication()方法被调用,它又把队

列中这个阶段的事件广播到每个实现 ActionSource(对 JSF 1.2 是 ActionSource2)的

UIComponent 组件。这是通过调用每个 UIComponent 组件的 broadcast()方法做到的,实

际上就是“触发了”动作事件,接着就由动作侦听器处理这些动作事件。可以用默认动

作侦听器编写定制动作方法或动作侦听器方法,并把它们绑定到 UIComponent 组件(实

现 ActionSource 的 UIComponent),处理动作事件。编写定制动作方法或动作侦听器方

法,并把它们绑定到实现 ActionSource 的 UIComponent 组件,为开发人员提供了参与

请求处理生命周期的钩子(hook),通过钩子,开发人员就能调用任何定制逻辑,如图

4-15 所示。

图 4-15 在调用应用程序阶段调用定制应用程序逻辑

在这个阶段,还可以为一个给定的序列或很多可能的序列指定后面的逻辑视图,这可

以通过为一次成功的表单提交定义一个特定的结果并返回这个结果来实现。例如:在成功

输出时,将用户重定向到下一页中。要让这种导航工作能够起作用,就需要在

faces-config.xml 文件中创建一个到“成功输出”的映射作为一条导航规则。一旦导航发生

之后,就可以转换到生命周期的 后一个阶段了。

CHAPTER 第 4 章 JSF 介绍

171

4

还应当指出,导航到不同页面,也发生在调用应用程序阶段。我们在前面的例子中已

经研究过导航到底是怎么发生的——登录应用程序使用了一个绑定到 submit(UICommand)

按钮的简单动作。当用户单击按钮时,触发动作事件,动作事件又在调用应用程序阶段调

用定制动作方法,处理登录凭据。请记住,这个代码只有在进入的数据已经通过前面阶段

(转换和验证已经完成)之后才会执行。登录成功时,会导航到新页面。

4.4.2.6 阶段 6:进行响应

现在到了 JSF 请求处理生命周期的 后阶段,在这个阶段渲染响应。为了把整个响应

渲染给客户端,又一次逐级对每个组件调用 encodeXX()方法。编码方法是用户界面组件(或

者更具体点,组件的渲染器)向客户端渲染组件的方法。渲染的标记语言可以是任何语言,

例如 HTML、WML、XML 等。

除了把响应渲染给客户端,渲染响应阶段还保存视图的当前状态,以便可以在后续 Web

请求中访问和恢复状态。这时,视图的当前状态被保存下来,供未来请求之用。

另外,还有更多精细的幕后细节与渲染响应阶段有关,其中包括:处理静态内容

(又称为“模板”源)与组件的动态内容交织的情况;处理各种动态输出源;在保持

顺序正确的同时把它们协调成一个可视的响应。一般来说,使用 JSF 时不需要处理这

些细节。

在生命周期的第 6 个阶段——进行响应中,我们可以在视图中显示当前状态中的所有

组件。

4.4.3 数据转换与验证

对用户提交的数据进行正确性验证,对于所有收集用户信息的应用程序(不仅仅是 Web

应用)都是非常重要的一个方面,是确保应用健壮性的重要保证。JSF 对数据验证提供了

强大的功能,在这一节中我们将讨论 JSF 的 Validator 组件,以及它们是如何与 UIInput 关

联的,同时还要讲解如何编写及注册 Validator 组件。

转换是什么?简单地说,转换是确保数据拥有正确的对象或者类型的过程。下面是两

个典型的转换:

字符串值可以转换为 java.util.Date;

字符串值可以转换为 Float。

至于验证,它用于确保数据包含所期望的内容。下面是两个典型的验证:

java.util.Date 的格式为 MM/yyyy;

Float 在 1.0 和 100.0 之间。

转换和验证的主要目的是确保在更新模型数据之前已经经过了正确的无害处理。之后,

当需要调用应用程序方法用这些数据实际做一些事情时,就可以有把握地假定模型的某些

状态。转换和验证使您可以侧重于业务逻辑,而不是侧重于对输入数据进行烦琐的资格认

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

172

定,比如 null 检验、长度限定、范围边界等。

因此,在更新模型数据生命周期阶段中,在组件数据被绑定到 Backing Bean 模型之前

进行转换和验证处理是有道理的。正如图 4-16 所示,转换发生在应用请求值阶段,而验证

发生在处理验证阶段。

图 4-16 要关注的转换和验证阶段

注意:

图 4-16 中描绘的转换和验证过程表示了将 UIInput 组件的 immediate 属性

设置为 false 时的应用程序流程。如果这个属性设置为 true,那么转换和验证会

发生在生命周期更早的时期,即应用请求值阶段。对使用 immediate 属性的详细

讨论超出了本文的范围,但是在某些情况下,比如管理动态清单,它很有用,它

甚至可以绕过验证(在与 UICommand 组件结合使用时)。

图 4-17 展示了当 immediate 属性设置为 true 时,在 JSF 应用程序生命周期中的哪些

地方进行转换和验证。

图 4-17 将 immediate 属性设置为 true

CHAPTER 第 4 章 JSF 介绍

173

4

下面,我们将用一个示例应用程序来展示 JSF 的转换和验证能力。这个示例应用程序

非常简单,没有面面俱到,我们的目的不是构建一个在真实世界中使用的应用程序。这个

示例应用程序将展示以下几点:

使用标准 JSF 转换器转换表单字段数据;

使用标准 JSF 验证组件验证表单字段数据;

如何编写自定义转换器和验证器;

如何在 faces-config.xml 文件中注册自定义转换器和验证器;

如何定制默认错误消息。

这个示例应用程序是一个简单的用户注册表单,我们的目标是收集用户数据,比如姓

名、年龄、电子邮箱地址和电话号码。然后,展示如何利用 JSF 转换和验证确保收集的数

据对于模型是合适的。

这个应用程序使用了 3 个 JSP 页:

index.jsp 将用户定向到 UserRegistration.jsp;

UserRegistration.jsp 包含应用程序的表单字段;

results.jsp 通知应用程序用户已经注册。

我们首先分析编写 JSF 转换过程的选择。

4.4.3.1 JSF 转换

如前所述,转换是确保数据对象或者类型正确的一个过程,因此,我们可以将字符串

值转换为其他类型,比如 Date 对象、基本浮点型或者 Float 对象。可以使用自带的转换

器,也可以编写自定义的转换器。

1.标准转换器

JSF 提供了许多标准数据转换器,也可以通过实现 Converter 接口插入自定义转换器。

表 4-7 显示了 JSF 进行简单数据转换所使用的转换器 id 及其对应的实现类。大多数数据

转换是自动发生的。 表 4-7 JSF 标准转换器

javax.faces.BigDecimal javax.faces.convert.BigDecimalConverter

javax.faces.BigInteger javax.faces.convert.BigIntegerConverter

javax.faces.Boolean javax.faces.convert.BooleanConverter

javax.faces.Byte javax.faces.convert.ByteConverter

javax.faces.Character javax.faces.convert.CharacterConverter

javax.faces.DateTime javax.faces.convert.DateTimeConverter

javax.faces.Double javax.faces.convert.DoubleConverter

javax.faces.Float javax.faces.convert.FloatConverter

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

174

图 4-18 展示了用户年龄的默认转换。JSF 标签配置如下:

<!-- UserRegistration.jsp -->

<h:inputText id="age" value="#{UserRegistration.user.age}"/>

图 4-18 各种情况的转换器

UserRegistration.user.age 表示一个值绑定属性,它的类型为 int。对于基本型或者

BigInteger/BigDecimal 的绑定,JSF 选择了标准转换器。不过,还可以通过 <f:converter/>

标签,利用一个特定的转换器来增加粒度,如下所示。

<!-- UserRegistration.jsp -->

<h:inputText id="age" value="#{UserRegistration.user.age}">

<f:converter id="javax.faces.Short"/>

</h:inputText>

在图 4-19 中,可以看到 JSF 使用标准转换器的场景。在这种情况下,虽然年龄实际

上是一个有效的整数,但转换仍然会失败,因为该值不是短整型的。

图 4-19 使用<f:converter>标签

CHAPTER 第 4 章 JSF 介绍

175

4

2. 日期转换

尽管在默认情况下,JSF 可以很好地处理基本型及类似的类型,但是在处理日期数据

时,必须指定转换标签 <f:convertDateTime/>。这个标签基于 java.text 包,并使用短、长

和自定义样式。下面是一个例子:

<h:inputText id="birthDate"

value="#{UserRegistration.user.birthDate}">

<f:convertDateTime pattern="MM/yyyy"/>

</h:inputText>

这个例子展示了如何用 <f:convertDateTime/> 确保用户的生日可以转换为格式为

MM/yyyy(月/年)的日期对象。请参阅 JSF 的 java.text.SimpleDataFormat,以获取模式

列表。

3. 其他样式

除了可以转换日期和时间格式外,JSF 还提供了处理像百分数或者货币数据这类值的

特殊转换器,这个转换器处理分组(如逗号)、小数、货币符号等。例如,以下

<f:convertNumber/>标签的用法就是处理货币的一种技巧.

<!-- UserRegistration.jsp -->

<h:inputText id="salary" value="#{UserRegistration.user.salary}">

<f:convertNumber maxFractionDigits="2"

groupingUsed="true"

currencySymbol="$"

maxIntegerDigits="7"

type="currency"/>

</h:inputText>

在图 4-20 中,可以看到格式编排不正确的货

币数据,以及所导致的转换错误。

4. 自定义转换器

如果需要将字段数据转换为特定于应用程序的

值对象,则需要自定义数据转换器,如下所示。

String 转 换 为 PhoneNumber 对 象

(PhoneNumber.areaCode、PhoneNumber.prefix、…);

String 转换为 Name 对象(Name.first、

Name.last);

图4-20 使用<f:convertNumber>标签

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

176

String 转换为 ProductCode 对象(ProductCode.partNum、ProductCode.rev、…)。

要创建自定义转换器,必须完成以下步骤:

实现 Converter 接口(也就是 javax.faxes.convert.Converter);

实现 getAsObject 方法,它将一个字段(字符串)转换为一个对象(如 PhoneNumber);

实现 getAsString 方法,它将一个对象(如 PhoneNumber)转换为一个字符串;

在 Faces 上下文中注册自定义转换器;

用 <f:converter/> 标签在 JSP 中插入这个转换器。

在图 4-21 中,JSF 在应用请求值阶段调用自定义转换器的 getAsObject 方法。

转换器必须在这里将请求字符串转换为所需的对象类型,然后返回这个对象,将它

存储在相应的 JSF 组件中。如果该值被返回呈现在视图中,那么 JSF 将在呈现响

应阶段调用 getAsString 方法。这意味着转换器还要负责将对象数据转换回字符串

表示形式。

图 4-21 自定义转换器 getAsObject 和 getAsString 方法

5. 创建自定义转换器

下面通过一个示例分析来展示 Converter 接口、getAsObject 和 getAsString 方法的实

现,同时还将展示如何在 Faces 上下文中注册这个转换器。

这个示例分析的目的是将一个单字段字符串值转换为一个 PhoneNumber 对象。我们

将一步一步地完成这个转换过程。

第 1 步:实现 Converter 接口

import javax.faces.convert.Converter;

import org.apache.commons.lang.StringUtils;

...

CHAPTER 第 4 章 JSF 介绍

177

4

public class PhoneConverter implements Converter {

...

}

第 2 步:实现 getAsObject 方法

这一步将一个字段值转换为一个 PhoneNumber 对象。

public class PhoneConverter implements Converter {

...

public Object getAsObject(FacesContext context, UIComponent component,

String value) {

if (StringUtils.isEmpty(value)){ return null;}

PhoneNumber phone = new PhoneNumber();

String [] phoneComps = StringUtils.split(value," ,()-");

String countryCode = phoneComps[0];

phone.setCountryCode(countryCode);

if ("1".equals(countryCode)){

String areaCode = phoneComps[1];

String prefix = phoneComps[2];

String number = phoneComps[3];

phone.setAreaCode(areaCode);

phone.setPrefix(prefix);

phone.setNumber(number);

}else {

phone.setNumber(value);

}

return phone;

}

}

第 3 步:实现 getAsString 方法

这一步将一个 PhoneNumber 对象转换为一个字符串。

public class PhoneConverter implements Converter {

...

public String getAsString(FacesContext context, UIComponent component,

Object value) {

return value.toString();

}

}

public class PhoneNumber implements Serializable {

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

178

...

public String toString(){

if (countryCode.equals("1")){

return countryCode + " " + areaCode

+ " " + prefix + " " + number;

}else{

return number;

}

}

}

第 4 步:在 Faces 上下文中注册自定义转换器

这一步可以通过两种方式执行。第一种方式是选择使用比如arcmind.PhoneConverter 的

id 来注册 PhoneConverter 类。JSP 页中的 <f:converter/> 标签会使用这个 id。

<converter>

<converter-id>arcmind.PhoneConverter</converter-id>

<converter-class>com.arcmind.converters.PhoneConverter</converter-class>

</converter>

第二种方式是注册 PhoneConverter 类来自动处理所有的 PhoneNumber 对象,如下所

示。

<converter>

<converter-for-class>com.arcmind.value.PhoneNumber</converter-for-class>

<converter-class>com.arcmind.converters.PhoneConverter</converter-class>

</converter>

第 5 步:在 JSP 中使用转换器标签

这一步的执行取决于所选择的注册方式。如果选择使用 arcmind.PhoneConverter 的 id

来注册 PhoneConverter 类,那么就使用 <f:converter/> 标签,如下所示。

<h:inputText id="phone" value="#{UserRegistration.user.phone}">

<f:converter converterId="arcmind.PhoneConverter" />

</h:inputText>

如果选择注册 PhoneConverter 类来自动处理所有的 PhoneNumber 对象,那么就不需

要在 JSP 页中使用<f:converter/>标签了。下面是不带转换器标签的代码。

<h:inputText id="phone" value="#{UserRegistration.user.phone}">

[Look mom no converter!]

</h:inputText>

CHAPTER 第 4 章 JSF 介绍

179

4

这样,我们就完成了这个示例应用程序的转换处理代码。

4.4.3.2 JSF 验证

如前所述,JSF 验证可以确保应用程序数据包含预期的内容。例如:

java.util.Date 为 MM/yyyy 格式;

Float 在 1.0 和 100.0 之间。

在 JSF 中有 4 种验证:

自带验证组件;

应用程序级验证;

自定义验证组件(实现 Validator 接口);

在 Backing Bean 中的验证方法(内联)。

我们将在下面的讨论中介绍并展示每一种验证形式。

1.JSF 验证生命周期和组件

下面是 JSF 提供的一组标准验证组件。

DoubleRangeValidator:组件的本地值必须为数字类型,必须在由 小值和/或 大值

所指定的范围内。

LongRangeValidator:组件的本地值必须为数字类型,并且可以转换为长整型,必须

在由 小值和/或 大值所指定的范围内。

LengthValidator:类型必须为字符串,长度必须在由 小值和/或 大值所指定的范围内。

2.标准验证

在示例应用程序中,用户的年龄可以是任意有效的整数(byte、short、int)。比如将年

龄设置为-2 是无意义的,所以可能要对这个字段添加一些验证。下面是一些简单的验证代

码,用以确保年龄字段中的数据模型完整性。

<h:inputText id="age" value="#{UserRegistration.user.age}">

<f:validateLongRange maximum="150"minimum="0"/>

</h:inputText>

完成年龄字段限制后,可能希望对名字字段的长度加以限制。可以像这样编写这个验证

代码:

<h:inputText id="firstName" value="#{UserRegistration.user.firstName}">

<f:validateLength minimum="2" maximum="25" />

</h:inputText>

图 4-22 显示了由上面标准验证示例所生成的标准验证错误消息。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

180

图 4-22 标准验证错误消息

尽管 JSF 自带的验证在许多情况下都可以满足需要,但是它有一些局限性。在处理电

子邮件、电话号码、URL、日期等验证数据时,有时编写自己的验证器会更好一些,我们

将在稍后对此进行讨论。

3. 应用程序级验证

在概念上,应用程序级验证实际上是业务逻辑验证,JSF 将表单和/或字段级验证与业务

逻辑验证分离。应用程序级验证主要需要在 Backing Bean 中添加代码,用这个模型确定绑定

到模型中的数据是否合格。对于购物车,表单级验证可以验证输入的数量是否有效,但是需

要使用业务逻辑验证检查用户是否超出了他或者她的信用额度。这是在 JSF 中分离关注点

的另一个例子。

例如:假定用户单击了绑定到某个操作方法的按钮,那么就会在调用应用程序阶段调

用这个方法;假定在更新模型阶段进行了更新,那么在对模型数据执行任何操纵之前,可

以添加一些验证代码,根据应用程序的业务规则检查输入的数据是否有效。

比如,在这个示例应用程序中,用户单击了 Register 按钮,这个按钮被绑定到应用程

序控制器的 register()方法。我们可以在 register() 方法中添加验证代码,以确定名字字段

是否为 null。如果该字段为 null,那么还可以在 FacesContext 中添加一条消息,指示相关

组件返回到当前页。

应用程序级验证非常直观并且容易实现。不过,这种形式的验证是在其他形式的验证

(标准、自定义)之后发生的。

应用程序级验证的优点如下:

CHAPTER 第 4 章 JSF 介绍

181

4

容易实现;

不需要单独的类(自定义验证器);

不需要编写者指定验证器。

应用程序级验证的缺点如下:

在其他形式的验证(标准、自定义)之后发生;

验证逻辑局限于 Backing Bean 方法,使得重用性很有限;

在大型应用程序和/或团队环境中可能难于管理。

实际上,应用程序级验证只应该用于那些需要业务逻辑验证的环境中。

4. 自定义验证组件

对于标准 JSF 验证器不支持的数据类型,则需要建立自己的自定义验证组件,其中

包括电子邮件地址和邮政编码。如果需要明确控制显示给 终用户的消息,那么还需要

建立自己的验证器。在 JSF 中,可以创建可在整个 Web 应用程序中重复使用的可插入验

证组件。

创建自定义验证器的步骤如下,我们将一步一步地进行分析。

创建一个实现了 Validator 接口的类 (javax.faces.validator.Validator)。

实现 validate 方法。

在 faces-config.xml 文件中注册自定义验证器。

在 JSP 页中使用 <f:validator/> 标签。

下面是创建自定义验证器的分步示例代码。

第 1 步:实现 Validator 接口

import javax.faces.validator.Validator;

import javax.faces.validator.ValidatorException;

...

public class ZipCodeValidator implements Validator{

private boolean plus4Required;

private boolean plus4Optional;

/** Accepts zip codes like 85710 */

private static final String ZIP_REGEX = "[0-9]{5}";

/** Accepts zip code plus 4 extensions like "-1119" or " 1119" */

private static final String PLUS4_REQUIRED_REGEX = "[ |-]{1}[0-9]{4}";

/** Optionally accepts a plus 4 */

private static final String PLUS4_OPTIONAL_REGEX = "([ |-]{1}[0-9]{4})?";

...

}

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

182

第 2 步:实现验证方法

这一步需要实现 validate 方法。

public void validate(FacesContext context, UIComponent component, Object

value) throws ValidatorException {

/* Create the correct mask */

Pattern mask = null;

/* more on this method later */

initProps(component);

if (plus4Required){

mask = Pattern.compile(ZIP_REGEX + PLUS4_REQUIRED_REGEX);

} else if (plus4Optional){

mask = Pattern.compile(ZIP_REGEX + PLUS4_OPTIONAL_REGEX);

} else if (plus4Required && plus4Optional){

throw new IllegalStateException("Plus 4 is either optional or

required");

}

else {

mask = Pattern.compile(ZIP_REGEX);

}

/* Get the string value of the current field */

String zipField = (String)value;

/* Check to see if the value is a zip code */

Matcher matcher = mask.matcher(zipField);

if (!matcher.matches()){

FacesMessage message = new FacesMessage();

message.setDetail("Zip code not valid");

message.setSummary("Zip code not valid");

message.setSeverity(FacesMessage.SEVERITY_ERROR);

throw new ValidatorException(message);

}

}

第 3 步:在 faces-config.xml 文件中注册自定义验证器

<validator>

<validator-id>arcmind.zipCodeValidator</validator-id>

<validator-class>com.arcmind.jsfquickstart.validation.ZipCodeValidator

CHAPTER 第 4 章 JSF 介绍

183

4

</validator-class>

</validator>

第 4 步:在 JSP 页中使用 <f:validator/> 标签

<f:validator/> 标签声明使用 zipCodeValidator。<f:attribute/> 标签将 plus4Optional 属

性设置为 true。注意:它定义了 inputText 组件的属性,而不是验证器的属性。

<h:inputText id="zipCode" value="#{UserRegistration.user.zipCode}">

<f:validator validatorId="armind.zipCodeValidator"/>

<f:attribute name="plus4Optional" value="true"/>

</h:inputText>

为了读取 zipCode 组件的 plus4Optional 属性,请完成以下配置:

private void initProps(UIComponent component) {

Boolean optional = Boolean.valueOf((String) component.getAttributes().

get("plus4Optional"));

Boolean required = Boolean.valueOf((String) component.getAttributes().

get("plus4Required"));

plus4Optional = optional==null ? plus4Optional :

optional.booleanValue();

plus4Required = required==null ? plus4Optional :

required.booleanValue();

}

总体而言,创建自定义验证器是相当直观的,并且可以使该验证在许多应用程序中重

复使用。缺点是必须创建一个类,并在 Faces 上下文中管理验证器注册。不过,通过创建

一个使用这个验证器的自定义标签,使其看上去像是一个自带的验证,可以进一步实现自

定义验证器。对于常见的验证问题,如电子邮件验证,这种方法可以支持这样一种设计理

念,即代码重用和一致的应用程序行为是 重要的。

5. Backing Bean 中的验证方法

作为创建单独的验证器类的替代方法,可以只在 Backing Bean 的方法中实现自定义

验证,只要这个方法符合 Validator 接口的 validate 方法的参数签名即可。例如,可以

编写以下方法:

[SomeBackingBean.java]

public void validateEmail(FacesContext context,

UIComponent toValidate,

Object value) {

String email = (String) value;

if (email.indexOf('@') == -1) {

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

184

((UIInput)toValidate).setValid(false);

FacesMessage message = new FacesMessage("Invalid Email");

context.addMessage(toValidate.getClientId(context), message);

}

}

然后,可以通过如下所示的 validator 属性在 JSF 中使用这个方法:

<h:inputText id="email"

value="#{UserRegistration.user.email}"

validator="#{UserRegistration.validateEmail}"

required="true">

</h:inputText>

JSF 用 validateEmail 方法对绑定到 user.email 模型属性的 inputText 组件值进行自

定义验证。如果电子邮件格式无效,那么就在相关组件的 Faces 上下文中添加消息。

6. 默认验证

注意上面 email 标签的 required 属性,利用 required 属性是一种默认验证形式。如

果这个属性设置为 true,那么相应的组件必须有一个值。一个重要的说明:如果 required 属

性设置为 false,那么就不用对这个标签/组件指派验证了,这样,JSF 将跳过对这个组件

的验证,并让值和组件的状态保持不变。

图 4-23 概述了我们上面讨论过的验证形式。

图 4-23 验证视图

CHAPTER 第 4 章 JSF 介绍

185

4

7. 自定义消息

JSF 提供的默认转换和验证消息非常长,这会让那些总是输入无效表单数据的 终用

户感到困惑。幸运的是,可以通过创建自己的消息资源绑定来改变 JSF 提供的默认消息。

jsf-impl.jar(或类似的文件中)中包含了一个 message.properties 文件,该文件提供默认的

JSF 转换和验证消息。

通过创建自己的 message.properties 文件并取消指定场所的 Faces 上下文中绑定的消

息资源,就可以更改默认消息,如图 4-24 所示。

图 4-24 取消消息资源绑定

4.4.4 JSF 事件处理

Web 应用通常需要响应用户事件,比如选择菜单项目或者单击按钮。例如,响应用户

对地址表单中的国家选择,更改场所和加载当前页面以更好地适应用户。与桌面程序一样,

用户的这些动作在 JSF 中也会产生事件。通常,输入控件产生的事件主要是值改变事件和

动作事件,而这些事件可以被事件监听器处理,就像在 JavaScript 中注册的事件监听器一

样。事件监听器响应事件,它们通常由 Java 代码写成,可以是后台 bean 方法,也可以是

单独类。使用后台 bean 方法更通用些(也更快捷),但是当监听器需要在不同的上下文中

重用时,实现单独的接口也是很有用的。

下面是一个下拉菜单中的事件监听器,用 valueChangeListener 表示的是值改变事件监

听器。

<h:selectOneMenu id="select" valueChangeListener="#{user.menuEvent}"

onchange="submit(); "value="#{user.selectedContent}">

<f:selectItems value="#{user.countryItems}"></f:selectItems>

</h:selectOneMenu>

在上述代码中,方法绑定#{user.menuEvent}引用 form bean 的 menuEvent 方法,当用户

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

186

从菜单中做出选择之后,onchange 属性将会被激活,从而将 form 提交到 Server 端,JSF 实

现就会调用 user 的 menuEvent 方法来响应这个事件。

下面的代码演示的是动作事件监听器。

<h:commandButton value="Login" action="login" id="btnSubmitAdd"

actionListener="#{user.login}" />

当按钮被点击时,JSF 页面会提交到 Server 端,在 JSF 的第 5 个阶段会调用 user 这个

ManangedBean 的 login,实现了对这个事件的响应。

JSF 支持 3 种类型的事件:

值改变事件;

动作事件;

阶段事件。

当输入组件的值发生变化并且提交表单时,值变化事件由该输入组件触发,这些组件

有<h:inputText>、<h:selectRadio>、<h:selectManyMenu>。

动作事件在组件被激活时由命令组件触发,如按钮、超链接等,当激活按钮或链接时,

命令组件(如<h:commandButton>和<h:commandLink>)会触发动作事件。动作事件在调用

应用程序阶段触发,接近生命周期的末端。

通常将动作监听器连接到 JSF 页面的命令组件,例如,可以将一个动作监听器添加到

一个按钮上。

当命令组件被激活时,它们提交请求,因此不需要使用 onchange 来强迫提交表单,这

与值变化事件中所做的有所不同,当激活一个命令或连接时,封闭的表单将被提交,然后

JSF 实现将触发动作事件。

认识 action listener 和 action 之间的区别很重要,动作被设计用于业务逻辑并参与导航

处理,而动作监听器通常执行用户页面逻辑,但不参与导航处理。当动作需要用户界面相

关的信息时,动作监听器通常与动作一起工作。

4.4.4.1 值变化事件

任何收集用户输入的组件,比如 HmtlInputText 或者 HtmlSelectManyCheckbox,都要产

生值改变事件。如果需要在事件被产生时执行监听器,则可以将值改变事件监听器注册为

后台 bean 的一个方法。比如:

<h:inputText valueChangeListener="#{editUserForm.namechanged}">

</h:inputText>

这样就声明了关联到 editUserForm.name 属性的 HtmlInputText 组件,而 UserBean.

nameChanged 方法将在控件的值改变时被调用。

如果有人编写了值改变监听器类而不是后台 bean 方法,则可这样注册它:

CHAPTER 第 4 章 JSF 介绍

187

4

<h:selectOneListbox value="#{preferences.userLocale}">

<f:selectItems value="#{locales}"/>

<f:valueChangeListener type="com.custom.LocaleChangeListener"/>

</h:selectOneListbox>

这样声明了关联到 preferences.favoriteColors 属性的 HtmlSelectManyCheckbox 组件。用

户将看到所有被 preferences.Colors 返回的颜色值,并显示为一系列复选框。当用户选择一

套不同的颜色时,值改变监听器 ColorChangeListener 方法就会被执行。

4.4.4.2 动作事件

Command 组件族中的组件(HtmlCommandButton 和 HtmlCommandLink)会产生动作

事件。如果需要在用户单击它们(或者某些第三方动作源组件)时执行某个操作,则可以

在后台 bean 中注册动作监听器,这非常类似于值改变监听器方法的注册。

<h:commandButton value="Update"

actionListener="#{editPartForm.updatePrice}"></h:commandButton>

这样就声明了标注为“Update”的 HtmlCommandButton 组件。当用户单击这个按钮时,

它将执行后台 bean 方法 editPartForm.updatePrice,并重新显示当前视图。

就像值改变监听器一样,我们也可以注册单独的动作监听器类:

<h:commandButton value="Next >>" action="next" >

<f:actionListener type="myapp.NextPageListener"/>

<f:actionListener type="myapp.ClickListener"/>

</h:commandButton>

这样就以文本标注“Next>>”声明了 HtmlCommandLink 组件。用户单击这个链接时,

NextPageListener 和 NavigationListener 都会被执行。我们可以注册 0 个或者多个动作监听器

类,但是只可注册一个动作监听器方法。

如前所述,可以通过注册动作方法而不是动作监听器方法来控制导航:

<h:commandButton value="Next >>" action="#{wizard.nextPage}" >

</h:commandButton>

在这个例子中,用户单击该按钮时,动作方法 wizard.nextPage 被执行,但是动作方法

的逻辑结果用来决定接下来要装入的页面。记住,也可以硬编码结果,比如:

<h:commandButton value="Next >>" action="next" ></h:commandButton>

这个动作不执行任何动作监听器,但其结果“next”将被用来选择下一个页面。

如果有许多流程需要执行,则可以综合使用动作监听器方法、动作监听器类及动作方

法(或者硬编码的动作属性)。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

188

<h:commandButton value="Next >>" action="#{wizard.nextPage}"

actionListener="#{wizard.calculateListener}" >

<f:actionListener type="myapp.NextPageListener"/>

<f:actionListener type="myapp.AuditListener"/>

</h:commandButton>

用户单击这个链接时,有 4 个动作监听器会被执行:动作方法、动作监听器方法和两

个动作监听器类。通常,注册这么多监听器可能不是特别快捷,但必要时,把这么多的功

能关联到特定的事件类型(这里是动作事件)将是十分强大的。

4.4.4.3 阶段事件

阶段事件是在每个阶段执行时产生的事件,对于阶段事件的监听可以有阶段前监听和

阶段后监听两种。对于阶段事件,我们在平常的开发中用得很少,下面来看一个例子:在

生命周期开始和结束时执行的一个简单的 PhaseListener。

package org.bluelight.ch04;

import javax.faces.event.PhaseEvent;

import javax.faces.event.PhaseId;

import javax.faces.event.PhaseListener;

public class LifeCyclePhaseListener implements PhaseListener {

/**

* generated UID

*/

private static final long serialVersionUID = 5856684181896382565L;

public void afterPhase(PhaseEvent pe) {

System.out.println("After-" + pe.getPhaseId());

if (PhaseId.RENDER_RESPONSE.equals(pe.getPhaseId()))

System.out.println("Process JSF request done.");

}

public void beforePhase(PhaseEvent pe) {

if (PhaseId.RESTORE_VIEW.equals(pe.getPhaseId()))

System.out.println("Process a new JSF request");

CHAPTER 第 4 章 JSF 介绍

189

4

System.out.println("Before-" + pe.getPhaseId());

}

public PhaseId getPhaseId() {

return PhaseId.ANY_PHASE;

}

}

在上面代码的 后一个方法 getPhaseId()中,注册这个 PhaseListener 在 JSF 生命周期中

的哪个阶段运行,在这里设置为 PhaseId.ANY_PHASE,说明每个阶段都要执行这个

PhaseListener。此外,还需要在 faces-config.xml 中配置这个 PhaseListener:

<lifecycle>

<phase-listener>org.bluelight.ch04.LifeCyclePhaseListener

</phase-listener>

</lifecycle>

在第一次进入页面时,控制台会打印如图 4-25 所示的信息。

图 4-25 控制台信息(一)

当单击 Submit 按钮提交页面时,控制台又会打印如图 4-26 所示的信息。

图 4-26 控制台信息(二)

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

190

注意:

在第一个请求 (non-postback),只有两个阶段,即 RESTORE_VIEW 和

RENDER_RESPONSE,这说明 JSF 只是创建 UI component tree,然后显示这个页

面。在下一步中点击按钮,然后查看打印出的消息,这次 JSF 请求走过了整个 JSF

生命周期。注意,按钮点击的动作事件在 INVOKE_APPLICATION 阶段处理……

4.4.5 JSF 表达式语言

JSF(Java Server Faces)提供了一种在 Web 应用程序页面中使用的表达式语言 (JSF

EL),用来访问位于页面 Bean、其他与 Web 应用程序关联的 Bean(如会话 Bean 和应用

程序 Bean)中的 JavaBeans 组件,以及 JSF 中特有的 ManangedBean。

JSF EL 可用于将 JavaBeans 绑定到组件属性,以简化组件从各种源访问数据的

方式。JSF EL 表达式使用语法 #{expr},与 JSP 的表达式语言用${}分割的情况略有

不同,值绑定表达式的语法与 JSP 2.0 中定义的表达式语言的语法类似,但是具有以下

不同之处:

值绑定表达式的表达式分隔符是 #{ 和 },而不是 ${ 和 }。

值绑定表达式不支持 JSP 表达式语言函数。

除了在分隔符方面存在区别外,这两种表达式类型在以下语义方面也有所不同:

在呈现期间,值绑定表达式由 JSF 实现(通过调用 getValue 方法)进行计算,而

不是由页面的编译代码进行计算。

即使没有出现页面,也可以采用编程方式计算值绑定表达式。

值绑定表达式计算通过当前 Web 应用程序的 Application 对象,利用已配置的

VariableResolver 和 PropertyResolver 对象的可用功能,应用程序可以为其提供能

够带来额外功能的插件替换类。

如果值绑定表达式用于 EditableValueHolder 组件(任何输入字段组件)的值属性,

则该表达式可用于修改引用的值,而不是在请求处理生命周期的“更新模型值”阶

段对其进行检索。

4.4.5.1 类型

表达式语言定义了以下类型:

布尔型:true 和 false;

整型:与 Java 中一样;

浮点型:与 Java 中一样;

字符串型:使用单引号和双引号;“转义为\”,‘转义为\’,而 \ 转义为\\;

空:null。

CHAPTER 第 4 章 JSF 介绍

191

4

4.4.5.2 运算符

除了.和 [] 运算符(后面进行讨论)外,表达式语言还提供了以下运算符:

算术运算符:+、-(二元)、*、/、div、%、mod、-(一元);

逻辑运算符:and、&&、or、||、not、!;

关系运算符:==、eq、!=、ne、<、lt、>、gt、<=、ge、>=、le,可以与其他值、

布尔型、字符串型、整型或浮点型相比较;

空运算符:empty,该运算符可以用来确定值是否为 null 或空的前缀运算;

条件运算符:A ?B:C,表示计算 B 或 C,具体情况取决于 A 的计算结果。

按从高到低、从左到右的顺序排列的运算符优先级如下所示:

[].

()(可以更改运算符的优先级)

-(一元) not ! empty

* / div % mod

+ -(二元)

< > <= >= lt gt le ge

== != eq ne

&& and

|| or

? :

4.4.5.3 保留字

以下单词是为表达式语言而保留的,不能用做标识符:

and false le not

div ge lt Null

empty gt mod or

eq instanceof ne true

4.4.5.4 获取值语义

当调用 ValueBinding 实例的 getValue 方法(例如,在页面呈现期间计算 JSP 标记属

性上的表达式,并计算该表达式)时,将返回计算的结果。计算方法如下:

JSF 表达式语言统一了对 . 和 [] 运算符的处理方式。expr-a.expr-b 等同于

a[“expr-b”],即表达式 expr-b 用于构造其值为标识符的类型,然后 [] 运算符将

与该值一起使用,

由 VariableResolver 实例计算表达式中 左侧的标识符,该实例是从此 Web 应用

程序的 Application 实例中获得的。如果位于 . 或 [] 运算符左侧的值为 RowSet,

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

192

则右侧的对象将被视为列名称。

由 PropertyResolver 实例计算表达式中 . 或 [...] 运算符的每个实例,

PropertyResolver 是从此 Web 应用程序的 Application 实例中获得的。

使用 . 运算符来访问变量的属性,然后可以随意进行嵌套。

4.4.5.5 隐式对象

表达式语言定义了一组隐式对象:

FacesContext —— 当前请求的 FacesContext 实例;

param —— 将请求参数名称映射到单个值中;

paramValues —— 将请求参数名称映射到值的数组中;

header —— 将请求标题名称映射到单个值中;

headerValues —— 将请求标题名称映射到值的数组中;

cookie —— 将 cookie 名称映射到单个 cookie 中;

initParam —— 将上下文初始化参数名称映射到单个值中;

允许访问各种范围的变量的对象如下:

requestScope —— 将请求范围的变量名称映射到它们的值中;

sessionScope —— 将会话范围的变量名称映射到它们的值中;

applicationScope —— 将应用程序范围的变量名称映射到它们的值中。

从范围上看,JSF 表达式语言允许访问的变量范围与 JSP 相比缺少了一个 PageScope,

因为 JSF 表达式语言是在渲染阶段就已经完成了表达式的计算并产生输出,因此缺少这个

范围。但是在实际应用的开发中会发现,PageScope 还是非常需要的,因此 Seam 中就推出

了 PageScope 范围,有关这个范围的详情请参照第 7 章“Seam 入门”。

当表达式按名称引用其中一个对象时,将返回相应的对象。隐式对象比同名属性的优

先级要高。例如,即使现有 facesContext 属性包含一些其他值,#{facesContext}也将返回

FacesContext 对象。

4.5 JSF 与 Spring 结合

在当前的 Web 开发中,Spring 是目前比较常用的框架,那么如何将 Spring 和 JSF 两种

框架集成在一起呢?这一节中主要讨论这个问题。

集成原理是获得彼此的上下文引用,以此进一步获得各自管理的 bean。这是可

能的,因为二者都是 Web 应用框架遵循 Servlet 规范,这就为二者整合提供了可能和

基础。

CHAPTER 第 4 章 JSF 介绍

193

4

在 Spring 中 ApplicationContext 是相当重要的类,对于 Web 应用,它还包装了 javax.

servlet.ServletContext,为 Web 应用提供了所有可以利用的数据,包括可管理 Bean。JSF 中通

过 FacesContext 类可以获得所有可以利用的资源,同样包括了 JSF 的可管理支持 Bean,它

们都围绕着 ServletContext 提供了自己的门面,通过各自的门面在 Servlet 容器的世界里彼

此相通,因而提供了集成的可能性。

JSF 本身对于 ManagedBean 的管理有依赖注入的功能,不过 Spring 的 IoC 容器能提

供更多的功能。另外,Spring 还有 AOP 框架,在事务处理上也可提供协助 Web 应用的

开发。

JSF 和 Spring 结合,主要目的就是让 Spring 的 Bean 名称可以被 JSF <managed-bean-name>

标签上的名称来使用。也就是说,当 JSF 定义文件中根据名称要求 Bean 实例时,若 JSF 在自

己的 Context 中找不到名称,则可以尝试到 Spring 的 Context 中去查找。

下面我们通过一个例子来演示如何让 JSF 识别 Spring 的管理对象。

UserManager.java(模拟一个业务类):

public class UserManager ...{

public String getChangedName(String name)...{

return "hello "+name;

}

}

User.java:

public class User ...{

private String name;

private UserManager userManager;

public String getName() ...{

return name;

}

public void setName(String name) ...{

this.name = userManager.getChangedName(name);

}

public UserManager getUserManager() ...{

return userManager;

}

public void setUserManager(UserManager userManager) ...{

this.userManager = userManager;

}

}

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

194

JSF 配置文件:

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE faces-config PUBLIC

"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN"

"http://java.sun.com/dtd/web-facesconfig_1_0.dtd">

<faces-config>

<application>

<variable-resolver>

org.springframework.web.jsf.DelegatingVariableResolver

</variable-resolver>

</application>

<navigation-rule>

<from-view-id>/index.jsp</from-view-id>

<navigation-case>

<from-outcome>login</from-outcome>

<to-view-id>/welcome.jsp</to-view-id>

</navigation-case>

</navigation-rule>

<managed-bean>

<managed-bean-name>user</managed-bean-name>

<managed-bean-class>User</managed-bean-class>

<managed-bean-scope>session</managed-bean-scope>

<managed-property>

<property-name>userManager</property-name>

<value>#{userManager}</value>

</managed-property>

</managed-bean>

</faces-config>

Spring 配置文件:

<?xml version="1.0" encoding="UTF-8"?>

<beans

xmlns=http://www.springframework.org/schema/beans

xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance

xsi:schemaLocation="http://www.springframework.org/schema/beans

CHAPTER 第 4 章 JSF 介绍

195

4

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="userManager" class="UserManager"/>

</beans>

实际上,在 JSF 的定义文件中并没有定义 userManager 的 Bean 实例,当 JSF 找不到

userManager 时,会使用 DelegatingVariableResolver 对象在 Spring 的 Context 中寻找同名的

Bean 实例。

web.xml :

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.

w3.org/2001/XMLSchema-instance" version="2.4" xsi:schemaLocation="http:

//java.sun.com/xml/ns/j2eehttp://java.sun.com/xml/ns/ j2ee/web-app_

2_4.xsd">

<context-param>

<param-name>contextConfigLocation</param-name>

<param-value>/WEB-INF/mvc-config.xml</param-value>

</context-param>

<listener>

<listener-class>

org.springframework.web.context.ContextLoaderListener

</listener-class>

</listener>

<servlet>

<servlet-name>Faces Servlet</servlet-name>

<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>

<load-on-startup>1</load-on-startup>

</servlet>

<servlet-mapping>

<servlet-name>Faces Servlet</servlet-name>

<url-pattern>*.faces</url-pattern>

</servlet-mapping>

</web-app>

index.jsp:

<%@ page language="java" pageEncoding="GB18030"%>

<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

196

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>

<html>

<head>

</head>

<body>

<f:view>

<h:form>

name:<h:inputText value="#{user.name}"></h:inputText>

<h:commandButton value="csubmit" action="login">

</h:commandButton>

</h:form>

</f:view>

</body>

</html>

welcome.jsp:

<%@ page language="java" pageEncoding="GB18030"%>

<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>

<html>

<head>

</head>

<body>

<f:view>

<h:outputText value="#{user.name}"></h:outputText>

</f:view>

</body>

</html>

运行 index.faces,JSF 会自动跳转到 index.jsp。

4.6 JSF 解决方案

4.6.1 DataTable 分页

在 JSF 的各个组件中,DataTable 组件无疑是应用比较频繁的组件,同时也是 受重视

的组件,因为这个表格一般和数据库中的数据打交道,关系着应用系统的性能。而在

CHAPTER 第 4 章 JSF 介绍

197

4

DataTable 组件中,分页机制是 受重视的一部分,可惜的是,标准的 JSF 规范中并没有提

到,而目前的 JSF 实现中的分页机制处理得又不够理想。因此,在这一节中提出了一个分

页解决办法。

对于大多数 Web 应用,分页都是必不可少的功能,当然在 JSF 中也一样。JSF 中的分

页机制目前主要有两种:一种是基于 Session 的分页机制,是将 DataTable 对应的数据全部

加载到内存中,然后在内存中分页;另一种就是结合数据库中的数据分页,每一次只读取

当前页的数据。下面分两个部分来分别讨论这两种机制。

4.6.2 一般分页

MyFaces 是 Apache 基金会中的一个一级项目,除了实现 JSF 标准外,还做了很多的扩

展工作。在 MyFaces 包中有一个扩展包 Tomahawk,我们将主要使用其中的两个 Component

实现分页:一个是<t:dataTable>,另一个是<t:dataScroller>。

下面的例子来自于 MyFaces-Sample,这里省去了其中和分页逻辑无关的内容,

详细的例子可以下载 MyFaces-Sample 包或者访问 http://www.irian.at/myfaces/home.jsf

查看。

第一部分:dataTable

<t:dataTable id="data" var="car" value="#{pagedSort.cars}" rows="10">

……

</t:dataTable>

在这一部分中,dataTable 绑定了一个 Backing Bean - pagedSort 中的 cars 属性,我们可

以在这个属性中加入数据访问逻辑,从数据库或者其他来源取得用于显示的数据。比如,

可以通过 Hibernate 获取一个 List,其中包含有用于显示的 POJOs。

注意:

dataTable 中的 rows 属性指的是每页的行数,是必须指定的;否则无法进

行分页。如果在项目中使用固定行数的分页,则建议把这个值写在

BaseBackingBean 中,并暴露一个 property 供页面调用,所以每次在页面中就

可以这么写:

#{backingBean.pageSize}

第二部分:dataScroller

<t:dataScroller id="scroll_1"

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

198

for="data"

fastStep="10"

paginator="true"

paginatorMaxPages="9">

<f:facet name="first" >

<t:graphicImage url="images/arrow-first.gif" border="1" />

</f:facet>

<f:facet name="last">

<t:graphicImage url="images/arrow-last.gif" border="1" />

</f:facet>

<f:facet name="previous">

<t:graphicImage url="images/arrow-previous.gif" border="1" />

</f:facet>

<f:facet name="next">

<t:graphicImage url="images/arrow-next.gif" border="1" />

</f:facet>

<f:facet name="fastforward">

<t:graphicImage url="images/arrow-ff.gif" border="1" />

</f:facet>

<f:facet name="fastrewind">

<t:graphicImage url="images/arrow-fr.gif" border="1" />

</f:facet>

</t:dataScroller>

这里定义了用于分页的<t:dataScroller>, 主要的是配置该分页 Component 针对哪个

dataTable 进行分页的“for”属性,该属性与 dataTable 绑定,并对其进行分页。在这里,

绑定了第一部分中的 id=“data”的 dataTable。下面有很多的<t:facet>是指定分页导航样式

的,这里使用了图片作为导航,可以把它们改成文字形式的导航。

当然这只是 简单,也是一种不推荐的分页方式,因为在每次进行分页的时候,将

会从数据库中取回所有的记录放入 List 中,然后 dataScroller 再对这个 List 进行分页。

如果在数据量很大的情况下,这种方式显然是不符合要求的,假设每条记录占用 1KB

内存,数据库中有 100 万条记录,每次要把这个 List 全部读取出来将占用 1GB 内存。

我们需要一种 On-DemandLoading 方式的读取,也就是只在需要查看某页的时候读取该

页的数据。

另外,JSF 的生命周期中有多个阶段会调用到#{pagedSort.cars}中对应的方法,如果在

这里调用了数据访问逻辑,就会在只显示一次页面的情况下进行多次数据库操作,也是相

当耗费资源的。

CHAPTER 第 4 章 JSF 介绍

199

4

所以我们需要有更好的分页方式去解决以上问题。下面将介绍另一种方式以改善这些

问题。

4.6.3 On-Demand 分页

前面直接使用了 MyFaces 中的两个 Component 完成了一个简单的分页,这里

将会介绍以 On-Demand Loading 方式来进行分页,仅仅在需要数据时加载。这种方

式就是不对 dataTable 和 dataScrollor 做任何修改,仅仅通过扩展 DataModel 来实现

分页。

DataModel 是一个抽象类,用于封装各种类型的数据源和数据对象的访问,JSF 中

dataTable 绑定的数据实际上被包装成了一个 DataModel,以消除各种不同数据源和数据

类型的复杂性。一般情况下,在代码中访问数据库并拿到了一个 List,交给 dataTable,

这时,JSF 会将这个 List 包装成 ListDataModel,dataTable 访问数据都是通过这个

DataModel 进行的,而不是直接使用 List。

然后,我们将所需的页的数据封装到一个 DataPage 中,这个类表示我们所需的一页的

数据,里面包含有 3 个元素:datasetSize、startRow 和一个用于表示具体数据的 List。

datasetSize 表示这个记录集的总条数,查询数据时,使用同样的条件取 count 即可;startRow

表示该页的起始行在数据库中所有记录集中的位置。

/**

* * A simple class that represents a "page" of data out of a longer set, ie a *

* list of objects together with in*fo to indicate the starting row and the full *

* size of the dataset. EJBs can return instances of this type * when returning

*

* subsets of available data

*/

public class DataPage {

private int datasetSize;

private int startRow;

private List data;

public DataPage(int datasetSize, int startRow, List data) {

this.datasetSize = datasetSize;

this.startRow = startRow;

this.data = data;

}

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

200

public int getDatasetSize() {

return datasetSize;

}

public int getStartRow() {

return startRow;

}

public List getData() {

return data;

}

}

接下来,我们要对 DataModel 进行封装,以达到分页的要求。该 DataModel 仅仅持有

一页的数据 DataPage,并在适当时加载数据,读取我们所需的页的数据。

public abstract class PagedListDataModel extends DataModel {

int pageSize;

int rowIndex;

DataPage page;

/**

* Create a datamodel that pages through the data showing the specified

* number of rows on each page.

*/

public PagedListDataModel(int pageSize) {

super();

this.pageSize = pageSize;

this.rowIndex = -1;

this.page = null;

}

public void setWrappedData(Object o) {

if (o instanceof DataPage) {

this.page = (DataPage) o;

} else {

throw new UnsupportedOperationException(" setWrappedData ");

}

}

CHAPTER 第 4 章 JSF 介绍

201

4

public int getRowIndex() {

return rowIndex;

}

public void setRowIndex(int index) {

rowIndex = index;

}

public int getRowCount() {

return getPage().getDatasetSize();

}

private DataPage getPage() {

if (page != null) {

return page;

}

int rowIndex = getRowIndex();

int startRow = rowIndex;

if (rowIndex == -1) {

// even when no row is selected, we still need a page

// object so that we know the amount of data available.

startRow = 0;

}

// invoke method on enclosing class

page = fetchPage(startRow, pageSize);

return page;

}

public Object getRowData() {

if (rowIndex < 0) {

throw new IllegalArgumentException(

" Invalid rowIndex for PagedListDataModel; not within page ");

}

// ensure page exists; if rowIndex is beyond dataset size, then

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

202

// we should still get back a DataPage object with the dataset size

// in it

if (page == null) {

page = fetchPage(rowIndex, pageSize);

}

int datasetSize = page.getDatasetSize();

int startRow = page.getStartRow();

int nRows = page.getData().size();

int endRow = startRow + nRows;

if (rowIndex >= datasetSize) {

throw new IllegalArgumentException(" Invalid rowIndex ");

}

if (rowIndex < startRow) {

page = fetchPage(rowIndex, pageSize);

startRow = page.getStartRow();

} else if (rowIndex >= endRow) {

page = fetchPage(rowIndex, pageSize);

startRow = page.getStartRow();

}

return page.getData().get(rowIndex - startRow);

}

public Object getWrappedData() {

return page.getData();

}

public boolean isRowAvailable() {

DataPage page = getPage();

if (page == null) {

return false;

}

int rowIndex = getRowIndex();

if (rowIndex < 0) {

return false;

} else if (rowIndex >= page.getDatasetSize()) {

CHAPTER 第 4 章 JSF 介绍

203

4

return false;

} else {

return true;

}

}

public abstract DataPage fetchPage(int startRow, int pageSize);

}

后,需要在Backing Bean中加一些内容,调用业务逻辑,并将数据交给PagedListDataModel,

来帮助我们完成 后的分页工作。

public class SomeManagedBean {

private DataPage getDataPage( int startRow, int pageSize) {

// access database here, or call EJB to do so

}

public DataModel getDataModel() {

if(dataModel==null ) {

dataModel=new LocalDataModel(20);

}

return dataModel;

}

private class LocalDataModel extends PagedListDataModel {

public LocalDataModel( int pageSize) {

super (pageSize);

}

public DataPage fetchPage( int startRow, int pageSize) {

// call enclosing managed bean method to fetch the data

return getDataPage(startRow, pageSize);

}

}

这里面有一个 getDataPage 方法,只需要把所有业务逻辑的调用放在这里就可

以了, 后业务逻辑调用的结果返回一个 List,总条数返回一个 int 型的 count 放到

DataPage 中。

为了实现复用,把上面第 3 段代码中的 LocalDataModel 类和 getDataPage 方法抽到

BasePaged BackingBean 中,把 getDataPage 方法改成:

protected abstract DataPage getDataPage(int startRow, int pageSize);

这样所有需要分页的 Backing Bean 继承自这个抽象类,并实现 getDataPage 方法即可

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

204

很容易地实现分页。

在具体应用中可以这么写:

protected DataPage getDataPage( int startRow, int pageSize){

List scheduleList =

scheduleService.getSchedulesByDate(scheduleDate,startRow,pageSize);

int dataSetSize =

scheduleService.getSchedulesCountByDate(scheduleDate);

return new DataPage(dataSetSize, startRow, scheduleList);

}

在数据访问中,只需要取出我们所需行数的记录就可以了,这在 Hibernate 中非常容易

实现。

如果使用 Criteria 查询,则只要加上:

criteria.setFirstResult(startRow);

criteria.setMaxResults(pageSize);

如果使用 Query 查询,则只要加上:

query.setFirstResult(startRow);

query.setMaxResults(pageSize);

并把两个参数传入即可。

我们需要另外写一个 count 的 DAO,取出相同查询条件的记录条数。

还需要修改 Backing Bean 中与 dataTable 绑定的 property,将返回类型由 List 改成

DataModel,而页面则不需要做任何修改就可以满足新的需求了。

其中 重要的是 PagedListDataModel 中 fetchPage 这个方法,当满足取数据的条件

时,都会调用它取数据。因为业务逻辑不同,不便于将业务逻辑的调用放在里面实现,于

是将其作为抽象方法,将具体的实现放到具体的 Backing Bean 中进行。在 BaseBackingBean

中,实现了这个方法,调用了 getDataPage(startRow,pageSize)方法,而在 BaseBackingBean

中,这个方法又推迟到更具体的页面中实现,这样,在具体的页面中只需要实现一个

getDataPage(startRow, pageSize)方法访问业务逻辑即可。

大功告成,这个实现把前面遇到的两个问题都解决了,On-Demand Loading 是没有问题

的,因为只有在首次读取和换页时 DataModel 才会向数据库请求数据。虽然在 JSF 的生命周

期中多次调用与 dataTable 绑定的方法,但是因为每次业务逻辑请求以后,数据都会存放在

DataPage 中,如果里面的数据满足需求的话,就不再请求访问数据库了,这样多次访问数据

库的问题也就解决了。

CHAPTER 第 4 章 JSF 介绍

205

4

4.6.4 Exception 统一处理

4.6.4.1 目标

解析错误信息,使用 Globalization 来显示消息。

4.6.4.2 分析

JSF 中有两类 error:一类是 input error,是由用户输入错误的信息造成的,一般在 input

验证或者转化过程中出现;另一类是 application error,是系统中可能出现的各种错误,如

SQLException、NullPointerException 等。对于 application error,在系统中会统一抛出

AppException,然后由我们自己定制的 ActionListener 处理;而对于 input error,则由 JSF

处理。下面详细讨论如何处理 application Error。

在系统中我们采用两种策略共同处理 Exception:当 JSF 请求处理中出现 Exception 时,

在这个页面上显示一条消息提示用户;当非 JSF 请求出现 Exception 时,将这个 Exception

抛给 Web Container,并将页面导航到某一个错误页面。

4.6.4.3 实现

下面分别讨论这两种策略是如何实现的。

第一种策略:JSF 请求,弹出提示消息。

在我们的系统中,结合 Java Exception 和 FacesMessage 来充当 Exception 处理器,

当 Exception 抛出时,尤其是 Business Exception 发生时,这个 Exception 是发生在 JSF

的 InvokeAppication 阶段,因此通过定制 ActionListener 就可以捕获这个 Exception,并

将这个 Exception 转换为 FacesMessage,再根据 Exception 的 Message 到资源文件中加载

对应的国际化。

当 ActionListener 无法捕获这个 Exception 时,将这个错误发送到 WebContainer,

并转向 error 页面。这时,对于这个请求的响应会产生 500 等 Error Code 并发送到客户

端。

因此,首先要定制 JSF 的 ActionListener,在其上添加以下方法:

try {

outcome = (String) methodBinding.invoke(facesContext, null);

} catch (Exception e) {

Throwable cause = e.getCause();

if (cause != null && cause instanceof AbortProcessingException) {

throw (AbortProcessingException) cause;

} else {

Exception ex = (Exception) e.getCause();

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

206

String message = ex.getMessage();

//get localized Message string from resource bundle

String localizedMessage = getBundleMessage(message);

if (null != localizedMessage && "".equals(localizedMessage)) {

throwNewFacesMessage(actionEvent.getComponent()

.getClientId(facesContext), localizedMessage,

FacesMessage.SEVERITY_ERROR);

} else {

throwNewFacesMessage(actionEvent.getComponent()

.getClientId(facesContext), message,

FacesMessage.SEVERITY_ERROR);

}

}// org.apache.myfaces.application.ActionListenerImpl

在上面的方法中拦截所有的 Exception,并转换为 FacesMessage,这样 JSF 会直接转向

Render 阶段,从而停止 Application method 的进一步处理,实现 Exception 的 handler。

然后,在页面上添加<h:messages>标签,就可以显示所有的错误消息。

后,覆盖 A4J.AJAX.onError,由于我们在系统中还采用了 RichFaces,RichFaces

在判断 Ajax 请求在服务器的处理出现错误后,会有一个默认的处理函数

A4J.AJAX.onError。这样当服务器端发送 500 等 Error Code 时,就可以被客户端的 Ajax

感知,并添加处理。

A4J.AJAX.onError = function(req,status,message) {

var report = "An error has occurred during the request,

Message is:" + message;

if(Ext.MessageBox.show){

Ext.MessageBox.show({

title: 'Error Report',

msg: report,

width:400,

buttons: Ext.MessageBox.OK

});

}else{

alert(report);

}

};

第二种策略:非 Ajax 请求,跳转到错误页面。

可以通过配置 web.xml 来实现:

CHAPTER 第 4 章 JSF 介绍

207

4

<error-page>

<exception-type>javax.faces.FacesException</exception-type>

<location>/error.jsp</location>

</error-page>

<error-page>

<error-code>500</error-code>

<location>/error.jsp</location>

</error-page>

这样,当产生错误时,就会转向这个页面。

示例:

// JSF Action

public String doAccept() {

Ecr curInitEcr = (Ecr) getContextVariable("#{curEcr}");

String statusMessage;

//if current Ecr is null or its uid is null or its

// CoordinatorEcrId is null

//then throw a Error message

if (curInitEcr == null || curInitEcr.getId() == null) {

statusMessage = getBundleMessage("Current_ECR_Not_Selected_Error");

setStatusBean(statusMessage, statusMessage);

//throwNewFacesErrorMessage("initECRTabsForm", this.statusMessage,

//FacesMessage.SEVERITY_INFO);

return null;

}

statusMessage = getBundleMessage("Current_ECR_Not_Selected_Error");

//在程序中抛出 Exception,这个 Exception将会被 ActionLister拦截并转换成

//FacesMessage,从而停止运行

if (null == statusMessage){

throw new AppException("Current_ECR_Not_Selected_Error");

}

//下面的代码在发生 Exception时将不会执行

Person person = this.userManager.getLoginPerson();

long personId = person.getId().longValue();

this.inquiryECRManager.doAcceptInitialEcr(curInitEcr, personId);

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

208

4.6.5 Shale 框架验证

虽然 JSF 提供了强大的数据验证功能,但利用默认的 Validator 只能提供后台数据验

证功能,没有提供对用户更加友好的前台数据验证功能。不过没关系,我们可以利用 Shale

为 JSF 增加强大的前台数据验证功能。

Shale 是一个基于 JSF 的 Web 开发框架。Shale 重用了大量的 Struts 基础代码,因此可

以称 Struts 为它的“父”框架,但 Shale 是面向服务架构的,它与 Struts 的 大不同之处在

于:Struts 与 JSF 集成,而 Shale 则建立在 JSF 之上。Struts 实质上是一个巨大的、复杂的

请求处理器;而 Shale 则是一组可以以任何方式进行组合的服务。

对于 Shale 这里不过多介绍,Shale 框架目前还没有得到广泛的应用,但是 Shale 框架

中有一个验证机制却很值得我们使用,这就是 Shale 的验证器。这个 Validator 采用的是

Apache 的 Commons Validation,同时又添加了页面端的 JavaScript 验证,因此 Shale 的

Validator 既可以进行 Client 端验证,也允许在 Server 端进一步验证,因此是标准 JSF 验证

的有利补充。

在使用时,需要下载 shale-core.jar 和 commons-validator.jar 包,将这两个包文件放到

WEB-INF/lib 目录下。

然后修改页面代码:

<%@ taglib uri="http://struts.apache.org/shale/core" prefix="s"%>

<h:form onsubmit="return validateForm(this);">

<h:inputText id="creditCardNumber" size="16"

value="#{userContext.creditCardNumber}">

<s:commonsValidator type="required"

arg="#{msgs.creditCardNumberPrompt}" server="true" client="true" />

<s:commonsValidator type="mask" mask="[4-6].*"

arg="#{msgs.creditCardNumberPrompt}" server="true" client="true" />

<s:commonsValidator type="creditCard"

arg="#{msgs.creditCardNumberPrompt}" server="true">

</h:inputText>

<h:message for="creditCardNumber" styleClass="errors" />

<s:validatorScript functionName="validateForm" />

</h:form>

只要改变一下提交方式,然后增加 commonsValidator 组件和 validatorScript 组件就可

以实现前台数据验证功能了。

需要注意的是,这里容易遇到的一个问题是 commons-validator.jar 版本不对, 开始使

用的是 commons-validator.jar 的 1.1.4版本,会产生错误,换成 commons-validator.jar 的 1.2

版本就可以了。

CHAPTER 第 4 章 JSF 介绍

209

4

还有就是如果有几个提交按钮,而只需要对其中的部分提交进行验证的话,则只要改

变相应按钮的 onClick 事件就可以了,比如 onClick=“return validateForm(this.form)”。

具体的 <s:commonsValidator>中的 type 属性对应了 commons Validator 中的验证方法,

可以参考 commons Validator 官方网站的相关文档(http://jakarta.apache.org/ commons/

validator/index.html)。

4.7 本章小结

在这一章中,介绍了 JSF 的一些基础知识,如一些基本组件的使用,以及验证和转换

等。 后介绍了一些解决方案,这些解决方案都是笔者在实际应用中遇到的问题,并把相

应的解决方案也记录下来,希望读者,尤其是一些有 JSF 实际使用经验的开发人员,在阅

读 后一部分时,能够得到一些启发。

CHAPTER

5

5.1 Facelets 简介

Facelets 是用来建立 JSF 应用程序的一个可供选择的表现层技术。Facelets 提供了一个

强有力的模板化系统,允许 JSF 应用使用 HTML 样式的模板来定义 JSF 的表现层,减少了

组件与表现层整合时的冗余代码。本身也不需要 Web 容器支持(JSP 是需要 Servlet 容器支

持的)。在这一章中,我们将会学习 Facelets,了解 Facelets 的特性。

Facelets 的特性如下:

可以和 JSF 1.1 或 JSF 1.2 工作,包括 Sun RI 和 Apache MyFaces;

不需要开发定制的 UIComponents 标签;

支持 JSF 组件和页面的快速模板化/装饰;

可以在不同的文件中声明 UIComponent 树(UICompositions);

支持到行/标签/属性的精确错误报告;

可以在不同的文件中声明标签, 甚至可以包含在 jar 包中;

全面支持 EL 表达式,包括对函数的支持;

编译期间的 EL 表达式验证;

XML 配置文件也不是必须采用的;

提供了“jsfc”属性,可以和 Tapestry 的“jwcid”起相同作用(例如:<input id="bar"

type= "text" jsfc="h:inputText" value="#{foo.bar}"/>)

可插拔的装饰设计使设计者的工作更加简单(例如:在编译期将<input type="text"/>

转换到<h:inputText/>);

第 5 章

Facelets

CHAPTER 第 5 章 Facelets

211

5

可以和任何 RenderKit 结合工作;

Facelets 不依赖于 Web 容器。

基于这些特性,Facelets 很快受到广大 JSF 开发人员的喜爱,目前 Facelets 的使用正在

不断扩大。

5.2 配置 Facelets

5.2.1 下载

安装 Facelets 的步骤很容易。

下载 Facelets 发行包 并解压缩。

把 jsf-facelets.jar 复制到 WEB-INF/lib 目录下(在应用程序部署时,它 终必须放

在 WEB-INF/lib 目录中)。

把 Facelets 初始化参数添加到 web.xml 文件中。

把 FaceletViewHandler 添加到 faces-config.xml 文件中。

5.2.2 安装和配置

Facelets 是作为 JSF 的一个 ViewHandler 实现的,可以被简单地配置到应用程序中。使

用 Facelets 很简单,确保 Facelets jar(jsf-facelets.jar)在项目的 classpath 中,并且对

faces-config.xml 文件做一个简单的修改就可以了。

<faces-config>

<application>

<!--使用 Facelets -->

<view-handler>com.sun.facelets.FaceletViewHandler</view-handler>

</application>

</faces-config>

JavaServer Faces 默认使用 JSP 文件定义视图(*.jsp),我们需要在 WEB-INF/web.xml 中

修改该类型。

<context-param>

<param-name>javax.faces.DEFAULT_SUFFIX</param-name>

<param-value>.xhtml</param-value>

</context-param>

依照上面定义的 context-param,我们就可以开始写 Facelets 并且将它们保存为.xhtml

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

212

类型的文档。现在可以开始使用 Facelets 了。

5.2.3 Hello World 示例

依照学习语言的习惯,在深入学习 Facelets 之前,我们首先来学习一个关于 Hello World

的小例子。

建立一个 Web Project,要确保 lib 目录下有下列 jar 文件存在。

+- /lib

+- /jsf-facelets.jar

+- /el-api.jar

+- /el-ri.jar

[下面存放 JSF的具体实现,以 Sun RI为例]

+- /jsf-api.jar

+- /jsf-impl.jar

+- /commons-digester.jar

+- /commons-logging.jar

+- /commons-collections.jar

+- /commons-beanutils.jar

修改 WEB-INF 目录下的 web.xml 文件。修改为:

<web-app>

<!--Facelets使用的文件格式为*.xhtml -->

<context-param>

<param-name>javax.faces.DEFAULT_SUFFIX</param-name>

<param-value>.xhtml</param-value>

</context-param>

<!--在 Facelets开发阶段,设置 Debug输出-->

<context-param>

<param-name>facelets.DEVELOPMENT</param-name>

<param-value>true</param-value>

</context-param>

<!--方便调试,可选 -->

<context-param>

<param-name>com.sun.faces.validateXml</param-name>

<param-value>true</param-value>

</context-param>

<context-param>

<param-name>com.sun.faces.verifyObjects</param-name>

CHAPTER 第 5 章 Facelets

213

5

<param-value>true</param-value>

</context-param>

<!-- Faces Servlet -->

<servlet>

<servlet-name>Faces Servlet</servlet-name>

<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>

<load-on-startup>1</load-on-startup>

</servlet>

<!-- Faces Servlet Mapping -->

<servlet-mapping>

<servlet-name>Faces Servlet</servlet-name>

<url-pattern>*.jsf</url-pattern>

</servlet-mapping>

</web-app>

修改 faces-config.xml 文件,将默认的 ViewHandler 设置为 Facelets,以便让 Facelets

生成页面。

<faces-config>

<application>

<view-handler>

com.sun.facelets.FaceletViewHandler

</view-handler>

</application>

</faces-config>

通过上面的 3 个步骤已经把环境设置好了,下面我们开始编写后台代码。首先编写 JSF

的 ManagedBean。

package tutorial;

import java.io.Serializable;

import java.util.Random;

import javax.faces.application.FacesMessage;

import javax.faces.component.UIComponent;

import javax.faces.context.FacesContext;

import javax.faces.validator.ValidatorException;

public class NumberBean implements Serializable {

protected final static Random rand = new Random();

protected int min;

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

214

protected int max;

protected int guess;

protected int actual;

// Default Constructor

public NumberBean() {

this.min = 1;

this.max = 10;

}

// 这个方法用来验证用户输入

public void validate(FacesContext context, UIComponent component,

Object value) throws ValidatorException {

// 将输入值转为 int

try {

int param = Integer.parseInt(value.toString());

// 验证参数

if (param > this.max || param < this.min) {

FacesMessage msg = new FacesMessage

("猜测的数字必须位于 " + this.min+ "和" + this.max + "之间");

throw new ValidatorException(msg);

}

} catch (NumberFormatException e) {

FacesMessage msg = new FacesMessage("必须为数字");

throw new ValidatorException(msg);

}

}

// lazy generate our actual value

public synchronized int getActual() {

if (this.actual == 0) {

this.actual = rand.nextInt(this.max - this.min);

this.actual += this.min;

}

return this.actual;

}

// our message for the response

public String getMessage() {

CHAPTER 第 5 章 Facelets

215

5

if (this.guess == this.getActual()) {

return "太棒了, 你猜对了!";

} else if (this.guess < this.getActual()) {

return "再往大一点猜猜看";

} else {

return "再往小一点猜猜看";

}

}

// other bean properties

public int getMin() {

return this.min;

}

public int getMax() {

return this.max;

}

public int getGuess() {

return this.guess;

}

public void setMin(int min) {

this.min = min;

}

public void setMax(int max) {

this.max = max;

}

public void setGuess(int guess) {

this.guess = guess;

}

}

我们将这个类注册为 ManagedBean,下面是 faces-config.xml 的代码。

<faces-config>

<!-- from project setup -->

<application>

<view-handler>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

216

com.sun.facelets.FaceletViewHandler

</view-handler>

</application>

<!--刚刚添加的 NumberBean -->

<managed-bean>

<managed-bean-name>NumberBean</managed-bean-name>

<managed-bean-class>tutorial.NumberBean</managed-bean-class>

<managed-bean-scope>session</managed-bean-scope>

<managed-property>

<property-name>min</property-name>

<value>1</value>

</managed-property>

<managed-property>

<property-name>max</property-name>

<value>10</value>

</managed-property>

</managed-bean>

</faces-config>

我们将 NumberBean 设置为 ManagedBean,并初始化了 min 和 max 的值。下面开始编

写页面代码。

我们刚才讲过,Facelets 的一个显著优势就是模板化,因此在这里也要先制作一个模板

文件 template.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>

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

<title>Facelets: Hello World 例子</title>

<style type="text/css">

<!--

body {

font-family: Verdana, Arial, Helvetica, sans-serif;

font-size: small;

}

-->

</style>

</head>

CHAPTER 第 5 章 Facelets

217

5

<body>

<!-- 下面是模板的第一个插入点 -->

<h1><ui:insert name="title">Default Title</ui:insert></h1>

<!-- 下面是模板的第二个插入点 -->

<p><ui:insert name="body">Default Body</ui:insert></p>

</body>

</html>

在这个模板文件中,我们用<ui:insert>标签定义了 title 和 body 两个插入点,表示以后

可以在这个点动态插入内容。

有了模板后,我们开始制作第一个页面。

<!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">

<body>

文字上面的部分将不会被显示

<!-- 指定模板 -->

<ui:composition template="/template.xhtml">

本段文字也将不会被显示

<!-- 定义一个片段,名字为 title -->

<ui:define name="title">

我正在想一个数字,这个数字在 #{NumberBean.min} 和 #{NumberBean.max}之间,

你能猜得到吗?

</ui:define>

本段文字也将不会被显示

<!-- 定义一个片段,名字为 body -->

<ui:define name="body">

<h:form id="helloForm">

<h:inputText type="text" id="userNo" value="#

{NumberBean.guess}"

validator="#{NumberBean.validate}" />

<br />

<h:commandButton type="submit" id="submit" action="success"

value="Submit" />

<br />

<h:message showSummary="true" showDetail="false"

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

218

style="color: red; fontweight:bold;"

id="errors1" for="userNo" />

</h:form>

</ui:define>

本段文字将不会被显示

</ui:composition>

本段文字也将不会被显示

</body>

</html>

在这个页面中,我们首先用<ui:composition template="/template.xhtml">表示,使用

template.xhtml 作为模板文件;随后,又定义了两个 ui 片段,分别命名为 title 和 body,表

示向模板文件中插入这两个片段中的内容,其他的部分忽略。

下面我们开始编写响应页面。

<!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">

<body>

<!-- 指定模板 -->

<ui:composition template="/template.xhtml">

<!-- 定义一个片段,名字为 title -->

<ui:define name="title">

#{NumberBean.message}

</ui:define>

<!-- 定义一个片段,名字为 body -->

<ui:define name="body">

<!-- 注意:这里使用 jsfc来定义一个 JSF 表单 -->

<form jsfc="h:form">

<input jsfc="h:commandButton" type="submit" id="back"

value="Back" action="success"/>

</form>

</ui:define>

</ui:composition>

</body>

</html>

CHAPTER 第 5 章 Facelets

219

5

在页面编写完成后,我们开始处理导航。

<faces-config>

<application>

<view-handler> com.sun.facelets.FaceletViewHandler</view-handler>

</application>

<!-- our NumberBean we created before -->

<managed-bean>

<managed-bean-name>NumberBean</managed-bean-name>

<managed-bean-class>tutorial.NumberBean

</managed-bean-class>

<managed-bean-scope>session</managed-bean-scope>

<managed-property>

<property-name>min</property-name>

<value>1</value>

</managed-property>

<managed-property>

<property-name>max</property-name>

<value>10</value>

</managed-property>

</managed-bean>

<!-- going from guess.xhtml to response.xhtml -->

<navigation-rule>

<from-view-id>/guess.xhtml</from-view-id>

<navigation-case>

<from-outcome>success</from-outcome>

<to-view-id>/response.xhtml</to-view-id>

</navigation-case>

</navigation-rule>

<!-- going from response.xhtml to guess.xhtml -->

<navigation-rule>

<from-view-id>/response.xhtml</from-view-id>

<navigation-case>

<from-outcome>success</from-outcome>

<to-view-id>/guess.xhtml</to-view-id>

</navigation-case>

</navigation-rule>

</faces-config>

在编写完成导航后,我们就可以部署运行这个系统了。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

220

5.3 Facelets 模板和扩展机制

5.3.1 UI Component 和 UIInsert

Facelets 是一个强大的模板化系统,它能让你用 HTML 风格的模板来定义 JSF 视图,

并能够减少那些适合于集成在视图中的组件的代码数量,而且不需要 Web 容器。

对于任何一种想要成功的视图技术来说,它必须拥有一些创建模板和重用的功能,而

且必须好用又易懂。对于 JSF 来说,Facelets 技术完美地解决了这个问题,并且延续了传统

的、基于标签的 UI 风格。

当人们第一次开始创建网页的时候,经常发现自己在多个文件中重复相同的内容。作

为一个开发人员,当开始用面向对象的思想来开发时,这样的重复劳动会让人灰心丧气。

把这些内容简单地维护在一个地方不是更漂亮吗?

模板化和重用的第一步是创建一个模板。一个网页通常由一些基本的部分组成:

header、body 和 footer。使用 Facelets,你能把这些通用的元素放在一个单独的页面里,并

创建一个带有可编辑区的模板,如下面的模板所示。

<!-- template.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>

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

<title>Sample Template</title>

</head>

<body>

<!-- 这里定义了标题 -->

<h1>#{title}</h1>

<div><ui:insert name="menu" /></div>

<p>

<!-- 这里定义了主体 -->

<ui:insert name="body" />

</p>

</body>

</html>

对于 menu 和 body 来说,<ui:insert/>标签用来标记这块地方的内容会根据每一页变化。

CHAPTER 第 5 章 Facelets

221

5

你可以用这个模板来创建其他的页面,并给 menu 和 body 区域提供不同的内容。

这个例子介绍了另外一个标签<ui:composition/>,该标签删掉了它外面的任何内容,也

就是说,你可以写一些普通的 HTML 页面,而 Facelets 只是用或者显示出现在

<ui:composition/>标签里面的内容。使用这个标签,同样可以选择提供一个模板属性,该属

性将定义内容如何或在哪里显示。

为了把内容和模板配对,<ui:define/> 标签的 name 属性和模板中<ui:insert/>标签的

name 属性一致时就可以替换。为了简单地传递变量或者文字,你可以使用<ui:param/>标签,

该标签把其 value 属性作为模板中的一个变量来替代。

下面就是结果页面。

<!-- template.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>

<meta http-equiv="Content-Type" content="text/html;

charset=iso-8859-1" />

<title>Sample Template</title>

</head>

<body>

<h1>Here's my Title</h1>

<div>Here's My Menu</div>

<p>Here's My Body</p>

</body>

</html>

在上面的例子中我们定义了两个插入点,如果模板文件中只有一个插入点,那么使用

模板就可以更加简单。下面例子中的模板文件只有一个插入点。

<!-- simple-template.xhtml -->

<!-- template.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>

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

<title>Sample Template</title>

</head>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

222

<body>

<p>

<!--在这里定义了主体 -->

<ui:insert name="body" />

</p>

</body>

</html>

那么模板的客户端就可以这样使用模板了:

……

<!-- simple-template-client.xhtml -->

<ui:composition template="simple-template.xhtml">

Here's My Simple Body

</ui:composition>

……

5.3.2 Facelets include 标签的用法

页面中的位置这个概念在页面中定义可重用的内容功能是相当强大的。上面几个例

子展示了如何使用模板中的位置来显示内容,但是如果想从另一个页面中包含进来一些

内容,那该如何做呢?可能这里涉及一系列的表单控件或者一个复杂的菜单,你可能喜

欢把它们分离到不同的文件中,以便重用或者单独管理而不用散乱地布局在页面的各个

角落。

总的来说,Facelets 提供了两种包含位置或者其他页面的方法。第一种是用<ui:include/>

标签,对于 Web 开发人员来说,该标签应该是相当熟悉的。

下面通过一个例子来学习使用这个标签。首先在页面上使用这个标签:

<!-- include.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>Insert title here</title>

</head>

<body>

<span id="leftNav">

<ui:include src="/WEB-INF/siteNav.xhtml"/>

CHAPTER 第 5 章 Facelets

223

5

</span>

</body>

</html>

上面的代码中通过<ui:include/>标签引用了 siteNav.xhtml 模板文件,下面我们来定义这

个模板文件:

<!-- template.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>Insert title here</title>

</head>

<body>

<!-- siteNav.xhtml -->

<ui:composition>

<!-- myfaces tomahawk components -->

<t:tree2 value="#{backingBean.options}" var="opt">

</t:tree2>

</ui:composition>

</body>

</html>

当 Facelets 处理 include.xhtml 时,siteNav.xhtml 的所有<ui:composition/>中的内容将被

包含进 include.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>Insert title here</title>

</head>

<body>

<!-- include.xhtml -->

<span id="leftNav">

<!-- myfaces tomahawk components -->

<t:tree2 value="#{backingBean.options}" var="opt">

</t:tree2>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

224

</span>

</body>

</html>

如果你想给 siteNav.xhtml 传递变量,这些变量供 tree 组件使用,那么可以使用<ui:param/>

标签。

<!-- include.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>Insert title here</title>

</head>

<body>

<span id="leftNav">

<ui:include src="/WEB-INF/siteNav.xhtml">

<ui:param name="menuBean" value="#{backingBean.options}"/>

</ui:include>

</span>

</body>

</html>

模板文件这时就要这么写:

<!-- template.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>Insert title here</title>

</head>

<body>

<!-- siteNav.xhtml -->

<ui:composition>

<!-- myfaces tomahawk components -->

<t:tree2 value="#{menuBean}" var="opt">

</t:tree2>

</ui:composition>

</body>

</html>

CHAPTER 第 5 章 Facelets

225

5

可以看到现在 siteNav.xhtml 可以使用变量 menuBean 并且 menuBean 是通过

<ui:include/>标签来传递的,通过这种方式就可以定义灵活的可重用的通用组件。

5.4 Facelets 自定义标签

对于<ui:include/>和<ui:param/>,Facelets 提供了一个更加简洁的解决方案,即通过支

持自定义标签来实现。不需要自己写任何 Java 代码。为了给 siteNav.xhtml 定制一个可重用

的标签,你必须创建一个简单的 XML 标签库文件,文件名为 my.taglib.xml。

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE facelet-taglib PUBLIC

"-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN"

"http://java.sun.com/dtd/facelet-taglib_1_0.dtd">

<facelet-taglib>

<namespace>">http://www.blue.com/jsf</namespace>

<tag>

<tag-name>siteNav.xhtml</tag-name>

<source>/WEB-INF/tags/siteNav.xhtml</source>

</tag>

</facelet-taglib>

除了在新标签库 XML 文件中制定 Facelets 外,你可能需要将尽可能多的标签添加到新

的标签库中。

为了使 Facelets 感知到新添加的标签,我们需要在 web.xml 中进行声明:

<context-param>

<param-name>facelets.LIBRARIES</param-name>

<param-value>

/WEB-INF/facelets/tags/my.taglib.xml

</param-value>

</context-param>

这样就可以在页面上,通过指定 http://www.bluelight.com/jsf 作为命名空间来引用新标

签了。

<?xml version="1.0" encoding="UTF-8"?>

<!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"

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

226

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

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

xmlns:my="http://www.blue.com/jsf" xml:lang="en" lang="en">

<ui:composition template="/template/global.xhtml">

<ui:define name="body">

<span id="leftNav">

<my:siteNav menuBean="#{backingBean.options}"/>

</span>

</ui:define>

</ui:composition>

</html>

这个 include-tag.xhtml 例子和前面的 include.xhtml 例子效果一样。menuBean 的属性作

为 siteNav.xhtml 的一个属性生效,就像<ui:param/>传递的那样。

在你的工程中可能包括了一个 jar 文件,其中包括了所有的自定义标签库,或者只是简

单地把自定义标签库放到了一个文件里。更多的标签在 Facelets Developer Documentation

中有详细描述,感兴趣的读者可以参阅。

可以看出,使用 Facelets 自定义标签还是比较简单的,首先是定义一个模板文件,然

后把这个模板文件声明为一个标签,再把这个标签定义注册给 Facelets, 后在页面上就可

以引用这个标签了。

Facelets 不仅提供了自定义标签的功能,还提供了自定义函数的功能,有关 Facelets 自

定义标签和函数的更加深入的内容,请参考 Facelets 开发文档 https://facelets.dev.java.

net/nonav /docs dev/docbook.html。

5.5 本章小结

在本章中,清楚地演示了使用 Facelets 的好处,即组件复合和页面模板,它的核心是

组件,而不是 Servlets 输出。但是采用 Facelets 也有些不足,其中之一就是对 Facelets 的

IDE 支持极少,只有一个 Eclipse IDE 实现完全支持 Facelets,而且看起来还不支持代码

补足。

也没有对 Facelets 调试的 IDE 支持(即设置断点之类的东西)。要想调试,需要阅读

Facelets 手册,打开 JDK 样式的日志,根据开发情况设置它的 init 参数。

从有利方面来说,笔者发现使用 Facelets API 非常自然和直观。调试在开始的时候有

些怪异,但是后来就适应了。随着 Facelets 发行包一起提供的演示应用程序没有定制标记

或功能的示例,但是核心项目代码中有,所以请用核心项目代码作为指导。

如果要使用新的 JSF 组件库,必须有公开这个库的 Facelets 标记库。有些主要的组

CHAPTER 第 5 章 Facelets

227

5

件库(例如 Oracle 和 Tomahawk)的标记库存在,但是即使这些也需要调整,就是必须调

整 Tomahawk 标记库才能在应用程序中得到 Tomahawk 的日历组件。如果想使用新的定

制组件库,就必须编写标记库文件。

虽然 Facelets 有些不足之处,但学习它还是值得的,Facelets 是 JSF 的未来,代表着

JSF 发展的方向。在 新的 JEE 6 规范中,Facelets 已经正式加入了 JSF 2.0 规范中,成为

JSF 视图层的首选,因此值得读者关注和学习。

参考资料

[1] Facelets 开发文档:https://facelets.dev.java.net/nonav/docs/dev/docbook.html

[2] 定义标签的更多资料:http://hi.baidu.com/jsfcn/blog/item/cb1aa8b177d0e7f9f2 fb438.html

CHAPTER

6

6.1 RichFaces 简介

简单地说,RichFaces 是一个拥有 Ajax 功能的 JSF 组件库。它的 大优势是可以使开

发者不再关注于烦琐的 JavaScript 实现,RichFaces 将这一切都变成透明的,即使你不是

JavaScript 高手也能做出华丽的、用户体验丰富的 Web 应用。

RichFaces 早是由 Exadel 推出的。2007 年 3 月 5 日,Red Hat 与 Exadel 达成战略合

作协议共同创建企业Web 2.0中心,与此同时Exadel旗下的Ajax4jsf、RichFaces和 Studio Pro

更名为 JBoss Ajax4jsf、JBoss RichFaces 和 Red Hat Developer Studio。同年 9 月,JBoss 宣

布将 Ajax4jsf 合并到 RichFaces 中。

RichFaces 目前 新的版本是 3.1.3 GA,它具有如下特性:

它可以完整地集成到 JSF 的生命周期中,这可以使 JSF 应用在使用 Ajax 时获得

大的收益。同类型的其他框架只允许你访问受管理的 Bean,而 RichFaces 允许你在

Ajax 的请求响应周期中访问 Action 和 Listener,调用服务器端的 Validator 和

Converter。

给现有的 JSF 应用增加 Ajax 功能更加容易。该框架提供两个类库:核心 Ajax 类库和

UI类库。核心Ajax类库可以帮助你为现有应用增加Ajax功能而不增加任何 JavaScript

代码;当然,你也可以将原有页面上的标准 JSF 组件替换为 UI 类库中包含 Ajax 功

能的新组件。

第 6 章

使用 RichFaces

CHAPTER 第 6 章 使用 RichFaces

229

6

更易于实现复杂的页面。RichFaces 的新组件增加了很多用户体验接口,使用这些

组件可以构建功能强大的应用。同时,RichFaces 设计时充分考虑了集成第三方的

组件,它可以无缝地和很多框架集成,让你有更多的选择。

使用其核心的 Ajax 类库可以方便地开发自己的 Ajax 组件。RichFaces 的 UI 组件是

由组件开发工具箱(Component Development Kit,CDK)开发而成的,可以使用这个

工具箱的代码生成功能,其中模板的语法很像 JSP 的语法,这更加缩短了你的入门

时间。

jar 包集成。使用 RichFaces 的众多功能,你不需要再花费更多的时间关注 JavaScript、

CSS 和组件关联所使用的图片,只需要将 jar 包放在 Web 的 CLASSPATH 下就可以

了。

皮肤功能。RichFaces 提供了皮肤功能,使你可以更加容易地定义组件的风格,更

加方便的是,可以在运行时改变它们。当然,RichFaces 框架本身也包含了众多皮

肤,可以自由选用。

6.2 使用条件

(1)支持的 JDK 版本

官方文档中要求 JDK 的版本为 5.0 或者以上版本,但经笔者测试发现 RichFaces 完全

可以运行在 JDK 1.4.2 平台之上,但需要额外的 el-api.jar 和 el-ri.jar 包。

(2)支持的 JSF 实现

目前,对于比较流行的 JSF 实现,RichFaces 均提供了良好的支持,这些 JSF 实现包括:

Sun JSF RI 1.1~1.2

MyFaces 1.1.1~1.2

Facelets JSF 1.1.1~1.2

Seam 1.2 ~ 2.0

(3)支持的服务器

对于大多数的服务器它都支持,包括:

Apache Tomcat 4.1~6.0

IBM Websphere 5.1~6.1

BEA Weblogic 8.1~9.0

Oracle AS/OC4J 10.1.3

Resion 3.0

Jetty 5.1.X

Sun Application Server 8 (J2EE 1.4)

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

230

Glassfish (J2EE 5)

JBoss 3.2~4.2.X

Sybase EA Server 6.0.1

(4)支持的浏览器

IE 6.0~7.0

Firefox 1.5~2.0

Opera 8.5~9.0

Netscape 7.0

Safari 2.0

6.3 配置 RichFaces

6.3.1 下载

请从 http://labs.jboss.com/jbossrichfaces/ 下载RichFaces 的 新版本,笔者完成本书时的 新

版本是 3.1.3 GA。下载以后的RichFaces 是一个 zip 文件,文件名为 richfaces-ui-3.1.3.GA-bin.zip。

将它解压缩,解压完以后的目录结构如图 6-1 所示。

图 6-1 RichFaces 的目录结构

目录 apidocs 下放置的是 RichFaces API 文档,它等同于 JDK 的 API 文档。

CHAPTER 第 6 章 使用 RichFaces

231

6

目录 docs 下放置的是 RichFaces 的用户使用向导和常见问题。

目录 lib 下有 3 个 jar 包,分别是 richfaces-api-3.1.3.GA.jar、richfaces-impl-3.1.3.GA.jar

和 richfaces-ui-3.1.3.GA.jar。

目录 tlddoc 下放置的是 RichFaces 的 JSP 标签说明文档,你可以在这里查阅各个标

签的详细属性。

6.3.2 安装及配置

在 Web 项目中配置 RichFaces 相当简单,请遵循下列步骤操作。

将 richfaces-api-3.1.3.GA.jar、richfaces-impl-3.1.3.GA.jar 和 richfaces-ui-3.1.3.GA.jar

复制到 WEB-INF/lib 下。

把下列代码段复制到 web.xml 中。

<context-param>

<param-name>org.richfaces.SKIN</param-name>

<param-value>blueSky</param-value>

</context-param>

<filter>

<display-name>RichFaces Filter</display-name>

<filter-name>richfaces</filter-name>

<filter-class>org.ajax4jsf.Filter</filter-class>

</filter>

<filter-mapping>

<filter-name>richfaces</filter-name>

<servlet-name>Faces Servlet</servlet-name>

<dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher>

<dispatcher>INCLUDE</dispatcher>

</filter-mapping>

在使用 RichFaces 的页面中引入以下 taglib:

<%@ taglib uri="http://richfaces.org/a4j" prefix="a4j"%>

<%@ taglib uri="http://richfaces.org/rich" prefix="rich"%>

大功告成,现在你可以开发 RichFaces 应用了。

下面通过 Hello World 示例来介绍 RichFaces 应用。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

232

6.3.3 HelloWorld 示例

HelloWorld 示例的界面如图 6-2 所示,当用户在文本框中输入文本时,文本框下面的

标签将显示用户输入的文本。

图 6-2 Hello World 示例的界面

笔者开发所使用的环境是 Eclipse 3.4.0、Apache Tomcat 6.0.14、Sun JSF RI 1.2 和

RichFaces 3.1.3 GA。

在 Eclipse 中创建动态 Web 项目,如图 6-3、图 6-4 和图 6-5 所示。

图 6-3 创建动态 Web 项目(一)

CHAPTER 第 6 章 使用 RichFaces

233

6

图 6-4 创建动态 Web 项目(二)

图 6-5 创建动态 Web 项目(三)

按照 6.3.2 节中所述配置 RichFaces。完整的 web.xml 内容如下所示:

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

234

<?xml version="1.0" encoding="GB18030"?>

<web-app xmlns="http://java.sun.com/xml/ns/javaee"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://java.sun.com/xml/ns/javaee

http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">

<!-- JSF参数设置 -->

<context-param>

<param-name>javax.faces.CONFIG_FILES</param-name>

<param-value>/WEB-INF/faces-config.xml</param-value>

</context-param>

<context-param>

<param-name>javax.faces.DEFAULT_SUFFIX</param-name>

<param-value>.jsp</param-value>

</context-param>

<!-- 设置 RichFaces的组件风格 -->

<context-param>

<param-name>org.richfaces.SKIN</param-name>

<param-value>blueSky</param-value>

</context-param>

<!-- RichFaces Filter设置 -->

<filter>

<display-name>RichFaces Filter</display-name>

<filter-name>richfaces</filter-name>

<filter-class>org.ajax4jsf.Filter</filter-class>

</filter>

<filter-mapping>

<filter-name>richfaces</filter-name>

<servlet-name>Faces Servlet</servlet-name>

<dispatcher>REQUEST</dispatcher>

<dispatcher>FORWARD</dispatcher>

<dispatcher>INCLUDE</dispatcher>

</filter-mapping>

<!-- JSF Servlet设置 -->

<servlet>

<servlet-name>Faces Servlet</servlet-name>

<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>

CHAPTER 第 6 章 使用 RichFaces

235

6

<load-on-startup>1</load-on-startup>

</servlet>

<servlet-mapping>

<servlet-name>Faces Servlet</servlet-name>

<url-pattern>*.jsf</url-pattern>

</servlet-mapping>

</web-app>

创建一个 Bean 对象,它将作为受管的 Bean 对象(Managed Bean)。

package demo;

public class Bean {

private String text;

public Bean() {

}

public String getText() {

return text;

}

public void setText(String text) {

this.text = text;

}

}

创建 HelloWorld.jsp 文件。

在 WebContent 文件夹下建立一个名为 HelloWorld.jsp 的文件。完成上面 3 步以后的工

程结构如图 6-6 所示。

图 6-6 建立 HelloWorld.jsp 文件以后的工程结构

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

236

HelloWorld.jsp 文件的内容如下:

<%@ page language="java" contentType="text/html; charset=GB18030"

pageEncoding ="GB18030"%>

<!-- 引入 RichFaces标签 -->

<%@ taglib uri="http://richfaces.org/a4j" prefix="a4j"%>

<%@ taglib uri="http://richfaces.org/rich" prefix="rich"%>

<!-- 引入 JSF标签 -->

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>

<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>

<html>

<head>

<title>Hello World</title>

</head>

<body>

<f:view>

<h:form>

<h:panelGrid>

<h:outputText value="请输入:"/>

<h:inputText size="50" value="#{bean.text}">

<a4j:support event="onkeyup" reRender="rep" />

</h:inputText>

<h:outputText value="Ajax响应:"/>

<h:outputText value="#{bean.text}" id="rep" />

</h:panelGrid>

</h:form>

</f:view>

</body>

</html>

在这个页面中,使用了<a4j:support>标签,该标签为<h:inputText>增加了 Ajax 功能,

该功能绑定到了 onkeyup 事件。当 onkeyup 事件触发时,RichFaces 将会向服务器发送 Ajax

请求,这就意味着用户刚刚输入的文本将会发送到服务器端。属性 reRender 定义了页面中

的某个部分将会更新。在这个例子中,id 等于 rep 的<h:outputText>与 reRender 的值相等,

所以它将会被更新。于是,用户刚刚输入的值就显示出来了。

配置 faces-config.xml 文件。

<?xml version="1.0" encoding="UTF-8"?>

<faces-config xmlns="http://java.sun.com/xml/ns/javaee"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

CHAPTER 第 6 章 使用 RichFaces

237

6

xsi:schemaLocation="http://java.sun.com/xml/ns/javaee

http://java. sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd" version="1.2">

<managed-bean>

<managed-bean-name>bean</managed-bean-name>

<managed-bean-class>demo.Bean</managed-bean-class>

<managed-bean-scope>request</managed-bean-scope>

<managed-property>

<property-name>text</property-name>

<property-class>java.lang.String</property-class>

<value />

</managed-property>

</managed-bean>

</faces-config>

配置相当简单,你可以利用 WTP 的可视化编辑器生成该文件。

至此,完成了 HelloWorld 示例。你现在可以启动服务器看看 RichFaces 的效果了。

6.3.4 RichFaces 配置进阶

如果你是一个细心的读者,你一定注意到了 HelloWorld 示例的 web.xml 文件中有一个

特殊的配置:

<context-param>

<param-name>org.richfaces.SKIN</param-name>

<param-value>blueSky</param-value>

</context-param>

在本节中,将详细讲述 RichFaces 的高级配置参数。

1. 初始化参数(<context-param>)

(1)org.richfaces.SKIN

这项配置我们在 HelloWorld 示例中使用过,该参数是设定 RichFaces 组件的皮肤风格。

RichFaces 内置了多种皮肤风格,其中 Default 是默认的风格,可以使用的其他风格有

blueSky、classic、deepMarine、emeraldTown、japanCherry、ruby、wine 和 plain。

除了使用字串值设置皮肤风格以外,还可以使用 EL 表达式动态设定皮肤风格。例如

下面的设置:

<context-param>

<param-name>org.ajax4jsf.SKIN</param-name>

<param-value>#{skinBean.skin}</param-value>

</context-param>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

238

(2)org.ajax4jsf.LOGFILE

这是一个指向应用程序或容器日志文件的 URL(如果有日志文件的话)。如果设置了

这个参数,那么日志文件的内容将会作为一个调试信息页面在框架(iframe)窗口中显示。

(3)org.ajax4jsf.VIEW_HANDLERS

这是一个由逗号分隔的序列,可以在这里定义 ViewHandler 实例,用于插入到

Handlers 链路上。Handlers 按照给定的顺序被插入到 RichFaces ViewHandlers 之前。例如,

在 Facelets 应用程序中,这个参数必须包含 com.sun.facelets.FaceletViewHandler,来代替

在 faces-config.xml 文件中的声明。

(4)org.ajax4jsf.CONTROL_COMPONENTS

这是一个由逗号分隔的名字序列,用于将一个组件作为一个特殊的控制情形,例如资

源文件加载器、别名(alias)Bean 组件等。这是一个从 COMPONENT_TYPE 静态属性反

射得到的一个组件类型。对于这种类型组件的编码方法常常在呈现的 Ajax 响应中被调用,

即使这个组件不在被更新区域中。

(5)org.ajax4jsf.ENCRYPT_RESOURCE_DATA

对于生成的资源,例如加密生成的数据,它会在资源的 URL 上被编码加密。例如,URL

指向一个由 mediaOutput 组件生成的图片,而 mediaOutput 组件包含一个生成方法的名字,

那么对于一个黑客的攻击,它很容易创建一个对于任何 JSF 后台 Beans 或其他属性的请求。为

了避免这样的攻击,在重要的应用程序中设置这个参数为“true”(仅适用于 JDK 的版本高于

1.4 的情况)。该属性的默认值为 false。

(6)org.ajax4jsf.ENCRYPT_PASSWORD

用于资源数据加密的一个种子。如果没有设置,将使用一个随机的种子。

(7)org.ajax4jsf.COMPRESS_SCRIPT

默认值为 true,不允许框架重新格式化 JavaScript 文件(这将不利于调试生成的 JavaScript

代码)。

2. org.ajax4jsf.Filter 初始化参数

(1)log4j-init-file

这是指向 log4j.xml 的路径参数,log4j.xml 可用于创建每个应用程序的日志信息。

(2)enable-cache

默认值为 true。启用框架所生成资源(JavaScript、CSS、images 等)的缓存。为了调

试开发自定义的 JavaScript 或 Style(CSS) 目的,应避免在浏览器中使用旧的缓存数据。

(3)forceparser

默认值为 true。通过一个 HTML 语法检查器强制解析每一个 JSF 页面。如果为 false,

则只有 Ajax 响应才被语法检查器解析且被转换为规范的 XML。设置为 false 除了可以提

高性能,还可以为 Ajax 更新提供视觉的效果。

3. 和 Facelets 协同工作

支持 Facelets 是 RichFaces 的一个重要特性,目前 RichFaces 支持 Facelets 的任何版本。

CHAPTER 第 6 章 使用 RichFaces

239

6

由于 Facelets 使用自己的 ViewHandler,它需要在 RichFaces 的 AjaxViewHandler 之前运行,

这需要配置一下 org.ajax4jsf.VIEW_HANDLERS 属性,其他设置不需要任何变更。

<context-param>

<param-name>org.ajax4jsf.VIEW_HANDLERS</param-name>

<param-value>com.sun.facelets.FaceletViewHandler</param-value>

</context-param>

4. 和 Seam 协同工作

请参看第 7 章。

6.4 RichFaces 的基本原理

6.4.1 简介

正像本章开篇所讲的那样,RichFaces 的目标是开发带有 Ajax 功能的组件库,这些组

件要能够很容易地添加到现有页面中而不需要开发者添加任何 JavaScript 代码。

那么,RichFaces 是如何工作的呢?我们首先来看一下一个 Ajax 请求的流程,如图 6-7

所示。

图 6-7 RichFaces 工作原理图(一个 Ajax 请求的流程)

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

240

为了使大家更好地熟悉这个流程,我们来分析一下 HelloWorld 程序。在 HelloWorld.jsp

中:

<h:inputText size="50" value="#{bean.text}">

<a4j:support event="onkeyup" reRender="rep" />

</h:inputText>

经过 JSF 的生命周期以后被转换为以下代码:

<input type="text" name="j_id79:j_id82" value="" onkeyup="A4J.AJAX.Submi

t('_viewRoot','j_id79',event,{'parameters':{'j_id79:j_id83':'j_id79:j_id

83'} ,'actionUrl':'/HelloWorld/pages/HelloWorld.faces;jsessionid=49AD520

44ACD852CED343990A46ED7CD?javax.portlet.faces.DirectLink=true'} )" s

ize="30"/>

函数 A4J.AJAX.Submit 定义在 richfaces-impl-3.1.3.GA.jar 下包名为 org.ajax4jsf.javascript.

scripts 的 AJAX.js 中(如图 6-8 所示),它的主要功能是将用户的 Ajax 请求放置在请求队列

中,RichFaces 框架会处理该请求队列。

图 6-8 AJAX.js 在包中的位置

该函数的源代码如下:

//将请求提交或者放入请求队列中。但是并不是一直无逻辑地放入,对于同一个队列多个相同

//请求,框架只会放入一个请求,也就是说,新的请求事件会代替上一次的请求

A4J.AJAX.Submit = function(containerId, form, evt, options) {

var domEvt;

evt = evt || window.event || null;

if (evt) {

// 创建新的事件复制对象

try {

domEvt = A4J.AJAX.CloneObject(evt, false);

} catch (e) {

//记录日志

LOG.warn("Exception on clone event " + e.name + ":" + e.message);

CHAPTER 第 6 章 使用 RichFaces

241

6

}

LOG.debug("Have Event " + domEvt + " with properties: target:

"+domEvt.target + ", srcElement: " + domEvt.srcElement+ ",

type: " + domEvt.type);

}

//对于请求队列的逻辑处理

if (options.eventsQueue) {

var eventsQueue = A4J.AJAX._eventsQueues[options.eventsQueue];

if (eventsQueue) {

var eventsCount = eventsQueue.options.eventsCount || 1;

//包装 Evenet Queue

eventsQueue.wait = true;

eventsQueue.containerId = containerId;

eventsQueue.form = form;

eventsQueue.domEvt = domEvt;

eventsQueue.options = options;

eventsQueue.options.eventsCount = eventsCount + 1;

//放弃 Queue内未完成的请求

if (options.ignoreDupResponses && eventsQueue.request) {

LOG.debug("Abort uncompleted request in queue "+ options.

eventsQueue);

//调用 abort方法放弃请求

eventsQueue.request.abort();

//将 request设置为 false

eventsQueue.request = false;

//不再等待

eventsQueue.wait = false;

//如果是延迟的请求,则结束等待将请求写到事件队列中

if (options.requestDelay) {

window.setTimeout(

function() {

我们从这个 JavaScript 函数中可以看到有很多队列的处理逻辑。RichFaces 对请

求的处理是十分严谨的,正像刚开始笔者就强调过的那样,它的功能并不是简单地

把所有请求都放置到队列中,然后由队列顺序执行;而是对请求进行了一定的“筛

选”,对于相同的多次请求, 后一次操作将会被放置到队列中。比如,用户多次单

击同一个按钮。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

242

LOG.debug("End delay waiting, make request in queue "

+ options.eventsQueue);

A4J.AJAX.SubmiteventsQueue(A4J.AJAX._eventsQueues

[options.eventsQueue]);

}, options.requestDelay);

LOG.debug("Create new waiting for request in queue "

+ options.eventsQueue);

return;

}

} else {

//写日志后不做任何事情,返回

LOG.debug("Put new event to queue " + options.eventsQueue);

return;

}

} else {

//队列定义

var queue = {

wait :false,

containerId :containerId,

form :form,

domEvt :domEvt,

options :options

};

A4J.AJAX._eventsQueues[options.eventsQueue] = queue;

//如果是延迟的请求,则结束等待将请求写到事件队列中

if (options.requestDelay) {

window.setTimeout(

function() {

LOG.debug("End delay waiting, make request in queue "

+ options.eventsQueue);

A4J.AJAX.SubmiteventsQueue(A4J.AJAX._eventsQueues

[options.eventsQueue]);

}, options.requestDelay);

LOG.debug("Event occurs, create waiting for request in queue "

+ options.eventsQueue);

return;

CHAPTER 第 6 章 使用 RichFaces

243

6

}

}

}

//完成队列处理逻辑以后进行请求处理

A4J.AJAX.SubmitRequest(containerId, form, domEvt, options);

};

为了更加清楚地让大家看到运行过程,我们使用 FireBug 进行调试。

同时为了验证 RichFaces 的 Ajax 请求经过了整个 JSF 阶段,我们在 A4J.AJAX.Submit

这一行设置一个断点,调试信息如图 6-9 所示。

图 6-9 调试 RichFaces

仔细查看 Submit 函数,注意到 后调用了 A4J.AJAX.SubmitRequest 函数。

SubmitRequest 函数的主要作用是构建 XMLHttpRequest 发送请求,同时处理响应

A4J.AJAX.processResponse,在 processResponse 函数中调用 A4J.AJAX.replacePage 对页面

局部进行重绘(实现方法请看加粗的代码)。

A4J.AJAX.replacePage = function(req) {

//响应中无任何信息则返回

if (!req.getResponseText()) {

LOG.warn("No content in response for replace current page");

return;

}

LOG.debug("replace all page content with response");

var isIE = _SARISSA_IS_IE;

//Prevent "Permission denied in IE7"

//Reset calling principal

var oldDocOpen = window.document.open;

if (isIE) {

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

244

LOG.debug("setup custom document.open method");

window.document.open = function() {

oldDocOpen.apply(this, arguments);

}

}

//请注意下面的代码将完成局部页面的替换逻辑

window.setTimeout( function() {

var isDocOpen = false;

try {

window.document.open(req.getContentType(), true);

LOG.debug("window.document has opened for writing");

isDocOpen = true;

window.document.write(req.getResponseText());

LOG.debug("window.document has been writed");

window.document.close();

LOG.debug("window.document has been closed for writing");

if (isIE) {

//对于 IE来说,页面上的脚本并没有激活

window.location.reload(false);

}

} catch (e) {

LOG.debug("exception during write page content " + e.Message);

if (isDocOpen) {

window.document.close();

}

//Firefox/Mozilla 不支持 document.write方法(XHMTL),使用//DOM操作使页

面局部替换

var oDomDoc = (new DOMParser()).parseFromString(req.

getResponseText(),

"text/xml");

if (Sarissa.getParseErrorText(oDomDoc) == Sarissa.PARSED_OK) {

LOG.debug("response has parsed as DOM documnet.");

Sarissa.clearChildNodes(window.document.documentElement);

var docNodes = oDomDoc.documentElement.childNodes;

for ( var i = 0; i < docNodes.length; i++) {

if (docNodes[i].nodeType == 1) {

LOG.debug("append new node in document");

CHAPTER 第 6 章 使用 RichFaces

245

6

var node = window.document.importNode(docNodes[i], true);

window.document.documentElement.appendChild(node);

}

}

} else {LOG.error("Error parsing response", Sarissa

.getParseErrorText(oDomDoc));

}

} finally {

window.document.open = oldDocOpen;

}

LOG.debug("page content has been replaced");

}, 30);

}

RichFaces 是通过以上方式实现 Ajax 功能的。为了使大家更加清楚地了解上面的工作

原理,下面的 Ajax 请求流程图(见图 6-10)将给大家一个直观的印象。

图 6-10 Ajax 请求流程图

那么发出的 Ajax 请求经过 JSF 的整个生命周期了吗?下面我们来验证一下,证明这个请求

的确经过了 JSF 的整个生命周期。我们是通过使用 JSF 的 Phase 监听器验证的,下面是监听器

的源代码。

PhaseListener.java:

package demo;

import javax.faces.event.PhaseEvent;

import javax.faces.event.PhaseId;

import javax.faces.event.PhaseListener;

public class MyPhaseListener implements PhaseListener {

public void afterPhase(PhaseEvent phaseEvent) {

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

246

System.out.println("after - " + phaseEvent.getPhaseId().toString());

if (phaseEvent.getPhaseId() == PhaseId.RENDER_RESPONSE)

System.out.println("Done with Request!\n");

}

public void beforePhase(PhaseEvent phaseEvent) {

if (phaseEvent.getPhaseId() == PhaseId.RESTORE_VIEW)

System.out.println("Processing new Request!");

System.out.println("before - " + phaseEvent.getPhaseId().

toString());

}

public PhaseId getPhaseId() {

return PhaseId.ANY_PHASE;

}

}

同时在 faces-config.xml 中注册这个 PhaseListener:

<?xml version="1.0" encoding="UTF-8"?>

<faces-config xmlns="http://java.sun.com/xml/ns/javaee"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://java.sun.com/xml/ns/javaee

http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd" version="1.2">

……

<lifecycle>

<phase-listener>

demo.MyPhaseListener

</phase-listener>

</lifecycle>

</faces-config>

重新打开浏览器,访问 HelloWorld.jsf,并在输入框中输入一个字符 H,页面显示如图 6-11

所示。

CHAPTER 第 6 章 使用 RichFaces

247

6

图 6-11 用户输入 H 后页面显示

查看控制台的日志输出,如图 6-12 所示。

图 6-12 控制台的日志输出

使用 FireBug 查看 POST 值:

AJAXREQUEST=_viewRoot&j_id79=j_id79&j_id79%3Aj_id82=

H&javax.faces.ViewState=_id15&j_id79%3Aj_id83=j_id79%3Aj_id83&

它与页面间的关系,即代码与实现之间的关系如图 6-13 所示。

Processing new Request!

before - RESTORE_VIEW 1

after - RESTORE_VIEW 1

before - RENDER_RESPONSE 6

after - RENDER_RESPONSE 6

Done with Request!

首次请求页面时只经过

恢复视图和呈现响应两

个阶段。

用户触发 onkeyup 事件

后,RichFaces 的 Ajax

请求经过了整个 JSF 生

命周期。

Processing new Request!

before - RESTORE_VIEW 1

after - RESTORE_VIEW 1

before - APPLY_REQUEST_VALUES 2

after - APPLY_REQUEST_VALUES 2

before - PROCESS_VALIDATIONS 3

after - PROCESS_VALIDATIONS 3

before - UPDATE_MODEL_VALUES 4

after - UPDATE_MODEL_VALUES 4

before - INVOKE_APPLICATION 5

after - INVOKE_APPLICATION 5

before - RENDER_RESPONSE 6

after - RENDER_RESPONSE 6

Done with Request!

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

248

图 6-13 代码与实现之间的关系

6.4.2 RichFaces 的体系架构

RichFaces 的体系架构图如图 6-14 所示。

图 6-14 RichFaces 的体系架构图

CHAPTER 第 6 章 使用 RichFaces

249

6

(1)Ajax 过滤器(Ajax Filter)

正像 6.3.2 节中描述的那样,要使用 RichFaces 特性需要在 web.xml 中注册 Ajax Filter,

这个 Filter 可以过滤并识别出不同的请求种类。下面的序列图展示了一个常规的 JSF 请求

和 Ajax 请求(见图 6-15)。

图 6-15 常规的 JSF 请求和 Ajax 请求的序列图

在第一种情形(JSF 请求)中整棵 JSF 树将被编码,而 RichFaces 的 Ajax 请求则

不同,它将编码 JSF 树的一部分,这棵树的大小取决于需要重绘的局部页面的大小。

与 JSF 请求还有一处不同的是,在 Ajax 响应发送到客户端之前,Ajax Filter 将解析响

应内容。

对于以上两种情形,用户请求的资源都注册在 ResourceBuilder 类中。当一个请求到达

时(参考图 6-16),Ajax Filter 首先查询资源缓存(Resource Cache),如果命中就将所请求的

资源返回;否则 Ajax Filter 就搜索 ResourceBuilder 类中注册的资源,并请求 ResourceBuilder

创建该资源。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

250

图 6-16 资源请求序列图

(2)Ajax 动作组件(Ajax Action Components)

这包括 Ajax 命令按钮(AjaxCommandButton)、Ajax 超链接(AjaxCommandLink)、Ajax

Poll(可以定时从服务器端获取信息)和 Ajax 扩展支持(AjaxSupport)等,这些组件都可以

用在客户端发送请求。

(3)Ajax 容器(Ajax Containers)

这其实是一个接口,它描述了 JSF 页面上的一个区域,这部分区域在 Ajax 请求期间会

被解码(decode)。AjaxViewRoot 和 AjaxRegion 都实现了这个接口。

(4)JavaScript 引擎(JavaScript Engine)

RichFaces 的 JavaScript 引擎是运行在客户端的,它将根据 Ajax 响应来更新 JSF 页面的

不同区域。请不要在页面上直接调用与引擎相关的 JavaScript 代码,RichFaces 会自动处理

相关的 JavaScript 代码。

6.4.3 如何发送 Ajax 请求

有很多种方法可以从 JSF 页面中发送请求,你可以使用<a4j:commoandButton>、

<a4j:commandLink>、<a4j:poll>或<a4j:support>(正如 HelloWorld 示例中的一样)等,所有

的这些标签都隐藏了用 JavaScript 创建 XMLHttpRequest 和发送 Ajax 请求的细节。当然这

些标签也允许你定义当 Ajax 请求完成时 JSF 页面上的哪些组件将重绘,这可以通过给

CHAPTER 第 6 章 使用 RichFaces

251

6

reRender 属性赋值实现,例如 reRender="id1,id2"。

<a4j:commoandButton>和<a4j :commandLink>标签是通过 onclick 事件发送 Ajax 请求

的,而<a4j:poll>标签是通过一个定时器定时发送 Ajax 请求的。

<a4j:support>是用处比较多的标签之一,它允许你对标准的 JSF 组件添加 Ajax 功能,

你可以任意选择一个 JavaScript 事件(如 onkeyup、onmouseover 等)来发送请求。

6.4.4 确定要发送的内容

直接的想法是将页面上的局部区域内部发送到服务器端,使用这种做法你可以在发

送 Ajax 请求时控制 JSF 视图的那一部分将被重新解码。

如何指定页面的局部区域呢? 简单的方法是什么都不做。这是因为位于<f:view>和

</f:view>标签之间的部分被认为是默认的 Ajax 区域。当然,也可以使用<a4j:region>标签在

页面上指定多个区域。

<a4j:region>

<!--这里是内容-->

</a4j:region>

6.4.5 决定要重绘的区域

决定要重绘的区域十分简单,只要指定 reRender 属性就可以了。如果有多个区域需要

重绘,则需要用逗号分隔。

<a4j:commandButton id="save" reRender="addlog,monTotal" value="保存" />

注意:

在使用 reRender 属性时,要特别注意<f:verbatim>标签。详细内容请参看“参

考资料”。

6.5 RichFaces 开发工具介绍

6.5.1 Red Hat Developer Studio

Red Hat Developer Studio 是由 JBoss 推出的 Web 2.0 开发工具,它本身是基于 Eclipse

平台的。可以从 http://www.jboss.com/products/devstudio 上获取更多的信息。如图 6-17 所示

为该工具的界面截图。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

252

图 6-17 Red Hat Developer Studio 工具的界面截图

6.5.2 Eclipse WTP

WTP 是由 Eclipse 推出的 Web 开发工具,可以从 http://download.eclipse.org/webtools/

downloads/上免费获取到。如图 6-18 所示是web tools 图标,如图 6-19 所示是WTP 的界面截图。

图 6-18 web tools 图标

图 6-19 Eclipse WTP 的界面截图

CHAPTER 第 6 章 使用 RichFaces

253

6

6.6 RichFaces 常用组件介绍

在这一节里,将为大家介绍项目中经常用到的 RichFaces 组件。当然,RichFaces 组件

并不只有这些,你也可以扩展 RichFaces 编写自己的组件。

6.6.1 Ajax 日志 <a4j:log>

标签说明

该标签将通过 JavaScript 弹出一个窗口,用于显示 Ajax 请求信息,便于用户进行调试。

<a4j:log>示例如图 6-20 所示。

图 6-20 <a4j:log>示例

标签属性

属 性 名 称 描 述

binding 通过值绑定与后台的 Bean 对应

height 弹出窗口的高度

hotkey 与热键(“Ctrl+Shift”组合键)配合打开调试窗口

id 每个组件拥有的唯一 id,如果未设置系统将自动产生一个

level 显示 log 级别(FATAL、ERROR、WARN、INFO、DEBUG、ALL),默认为 ALL

name 弹出窗口的名称

popup 当为 true 时显示为弹出窗口;否则为 div 元素内容

rendered 是否渲染

width 弹出窗口的宽度

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

254

标签示例

HelloWorld.jsp:

<%@ page language="java" contentType="text/html; charset=GB18030"

pageEncoding="GB18030"%>

<!-- 引入 RichFaces标签 -->

<%@ taglib uri="http://richfaces.org/a4j" prefix="a4j"%>

<%@ taglib uri="http://richfaces.org/rich" prefix="rich"%>

<!-- 引入 JSF标签 -->

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>

<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>

<html>

<head>

<title>Hello World</title>

</head>

<body>

<f:view>

<h:form>

<h:panelGrid>

<h:outputText value="请输入:" />

<h:inputText size="30" value="#{bean.text}">

<a4j:support event="onkeyup" reRender="rep" />

</h:inputText>

<h:outputText value="Ajax响应:" />

<h:outputText value="#{bean.text}" id="rep" />

</h:panelGrid>

<a4j:log level="ALL" popup="true" width="400" hotkey="M" />

</h:form>

</f:view>

</body>

</html>

Bean.java:

package demo;

public class Bean {

private String text;

CHAPTER 第 6 章 使用 RichFaces

255

6

public Bean() {

}

//get & set 方法

}

注意事项

该标签所提供的信息是有限的,根据笔者的实际项目经验,大部分的调试还是通过

FireBug 进行的。

6.6.2 Ajax 监听器<a4j:ajaxListener>

标签说明

除了标准的 JSF 监听器之外,RichFaces 又增加了一个 Ajax 监听器,该监听器在渲

染响应之前被调用。普通的 ValueChangeListener 在验证失败之后不会被调用,但 Ajax

监听器在每一个 Ajax 响应中都会被调用。因此,如果要更新一组需要重绘的组件列表,

那么用这个标签是一个好方法。如图 6-21 所示是一个示例,从控制台中可以清楚地看

到后台所触发的 Ajax 事件。

图 6-21 <a4j:ajaxListener>示例

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

256

标签属性:

属 性 名 称 描 述

type 注册到 AjaxListener 中类的完全限定的 Java 类名

标签示例

AjaxListener.jsp:

<f:view>

<h:form>

<h:panelGrid>

<a4j:commandLink id="cLink" value="点击发送 Ajax请求">

<a4j:ajaxListener type="demo.BeanListener" />

</a4j:commandLink>

</h:panelGrid>

</h:form>

</f:view>

BeanListener.java:

package demo;

import org.ajax4jsf.event.AjaxEvent;

import org.ajax4jsf.event.AjaxListener;

public class BeanListener implements AjaxListener {

public void processAjax(AjaxEvent arg0) {

System.out.println("Ajax事件已触发!");

}

}

6.6.3 参数 <a4j:actionparam>

标签说明

这个标签是联合<f:actionListener>、<f:param>的桥梁。在渲染阶段,像往常一样被父

组件 (<h:commandLink>)解码;在处理请求阶段,如果父组件执行一个动作事件,则更

新“assignTo”属性的值为它的新值。如果一个转换属性被指定了,则使用它来编码和解码

该值成为保存在 html 参数中的字符串。

该标签有一个 noEscape 属性,如果该属性设置为 true,那么它的参数值将被作为

CHAPTER 第 6 章 使用 RichFaces

257

6

JavaScript 计算。如图 6-22 所示为该标签的示例。

图 6-22 <a4j:actionparam>示例

标签属性

属 性 名 称 描 述

assignTo EL 表达式,用来更新 bean 的属性。如果父组件执行一个 actionEvent,该值将被更新

binding 组件绑定

converter 使用一个转换器(converter)的 id 或者引用一个 converter

id 每个组件都应该有一个唯一的 id。如果没有指定,将会自动产生

name 参数的名字,见图 6-23 中 FireBug 里红色方框部分

noEscape 如果设置为 true,该值将不会被附上单引号,那么参数的值就可以在客户端以 JavaScript 计算。

该属性对于非 Ajax 组件无效

value 初始化的值或者值绑定

标签示例

ActionParam.jsp:

...

<html>

<head>

<title>Action Param</title>

<script language="javascript">

var foo = "test";

</script>

</head>

<body>

<f:view>

<h:form>

<h:panelGrid>

<a4j:commandButton value="actionparam测试" reRender="output">

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

258

<a4j:actionparam noEscape="true" name="param1" value="foo"

assignTo="#{bean.text}" />

</a4j:commandButton>

<h:outputText id="output" value="bean.text的值为:#{bean.text}"/>

</h:panelGrid>

</h:form>

</f:view>

</body>

</html>

Bean.java:

package demo;

public class Bean {

private String text;

public Bean() {

}

public String getText() {

return text;

}

public void setText(String text) {

this.text = text;

}

}

使用 FireBug 查看 post 到服务器端的数据,你会发现 param1 的值,如图 6-23 所示。

图 6-23 使用 FireBug 调试 post 参数

CHAPTER 第 6 章 使用 RichFaces

259

6

6.6.4 按钮 <a4j:commandButton>

标签说明

该标签与<h:commandButton>十分类似,唯一的区别就是<a4j:commandButton>具有

Ajax 功能,它允许动态提交表单并根据服务器响应重绘局部页面。如图 6-24 所示为一个

示例。

图 6-24 <a4j:commandButton>示例

标签属性

属 性 名 称 描 述

accessKey 触发该组件的快捷键

action

绑定到一个方法。如果该 UIComponent 被用户激活,根据 immediate 属性的值,

此方法会在请求处理生命周期中的 Apply Request Values 或者 Invoke Application 阶段

被调用

actionExpress 绑定 action 的表达式

actionListener 方法绑定,当该组件被 Ajax 请求激活时,将调用该监听器方法处理该事件。该方法必

须为 public 的并且接收一个 AjaxEvent 参数,返回 void

ajaxSingle 如果值为 true,则只提交一个文本域或者链接,而不是提交整个表单

alt 文本说明

binding 组件绑定

bypassUpdates 如果值为 true,则在处理验证阶段后跳过更新模型值阶段到呈现阶段。可以用来验证组

件的输入值

data Ajax 请求时传回客户端的序列化数据(默认以 JSON 传递)。可以通过“data.foo”的语

法方式访问

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

260

续表

属 性 名 称 描 述

dir 文本方向

disabled 把控件的状态设置为不能使用(置灰)

eventsQueue 用来避免在同一个事件上重复请求的队列的名字。可以用来减少周期事件(如按键、鼠

标移动)请求的次数

focus Ajax 响应完成时需要获取焦点的控件 id

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

ignoreDupResponses

如果请求响应与队列中 近一次的响应“相似”,则将该属性设置为 true 可以忽略该响

应。ignoreDupResponses="true"并没有取消服务器处理请求,它只是避免了服务器端不必

要的更新

image 在这个 button 中要显示图片的绝对 URL 或者相对 URL。如果该属性被指定,那么该标

签将被渲染为<input type="impage">;否则由 type 属性值决定

immediate

如果该组件被 Ajax 请求激活,标记该组件应该立即(在应用请求值阶段)被感兴趣的

监听器和动作处理,而不是等到调用应用程序阶段。同<h:commandButton>的 immediate

属性

lang 产生该组件标记所使用的语言

limitToList 如果值为 true,则仅更新 reRender属性中指定的组件;否则(默认值)更新所有Ajax Region

中的组件

onbeforedomupdate DOM 更新之前所调用的 JavaScript 代码

onblur 当失去焦点时发生的事件

onchange 元素值发生变化时的事件

onclick 点击事件

oncomplete 在客户端请求完成时调用的 JavaScript 代码

ondbclick 双击时发生的事件

onfocus 获得焦点时发生的事件

onkeydown 键按下时发生的事件

onkeypress 键按下并释放时发生的事件

onkeyup 键释放时发生的事件

onmousepress 按下鼠标按键时发生的事件

onmousemove 鼠标光标在元素上移动时发生的事件

onmouseout 鼠标光标移开元素时发生的事件

CHAPTER 第 6 章 使用 RichFaces

261

6

续表

属 性 名 称 描 述

onmouseover 当鼠标移动到组件上方时发生的事件

onmouseup 当释放鼠标按键时发生的事件

rendered 如果值为 false,该组件将不会被显示

requestDelay 在 JavaScript 事件上延迟(单位为 ms)发送 Ajax 请求。与事件队列一起使用,可以减

少键盘或者鼠标移动触发请求的次数

reRender

该组件调用 Ajax 请求后重新渲染的组件的 id(在 UIComopnent.findComponent()中

使用),可以是一个 id,也可以是用逗号分开的很多 id,还可以是数据或集合的 EL

表达式

size 定义组件显示的尺寸

status 请求状态组件的 id(在 UIComopnent.findComponent()中使用)

style 应用在该组件上的 CSS

styleClass 和 HTML 的 class 属性一样

tabIndex 设置不同元素之间获得焦点的顺序(按下 Tab 键时)

timeout Ajax 请求的超时时间(单位是 ms)

title 该组件产生的标记元素的提示文字(当鼠标移动到该组件上面时出现的提示文字)

type 指定创建组件的类型。该属性的取值为 submit|reset|image|button,默认值为 submit

value 该组件的当前值

标签示例

CommandButton.jsp:

<f:view>

<h:form>

<h:panelGrid>

<a4j:commandButton value="点击" action="#{bean.click}"

reRender="output"/>

<h:outputText id="output" value="#{bean.text}"/>

</h:panelGrid>

</h:form>

</f:view>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

262

Bean.java:

package demo;

public class Bean {

private String text;

public Bean() {

}

public String click(){

text = "你点击了按钮!";

return null;

}

public String getText() {

return text;

}

public void setText(String text) {

this.text = text;

}

}

6.6.5 链接 <a4j:commandLink>

标签说明

该标签类似于<h:commandLink>标签,不同的是它具有 Ajax 功能。如图 6-25 所示为一

个示例。

图 6-25 <a4j:commandLink>示例

CHAPTER 第 6 章 使用 RichFaces

263

6

标签属性

属 性 名 称 描 述

accessKey 触发该组件的快捷键

action

绑定到要被激活的程序动作方法,如果该 UIComponent 被用户激活,根据 immediate

属性的值,此方法在请求处理生命周期中的Apply Request Values 或者 Invoke Application

阶段被调用

actionExpress 绑定 action 的表达式

actionListener 方法绑定,当该组件被 Ajax 请求激活时,将调用该监听器方法处理该事件。该方法必

须为 public 的并且接收一个 AjaxEvent 参数,返回 void

ajaxSingle 如果值为 true,则只提交一个文本域或者链接,而不是提交整个表单

alt 文本说明

binding 组件绑定

bypassUpdates 如果值为 true,在处理验证阶段后跳过更新模型值阶段到呈现阶段。可以用来验证组

件的输入值

charset 该属性指定了通过链接指派的资源的字符编码

coords

定义区域

a.矩形:必须使用 4 个数字,前 2 个数字为左上角坐标,后 2 个数字为右下角坐标

例:<area shape=rect coords=100,50,200,75 href="URL">

b.圆形:必须使用 3 个数字,前 2 个数字为圆心的坐标, 后 1 个数字为半径长度

例:<area shape=circle coords=85,155,30 href="URL">

c.任意图形(多边形):将图形的每一转折点坐标依序填入

例:<area shape=poly coords=232,70,285,70,300,90,250,90,200,78 href="URL">

data Ajax 请求时传回客户端的序列化数据(默认以 JSON 传递)。可以通过“data.foo”的

语法方式访问

dir 文本方向

eventsQueue 用来避免在同一个事件上重复请求的队列的名字。可以用来减少周期事件(如按键、鼠

标移动)请求的次数

focus Ajax 响应完成时需要获取焦点的控件 id

hreflang

该属性指定了通过 href 指派的资源的基本语言,并且仅在 href 被指定的情

况下使用 (也就是说,如果 A 中没有指定 href 属性,就不应该出现 hreflang 属

性 )

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

264

续表

属 性 名 称 描 述

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

ignoreDupResponses

如果请求响应与队列中 近一次的响应“相似”,则将该属性设置为 true 可以忽略该响

应。ignoreDupResponses="true"并没有取消服务器处理请求,它只是避免了服务器端不必

要的更新

immediate

如果该组件被 Ajax 请求激活,标记该组件应该立即(在应用请求值阶段)被感兴趣

的监听器和动作处理,而不是等到调用应用程序阶段。同<h:commandButton>的

immediate 属性

lang 产生该组件标记所使用的语言

limitToList 如果值为 true,则仅更新 reRender 属性中指定的组件;否则(默认值)更新所有 Ajax

Region 中的组件

onbeforedomupdate DOM 更新之前所调用的 JavaScript 代码

onblur 当失去焦点时发生的事件

onclick 点击事件

oncomplete 在客户端请求完成时调用的 JavaScript 代码

ondbclick 双击时发生的事件

onfocus 获得焦点时发生的事件

onkeydown 键按下时发生的事件

onkeypress 键按下并释放时发生的事件

onkeyup 键释放时发生的事件

onmousepress 按下鼠标按键时发生的事件

onmousemove 鼠标光标在元素上移动时发生的事件

onmouseout 鼠标光标移开元素时发生的事件

onmouseover 当鼠标移动到组件上方时发生的事件

onmouseup 当释放鼠标按键时发生的事件

rel

该属性描述了从当前文档到通过 href 属性定义的锚点之间的关系。该属性值是通过空

格分隔的 link 类型(值)的列表(在 Web 标准开发中,通常用来代替 target 属性,配合脚

本,用来表示目的窗口)

rendered 如果值为 false,该组件将不会被显示

CHAPTER 第 6 章 使用 RichFaces

265

6

续表

属 性 名 称 描 述

requestDelay 在 JavaScript 事件上延迟 ( 单位为 ms )发送 Ajax 请求。与事件队列一起使用,可以减

少键盘或者鼠标移动触发请求的次数

reRender

该组件调用 Ajax 请求后重新渲染的组件的 id(在 UIComopnent.findComponent()中

使用),可以是一个 id,也可以是用逗号分开的很多 id,还可以是数据或集合的 EL

表达式

rev 该属性用来描述从通过 href 属性指定的锚点到当前文档的反向链接。该属性值是通过

空格分隔的 link 类型(值)的列表

shape

该属性指定一个区域的形状,可能的取值为:

* default:指定整个区域

* rect:指定一个矩形区域

* circle:定义一个圆形区域

* poly:定义一个多边形区域

status 请求状态组件的 id(在 UIComopnent.findComponent()中使用)

style 应用在该组件上的 CSS

styleClass 和 HTML 的 class 属性一样

tabIndex 设置不同元素之间获得焦点的顺序(按下 Tab 键时)

target 指定文档被打开的 frame 名称(同<a href>标签的 target 属性)

timeout Ajax 请求的超时时间(单位是 ms)

title 该组件产生的标记元素的提示文字(当鼠标移动到该组件上面时出现的提示文字)

type 该属性指定链接资源所采用的内容类型 (比如,网页通常为 text/html)

value 该组件的当前值

标签示例

CommandLink.jsp:

<f:view>

<h:form>

<h:panelGrid>

<a4j:commandLink value="点击链接" action="#{bean.click}"

reRender="output">

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

266

<a4j:actionparam name="myparam" value="myvalue"

assignTo="#{bean.text}" />

</a4j:commandLink>

<h:outputText id="output" value="#{bean.text}"/>

</h:panelGrid>

</h:form>

</f:view>

Bean.java:

package demo;

public class Bean {

private String text;

public Bean() {

}

public String click(){

text = "你点击了!";

return null;

}

public String getText() {

return text;

}

public void setText(String text) {

this.text = text;

}

}

实际上,RichFaces 仍然会将<aj4:commandLink>渲染为<a>标签,但它为什么具有了

Ajax 功能呢?我们来看一下示例所生成的 HTML 代码,其中加粗的部分就是

<a4j:commandLink>对应部分生成的代码。我们可以看到,它是通过一个()JavaScript 函数

A4J.AJAX.Submit 实现 Ajax 功能的,你如果细心的话,就会发现我们所定义的一个 myparam

参数也在其中。

<html>

<head>

CHAPTER 第 6 章 使用 RichFaces

267

6

</head>

<body>

<form id="j_id_jsp_614270122_1"

enctype="application/x-www-form-urlencoded"

action="/HelloWorld/pages/CommandLink.jsf;

jsessionid=595FF7ABF2719C5F2AE0BA84F56DE42F" method="post"

name="j_id_jsp_614270122_1">

<input type="hidden" value="j_id_jsp_614270122_1"

name="j_id_jsp_614270122_1"/>

<table>

<tbody>

<tr>

<td>

<a id="j_id_jsp_614270122_1:j_id_jsp_614270122_3"

onclick="A4J.AJAX.Submit('j_id_jsp_614270122_0',

'j_id_jsp_614270122_1',event,{'parameters':

{'j_id_jsp_614270122_1:j_id_jsp_614270122_3':

'j_id_jsp_614270122_1:j_id_jsp_614270122_3','myparam':'myvalue'},

'actionUrl':'/HelloWorld/pages/CommandLink.jsf;

jsessionid=595FF7ABF2719C5F2AE0BA84F56DE42F?

javax.portlet.faces.DirectLink=true'} );return false;"

name="j_id_jsp_614270122_1:j_id_jsp_614270122_3"

href="#">点击链接</a>

</td>

</tr>

<tr>

</tr>

</tbody>

</table>

<input id="javax.faces.ViewState" type="hidden"

value="_id2" name="javax.faces.ViewState"/>

</form>

</body>

<script>

</script>

</html>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

268

6.6.6 Ajax 状态 <a4j:status>

标签说明

该标签用于显示当前 Ajax 请求的状态。目前只支持对两种状态进行配置:一种是 Ajax

请求中;另一种是 Ajax 请求结束。如图 6-26 所示是一个请求中的示例。

图 6-26 <a4j:status>示例

标签属性

属 性 名 称 描 述

binding 组件绑定

dir 文本方向

for 用于指示 Ajax 状态容器的 id

forceId 如果值为 true,则呈现组件 id 为 HTML 代码而不是 JSF 产生的代码(等同于 MyFaces 中

的概念)

hreflang 该属性指定了通过 href 指派的资源的基本语言,并且仅在 href 被指定的情况下使用(也就是

说,如果 A 中没有指定 href 属性,就不应该出现 hreflang 属性)

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

lang 产生该组件标记所使用的语言

layout 定义面板的布局,可以为 block 或者 inline

onclick 点击事件

ondbclick 双击时发生的事件

onkeydown 键按下时发生的事件

CHAPTER 第 6 章 使用 RichFaces

269

6

续表

属 性 名 称 描 述

onkeypress 键按下并释放时发生的事件

onkeyup 键释放时发生的事件

onmousedown 鼠标按下时发生的事件

onmousemove 鼠标光标在元素上移动时发生的事件

onmouseout 鼠标光标移开元素时发生的事件

onmouseover 当鼠标移动到组件上方时发生的事件

onmouseup 当释放鼠标按键时发生的事件

onstart 开始 Ajax 请求时发生的事件

onstop Ajax 请求完成时发生的事件

rendered 如果值为 false,该组件将不会被显示

startStyle 开始请求时所要运用的 CSS 样式

startStyleClass 开始请求时所要运用的 CSS 样式类名称

startText Ajax 请求开始时要显示的文本

stopStyle Ajax 请求完成时的样式

stopStyleClass Ajax 请求完成时的样式类名称

stopText Ajax 请求完成时要显示的文本

style 用于这个控件的 CSS 样式

styleClass 用于这个控件的 CSS 样式类名称

title 该组件产生的标记元素的提示文字(当鼠标移动到该组件上面时出现的提示文字)

标签示例

Status.jsp:

<%@ page language="java" contentType="text/html; charset=GB18030"

pageEncoding="GB18030"%>

<!-- 引入 RichFaces标签 -->

<%@ taglib uri="http://richfaces.org/a4j" prefix="a4j"%>

<%@ taglib uri="http://richfaces.org/rich" prefix="rich"%>

<!-- 引入 JSF标签 -->

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

270

<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>

<html>

<head>

<title>Ajax Status</title>

</head>

<body>

<f:view>

<h:form>

<h:panelGrid>

<h:outputText value="请输入:" />

<h:inputText size="30" value="#{bean.text}">

<a4j:support event="onkeyup" reRender="rep" />

</h:inputText>

<h:outputText value="Ajax响应:" />

<h:outputText value="#{bean.text}" id="rep" />

</h:panelGrid>

<!-- 加载对话框,6.6.12节将介绍 modelPanel的详细用法-->

<rich:modalPanel id="ajaxLoadingModalBox" height="100" width="100"

zindex="2000" resizeable="false">

<f:facet name="header">

<h:outputText value="请稍候" />

</f:facet>

<rich:panel>

<h:outputText value="加载中..."/>

</rich:panel>

</rich:modalPanel>

<!-- Ajax请求开始时显示对话框,结束时关闭对话框-->

<a4j:statusonstart="Richfaces.showModalPanel(

'ajaxLoadingModalBox',{width:100, top:200})"

onstop="Richfaces.hideModalPanel('ajaxLoadingModalBox')" />

</h:form>

</f:view>

</body>

</html>

6.6.7 扩展 Ajax 事件 <a4j:support>

标签说明

在 HelloWorld 应用中,其实已经使用了<a4j:support>标签。该标签的功能是对标

CHAPTER 第 6 章 使用 RichFaces

271

6

准的 JSF 组件添加 Ajax 功能支持。例如,对于<h:dataTabel>可以增加行单击或者双击

功能。在如图 6-27 所示的示例中,当用户双击表格中的某一行时,将显示用户选中的

信息。

图 6-27 <a4j:support>示例

标签属性

属 性 名 称 描 述

action

绑定到要被激活的程序动作方法,如果该 UIComponent 被用户激活,根据 immediate

属性的值,此方法在请求处理生命周期中的 Apply Request Values 或者 Invoke Application

阶段被调用

actionExpression 绑定 actoin 的表达式

actionListener 方法绑定,当该组件被 Ajax 请求激活时,将调用该监听器方法处理该事件。该方法必

须为 public 的并且接收一个 AjaxEvent 参数,返回 void

ajaxSingle 如果值为 true,则仅提交一个 field/link,而不是整个 form 中的内容

binding 组件绑定

bypassUpdates 如果值为 true,则在处理验证阶段后跳过更新模型值阶段到呈现阶段。可以用来验证组

件的输入值

data Ajax 请求时传回客户端的序列化数据(默认以 JSON 传递)。可以通过“data.foo”的语

法方式访问

disableDefault 使目标事件的动作不可用 ( 在 JavaScript 代码中添加“return false;”)

event 父组件的 JavaScript 事件属性的名称 (onclick、onchange 等)

eventsQueue 用来避免在同一个事件上重复请求的队列的名字。可以用来减少周期事件(如按键、鼠

标移动)请求的次数

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

272

续表

属 性 名 称 描 述

focus Ajax 响应完成时需要获取焦点的控件 id

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

ignoreDupResponses

如果请求响应与队列中 近一次的响应“相似”,则将该属性设置为 true 可以忽略该响

应。ignoreDupResponses="true"并没有取消服务器处理请求,它只是避免了服务器端不必

要的更新

immediate

如果该组件被 Ajax 请求激活,标记该组件应该立即(在应用请求值阶段)被感兴趣的

监听器和动作处理,而不是等到调用应用程序阶段。同<h:commandButton>的 immediate

属性

limitToList 如果值为 true,则仅更新 reRender属性中指定的组件;否则(默认值)更新所有Ajax Region

中的组件

onbeforeupdate DOM 更新前所调用的 JavaScript 代码

oncomplete Ajax 请求结束时调用的 JavaScript 代码

onsubmit 提交 Ajax 事件之前所调用的 JavaScript 代码

rendered 如果值为 false,该组件将不会被显示

requestDelay 在 JavaScript 事件上延迟 ( 单位为 ms )发送 Ajax 请求。与事件队列一起使用,可以减

少键盘或者鼠标移动触发请求的次数

reRender

该组件调用 Ajax 请求后重新渲染组件的 id(在 UIComopnent.findComponent()中使

用),可以是一个 id,还可以用是逗号分开的很多 id,还可以是数据或者集合的 EL

表达式

status Request status 组件的 id(在 UIComopnent.findComponent()中使用)

timeout Ajax 请求的超时时间(单位是 ms)

标签示例

Support.jsp:

<f:view>

<h:form>

<rich:dataTable value="#{myList.list}" var="item"

binding="#{myList.dataTable}">

CHAPTER 第 6 章 使用 RichFaces

273

6

<a4j:support event="onRowDblClick" action="#{myList.dbClick}"

reRender="userChoosed" />

<f:facet name="header">

<h:outputText value="标题"/>

</f:facet>

<rich:column>

<h:outputText value="#{item}"/>

</rich:column>

</rich:dataTable>

<h:outputText id="userChoosed"value="当前你选中的是:#{myList.choosed}"/>

</h:form>

</f:view>

MyList.java:

package demo;

import java.util.ArrayList;

import java.util.List;

import javax.faces.component.UIData;

public class MyList {

private List<String> list;

private String choosed;

private UIData dataTable;

public String dbClick(){

choosed = (String) dataTable.getRowData();

return null;

}

public List<String> getList() {

if(list == null)

{

list = new ArrayList<String>();

for(int i=0;i<5;i++){

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

274

list.add("第" + i +"行");

}

}

return list;

}

public void setList(List<String> list) {

this.list = list;

}

public String getChoosed() {

return choosed;

}

public void setChoosed(String choosed) {

this.choosed = choosed;

}

public UIData getDataTable() {

return dataTable;

}

public void setDataTable(UIData dataTable) {

this.dataTable = dataTable;

}

}

faces-config.xml:

<managed-bean>

<managed-bean-name>myList</managed-bean-name>

<managed-bean-class>demo.MyList</managed-bean-class>

<managed-bean-scope>request</managed-bean-scope>

</managed-bean>

6.6.8 日历控件 <rich:calendar>

标签说明

日历控件可以大大地简化日期的输入和显示,示例如图 6-28 所示。

CHAPTER 第 6 章 使用 RichFaces

275

6

图 6-28 <rich:calendar>示例

标签属性

属 性 名 称 描 述

ajaxSingle 如果值为 true,则仅提交一个 field/link,而不是整个 form 中的内容

binding 组件绑定

boundaryDatesMode

本属性用来设定月份边界显示模式。共有 3 个值可选:

inactive

这是默认值,此时在月份边界之外的部分不可点击,如下图圈中的部分。

scroll

设定为此值时,月份边界之外的值可以点击,但同时日历会滚动到所点击日期所

在的月份,见下图。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

276

续表

属 性 名 称 描 述

boundaryDatesMode

select

设定为此值时,当点击月份边界之外的值时,日历控件会变为选定状态,见下图。

bypassUpdates 如果值为 true,则在处理验证阶段后跳过更新模型值阶段到呈现阶段。可以用来

验证组件的输入值

buttonClass 日历按钮的 CSS 样式

buttonIcon 日历按钮的图标。如果设定了 buttonLabel 属性,那么此值将被忽略

buttonIconDisabled 定义日历按钮在禁用时的图标。如果设定了 buttonLabel 属性,那么此值将被忽略。

buttonLabel

定义日历按钮的标题,如果此值被设定后,那么 buttonIcon 和 buttonIconDisabled

属性将被忽略

默认时该值没有设定:

如果把值设定为“选择日期”:

CHAPTER 第 6 章 使用 RichFaces

277

6

续表

属 性 名 称 描 述

cellHeight 单元格的高度

cellWidth 单元格的宽度

converter 所要引用的转换器的 id

converterMessage 使用此值替代转换器显示的消息

currentDate 当前日期

currentDateChangeListener 使用这个属性绑定一个动作监听器,当日期被选定时,监听器会被通知到

dataModel 日历的数据模型,如果没有设定这个属性,那么所有和数据模型相关的功能都不

能使用

dataPattern 定义日期的样式,例如:yyyy-MM-dd

dayStyleClass 和 JavaScript 函数绑定,用于高亮显示特殊的单元格

direction 定义日期控件弹出时的方位。可以是如下值:top-left(左上)、top-right(右上)、

bottom-left(左下)、bottom-right(右下,这是默认值)和 auto(自动)

disabled 把控件的状态设置为不能使用(置灰)

enableManualInput 该值为 true 时,文本框内的值用户可以编辑;值为 false 时,文本框被设置为只读

方式,用户只能通过日历控件选择日期

eventsQueue 用来避免在同一个事件上重复请求的队列的名字,可以用来减少周期事件(如按

键、鼠标移动)请求的次数

focus Ajax 响应完成时需要获取焦点的控件 id

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

ignoreDupResponses

如果请求响应与队列中 近一次的响应“相似”,则将该属性设置为 true 可以忽略

该响应。ignoreDupResponses="true"并没有取消服务器处理请求,它只是避免了服务

器端不必要的更新

immediate

如果该组件被 Ajax 请求激活,标记该组件应该立即(在应用请求值阶段)被感兴趣

的监听器和动作处理,而不是等到调用应用程序阶段。同<h:commandButton>的

immediate 属性

inputClass 文本框的 CSS 样式类名称

inputStyle 文本框的 CSS 样式

isDayEnabled 应该与返回 day 状态的一些 JavaScript 函数绑定

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

278

续表

属 性 名 称 描 述

jointPoint

设定弹出的日历与按钮之间的位置关系。可以是如下值:top-left(左上)、top-right

(右上)、bottom-left(左下)、bottom-right(右下,这是默认值)和 auto(自动)。例

如:下图是设定为 bottom-right 时的效果。

limitToList 如果值为 true,则仅更新 reRender 属性中指定的组件;否则(默认值)更新所有 Ajax

Region 中的组件

locale 设定 locale 信息

mode 值可以是 ajax 或者 client。设定为 ajax 时,将以 Ajax 方式运行;否则以客户端纯

脚本运行

monthLabels 配置月份的显示标签

monthLabelsShort 配置月份的短名称

onbeforedomupdate DOM 更新之前所调用的 JavaSript 代码

oncollapse 展开时发生的事件

oncomplete 在客户端请求完成时调用的 JavaScript 代码

oncurrentdayselect 选中当前日期时发生的事件

ondatemouseout 鼠标移出单元格时发生的事件

ondatemouseover 鼠标滑过单元格时发生的事件

ondateselect 选中日期时发生的事件

onexpend 展开时发生的事件

oninputblur 输入框失去焦点时发生的事件

oninputchange 输入框值发生变化时发生的事件

oninputclick 点击输入框时发生的事件

oninputfocus 输入框获得焦点时发生的事件

CHAPTER 第 6 章 使用 RichFaces

279

6

续表

属 性 名 称 描 述

oninputkeydown 输入框内键按下时发生的事件

oninputkeypress 输入框内键按下并释放时发生的事件

oninputkeyup 输入框内键释放时发生的事件

oninputselect 选中输入框时发生的事件

popup

设置为 true 时,样式为:

设置为 false 时,样式为:

rendered 如果值为 false,该组件将不会被显示

requestDelay 在 JavaScript 事件上延迟(单位为 ms)发送 Ajax 请求。与事件队列一起使用,可

以减少键盘或者鼠标移动触发请求的次数

preloadDateRangeBegin 定义初始化日期的起始值,渲染时将从数据模型中取出

preloadDateRangeEnd 定义初始化日期的结束值,渲染时将从数据模型中取出

required 是否是必需的(值为 true 或者 false)

requiredMessage 当不满足 requried 属性时所要显示的信息

reRender

该组件调用 Ajax 请求后重新渲染组件的 id(在 UIComopnent.findComponent()中使

用),可以是一个 id,也可以是用逗号分开的很多 id,还可以是数据或者集合的 EL 表

达式

showApplyButton 如果设定为 false,应用按钮将不会被显示

showInput 如果设定为 false,输入框将被隐藏。

showScrollerBar 如果设定为 false,滚动条将不会出现。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

280

续表

属 性 名 称 描 述

showWeekDaysBar

如果设定为 false,表示星期的状态栏将不会出现

showWeekBar

如果设定为 false,表示一年内第几周的状态栏将不会出现

showWeekBar

status 请求状态组件的 id(在 UIComopnent.findComponent()中使用)

style 应用在该组件上的 CSS

styleClass 和 HTML 的 class 属性一样

tabIndex 设置不同元素之间获得焦点的顺序(按下 Tab 键时)

timeout Ajax 请求的超时时间(单位是 ms)

timezone 该属性用于计算当前日期

CHAPTER 第 6 章 使用 RichFaces

281

6

续表

属 性 名 称 描 述

todayControlMode

必须为以下 3 个值中的一个

scroll:日历控件将滚动到当前日期

select:日历控件将把今天的日期作为选中值

hidden:将隐藏“Today”按钮

toolTipMode 用于指定加载提示信息的方式,必须使用以下值:

none、single、batch

validator 指定一个 validate 方法,在 JSF 生命周期的处理验证阶段将调用该方法

validatorMessage 用于显示验证信息

value 初始值

valueChangeListener 用于值变化时的监听器

verticalOffset 设定日历图标按钮和日历窗口之间的垂直距离

weekDayLabels 显示星期的标签,例如 Sun、Mon、Tue、Wed、…

weekDayLabelsShort 用于星期的简短名称

title 该组件产生的标记元素的提示文字(当鼠标移动到该组件上面时出现的提示文字)

type submit|reset|image|button 用于指定创建组件的类型,默认值为 submit

zindex 与 HTML 中 zindex 元素意义相同

使用以上标签属性可以解决绝大部分的项目需求,如果觉得还不满意,<rich:calendar>

还提供了 hearder、footer、optionalHeader、optionalFooter 等 Facet,在以上的 Facet 中你可

以使用如下元素: {currentMonthControl} 、 {nextMonthControl} 、 {nextYearControl} 、

{previousYearControl}、{previousMonthControl}、{todayControl}、{selectedDateControl},

这些元素都会用于相应标签的输出。<rich:calendar>布局如图 6-29 所示。

图 6-29 <rich:calendar>布局

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

282

下面我们给出一个使用 Facet 的例子。

标签示例

若是简单使用,像下面这样就可以了。

<rich:calendar datePattern="yyyy年 MM月 dd日 HH:mm"/>

效果如图 6-30 所示。

图 6-30 <rich:calendar>简单示例效果

也可以使用 Facet 自己定制成如图 6-31 所示的样式。

图 6-31 <rich:calendar>定制样式

Calendar.jsp:

<rich:calendar id="myCalendar" popup="true"

value="#{dateBean.userChoosed}"

preloadDateRangeBegin="#{dateBean.userChoosed}"

preloadDateRangeEnd="#{dateBean.userChoosed}"

currentDate="#{dateBean.userChoosed}" cellWidth="40px"

cellHeight="40px" datePattern="yyyy年 MM月 dd日"

CHAPTER 第 6 章 使用 RichFaces

283

6

weekDayLabelsShort="周日,周一,周二,周三,周四,周五,周六"

jointPoint="bottom-right">

<f:facet name="header">

<h:panelGrid columns="2" width="100%" columnClasses="width100,fake">

<h:outputText value="{selectedDateControl}" />

<h:outputText value="{todayControl}"

style="font-weight:bold;text-align:left" />

</h:panelGrid>

</f:facet>

<f:facet name="weekDay">

<h:panelGroup style="width:60px; overflow:hidden;"

layout= "block">

<h:outputText value="{weekDayLabelShort}" />

</h:panelGroup>

</f:facet>

<f:facet name="weekNumber">

<h:panelGroup>

<h:outputText value="{weekNumber}" style="color:red"/>

</h:panelGroup>

</f:facet>

<f:facet name="footer">

<h:panelGrid columns="3" width="100%"

columnClasses="fake, width100 talign">

<h:outputText value="{previousMonthControl}"

style="font-weight:bold;" />

<h:outputText value="{currentMonthControl}"

style="font-weight:bold;" />

<h:outputText value="{nextMonthControl}"

style="font-weight:bold;" />

</h:panelGrid>

</f:facet>

<h:outputText value="{day}"></h:outputText>

</rich:calendar>

DateBean.java:

public class DateBean {

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

284

private Date userChoosed = new Date();

public Date getUserChoosed() {

return userChoosed;

}

public void setUserChoosed(Date userChoosed) {

this.userChoosed = userChoosed;

}

}

faces-config.xml:

<managed-bean>

<managed-bean-name>dateBean</managed-bean-name>

<managed-bean-class>demo.DateBean</managed-bean-class>

<managed-bean-scope>request</managed-bean-scope>

</managed-bean>

6.6.9 列表移动<rich:listShuttle>

标签说明

此组件用于将用户选中的项目从一个列表移动到另外一个列表,同时用户还可以对已

经选中的项目进行排序。该组件具有如下优点:

灵活的可配制的外观;

可排序;

支持多选;

支持快捷键。

如图 6-32 所示为一个示例。

图 6-32 <rich:listShuttle>示例

CHAPTER 第 6 章 使用 RichFaces

285

6

标签属性

属 性 名 称 描 述

activeItem 保存活动项目

ajaxKeys 定义 Ajax 请求以后更新的字串

binding 组件绑定

bottomControlClass 定义底部控件的样式

bottomControlLabel 定义底部控件的标签

columnClasses 以逗号分隔的列的样式

componentState EL 表达式,用于保存组件状态或者重定义状态

controlsType 定义控件的形式,可以是 button 或者 none

converter 转换器的 id

copyAllControlClass copyAll 控件的样式

copyAllControlLabel copyAll 控件的标签

copyControlClass copy 控件的样式

copyControlLabel copy 控件的标签

disabledControlClass 禁用时的控件样式

downControlClass down 控件的样式

downControlLabel down 控件的标签

fastMoveControlsVisible 值为 false 时,copyAll 和 RemoveAll 控件将不显示

fastOrderControlsVisible 值为 false 时,top 和 bottom 控件将不显示

first 要显示的第一行,以 0 为起始值

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

ignoreDupResponses

如果请求响应与队列中 近一次的响应“相似”,则将该属性设置为 true 可以忽

略该响应。ignoreDupResponses="true"并没有取消服务器处理请求,它只是避免了服

务器端不必要的更新

immediate

如果该组件被 Ajax 请求激活,标记该组件应该立即(在应用请求值阶段)被感兴趣

的监听器和动作处理,而不是等到调用应用程序阶段。同<h:commandButton>的

immediate 属性

listClass list 控件的样式

listsHeight list 控件的高度

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

286

续表

属 性 名 称 描 述

moveControlsVerticalAlign move、copy 等控件相对于 list 控件的纵向位置

moveControlsVisible 值为 false 时,copy 和 remove 控件将不显示

onbottomclick 点击 bottom 控件时触发的 JavaScript 事件

onclick 点击时触发

oncopyallclick 点击 copyAll 控件时触发的事件

oncopyclick 点击 copy 控件时触发的事件

ondblclick 双击时触发的事件

ondownclick 点击 down 控件时触发的事件

onlistchanged list 控件发生变更时触发的事件

onmousemove 鼠标光标在元素上移动时发生的事件

onmouseout 鼠标光标移开元素时发生的事件

onmouseover 当鼠标移动到组件上方时发生的事件

onorderchanged 排序完成后发生的事件

onremoveallclick 点击 RemoveAll 控件时发生的事件

onremoveclick 点击 remove 控件时发生的事件

ontopclick 点击 top 控件时发生的事件

onupclick 点击 up 控件时发生的事件

orderControlsVerticalAlign order 控件相对于 list 的纵向位置

orderControlsVisible 值为 false 时,up 和 down 控件将不显示

removeAllControlClass removeAll 控件的样式

removeAllControlLabel removeAll 控件的标签

removeControlClass remove 控件的样式

removeControlLabel remove 控件的标签

rendered 如果值为 false,则该组件将不渲染。

required 如果值为 true,则该组件将验证空值

rowClasses 行的样式

rowKey 对于指定行的唯一区别标识

rowKeyVar 在 request 范围内可以访问的 row 的标识

rows 要显示的行数,以 0 为起始值

showButtonLabels 为控件显示标签

CHAPTER 第 6 章 使用 RichFaces

287

6

续表

属 性 名 称 描 述

sourceCaptionLabel 定义源列表的标题

sourceListWidth 定义源列表的宽度

sourceSelection 控制服务器源的选择

sourceValue 定义将要显示的源表

stateVar 提供在客户端可以访问的组件状态变量

style 当组件被渲染时的样式

styleClass 当组件被渲染时的样式类

switchByClick 如果值为 true,则源列表和目标列表之间的拖拽将以点击的形式实现

targetCaptionLabel 定义目标列表的标题

targetListWidth 定义目标列表的宽度

targetSelection 控制服务器端目标的选择

targetValue 定义将要显示的目标列表

topControlClass top 控件的样式

topControlLabel top 控件的标签

upControlClass up 控件的样式

upControlLabel up 控件的标签

validator 指定一个 validate 方法,在 JSF 生命周期的处理验证阶段将调用该方法

valueChangeListener 值变更时的监听器

var 在页面上定义一个列表

标签示例

ListShuttle.jsf:

<rich:listShuttle sourceValue="#{toolBar.freeItems}"

targetValue="#{toolBar.items}" var="items" listHeight="300"

listWidth="300"

sourceCaptionLabel="可选项"

targetCaptionLabel="已选项"

converter="listShuttleconverter"

copyAllControlLabel="移动全部"

copyControlLabel="移动"

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

288

removeAllControlLabel="移除全部"

removeControlLabel="移除"

topControlLabel="置顶"

upControlLabel="向上"

downControlLabel="向下"

bottomControlLabel="置底">

<rich:column width="18">

<h:graphicImage value="#{items.iconURI}"></h:graphicImage>

</rich:column>

<rich:column>

<h:outputText value="#{items.label}"></h:outputText>

</rich:column>

<a4j:support event="onlistchanged" reRender="toolBar"/>

</rich:listShuttle>

Converter.java(listShuttle 的转换器类):

public class Converter implements javax.faces.convert.Converter {

public Object getAsObject(FacesContext context,

UIComponent component,String value) {

int index = value.indexOf(':');

return new ToolBarItem(value.substring(0, index),

value.substring(index + 1));

}

public String getAsString(FacesContext context,

UIComponent component,Object value) {

ToolBarItem optionItem = (ToolBarItem) value;

return optionItem.getLabel() + ":" + optionItem.getIcon();

}

}

CHAPTER 第 6 章 使用 RichFaces

289

6

ToolBar.java:

public class ToolBar {

private List<ToolBarItem> items = new ArrayList<ToolBarItem>();

private List<ToolBarItem> freeItems = new ArrayList<ToolBarItem>();

public ToolBar() {

ToolBarItem item = new ToolBarItem();

item.setIcon("create_folder");

item.setLabel("新建文件夹");

items.add(item);

item = new ToolBarItem();

item.setIcon("create_doc");

item.setLabel("新建文档");

items.add(item);

item = new ToolBarItem();

item.setIcon("find");

item.setLabel("查找");

items.add(item);

item = new ToolBarItem();

item.setIcon("open");

item.setLabel("打开");

freeItems.add(item);

item = new ToolBarItem();

item.setIcon("save");

item.setLabel("保存");

freeItems.add(item);

item = new ToolBarItem();

item.setIcon("save_all");

item.setLabel("全部保存");

freeItems.add(item);

item = new ToolBarItem();

item.setIcon("delete");

item.setLabel("删除");

freeItems.add(item);

}

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

290

public List<ToolBarItem> getItems() {

return items;

}

public void setItems(List<ToolBarItem> items) {

this.items = items;

}

public List<ToolBarItem> getFreeItems() {

return freeItems;

}

public void setFreeItems(List<ToolBarItem> freeItems) {

this.freeItems = freeItems;

}

}

ToolBarItem.java:

public class ToolBarItem {

private String icon;

private String label;

private String iconURI;

public ToolBarItem() {

}

public ToolBarItem(String label, String icon) {

setLabel(label);

setIcon(icon);

}

public String getLabel() {

return label;

}

public void setLabel(String label) {

this.label = label;

}

CHAPTER 第 6 章 使用 RichFaces

291

6

public String getIcon() {

return icon;

}

public void setIcon(String icon) {

this.icon = icon;

}

public int hashCode() {

final int prime = 31;

int result = 1;

result = prime * result + ((icon == null) ? 0 : icon.hashCode());

result = prime * result + ((label == null) ? 0 : label.hashCode());

return result;

}

public boolean equals(Object obj) {

if (this == obj)

return true;

if (obj == null)

return false;

if (getClass() != obj.getClass())

return false;

final ToolBarItem other = (ToolBarItem) obj;

if (icon == null) {

if (other.icon != null)

return false;

} else if (!icon.equals(other.icon))

return false;

if (label == null) {

if (other.label != null)

return false;

} else if (!label.equals(other.label))

return false;

return true;

}

public String getIconURI() {

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

292

iconURI = "/images/icons/" + icon + ".gif";

return iconURI;

}

}

faces-config.xml:

<converter>

<converter-id>listShuttleconverter</converter-id>

<converter-class>demo.Converter</converter-class>

</converter>

<managed-bean>

<managed-bean-name>toolBar</managed-bean-name>

<managed-bean-class>demo.ToolBar</managed-bean-class>

<managed-bean-scope>request</managed-bean-scope>

</managed-bean>

6.6.10 可排序列表<rich:orderingList>

标签说明

该组件用于显示一个可以排序的列表,在客户端需要排序时特别有用。如图 6-33 所示

为一个示例。

图 6-33 <rich:orderingList>示例

标签属性

CHAPTER 第 6 章 使用 RichFaces

293

6

属 性 名 称 描 述

activeItem 保存活动项目

ajaxKeys 定义 Ajax 请求以后更新的字串

binding 组件绑定

bottomControlLabel 定义底部控件的标签

columnClasses 以逗号分隔的列的样式

componentState EL 表达式,用于保存组件状态或者重定义状态

controlsHorizontalAlign

影响控件布局,可供选择的值为:

left:控制按钮将显示在列表的左面

right(默认值):控制按钮将显示在列表的右面

controlsType 定义控件的形式,可以是 button 或者 none

controlsVerticalAlign

影响控件布局,可供选择的值为:

top:控制按钮将显示在列表的上面

bottom:控制按钮将显示在列表的下面

center(默认值):居中显示

converter 转换器的 id

copyAllControlClass copyAll 控件的样式

copyAllControlLabel copyAll 控件的标签

copyControlClass copy 控件的样式

copyControlLabel copy 控件的标签

disabledControlClass 禁用时的控件样式

downControlClass down 控件的样式

downControlLabel down 控件的标签

fastMoveControlsVisible 值为 false 时,copyAll 和 RemoveAll 控件将不显示

fastOrderControlsVisible 值为 false 时,top 和 bottom 控件将不显示

first 要显示的第一行,以 0 为起始值

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

ignoreDupResponses

如果请求响应与队列中 近一次的响应“相似”,则将该属性设置为 true 可以忽

略该响应。ignoreDupResponses="true"并没有取消服务器处理请求,它只是避免了

服务器端不必要的更新

immediate

如果该组件被 Ajax 请求激活,标记该组件应该立即(在应用请求值阶段)被感兴趣

的监听器和动作处理,而不是等到调用应用程序阶段。同<h:commandButton>的

immediate 属性

listClass list 控件的样式

listsHeight list 控件的高度

moveControlsVerticalAlign move、copy 等控件相对于 list 控件的纵向位置

moveControlsVisible 值为 false 时,copy 和 remove 控件将不显示

onbottomclick 点击 bottom 控件时触发的 JavaScript 事件

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

294

续表

属 性 名 称 描 述

onclick 点击时触发的事件

oncopyallclick 点击 copyAll 控件时触发的事件

oncopyclick 点击 copy 控件时触发的事件

ondblclick 双击时触发的事件

ondownclick 点击 down 控件时触发的事件

onlistchanged list 控件发生变更时触发的事件

onmousemove 鼠标光标在元素上移动时发生的事件

onmouseout 鼠标光标移开元素时发生的事件

onmouseover 当鼠标移动到组件上方时发生的事件

onorderchanged 排序完成后发生的事件

onremoveallclick 点击 RemoveAll 控件时发生的事件

onremoveclick 点击 remove 控件时发生的事件

ontopclick 点击 top 控件时发生的事件

onupclick 点击 up 控件时发生的事件

orderControlsVerticalAlign order 控件相对于 list 的纵向位置

orderControlsVisible 值为 false 时,up 和 down 控件将不显示

removeAllControlClass removeAll 控件的样式

removeAllControlLabel removeAll 控件的标签

removeControlClass remove 控件的样式

removeControlLabel remove 控件的标签

rendered 如果值为 false,则该组件将不渲染

required 如果值为 true,则该组件将验证空值

rowClasses 行的样式

rowKey 对于指定行的唯一区别标识

rowKeyVar 在 request 范围内可以访问的 row 的标识

rows 要显示的行数,以 0 为起始值

showButtonLabels 为控件显示标签

sourceCaptionLabel 定义源列表的标题

sourceListWidth 定义源列表的宽度

sourceSelection 控制服务器源的选择

sourceValue 定义将要显示的源表

stateVar 提供在客户端可以访问的组件状态变量

style 当组件被渲染时的样式

styleClass 当组件被渲染时的样式类

switchByClick 如果值为 true,则源列表和目标列表之间的拖拽将以点击的形式实现

CHAPTER 第 6 章 使用 RichFaces

295

6

续表

属 性 名 称 描 述

targetCaptionLabel 定义目标列表的标题

targetListWidth 定义目标列表的宽度

targetSelection 控制服务器端目标的选择

targetValue 定义将要显示的目标列表

topControlClass top 控件的样式

topControlLabel top 控件的标签

upControlClass up 控件的样式

upControlLabel up 控件的标签

validator 指定一个 validate 方法,在 JSF 生命周期的处理验证阶段将调用该方法

valueChangeListener 值变更时的监听器

var 在页面上定义一个列表

标签示例

OrderingList.jsf:

<rich:orderingList value="#{library.libraryAsList}" var="lib"

listHeight="300"

listWidth="350"

topControlLabel="置顶"

upControlLabel="向上"

downControlLabel="向下"

bottomControlLabel="置底">

<rich:column width="200">

<f:facet name="header">

歌曲名称

</f:facet>

<h:outputText value="#{lib.title}"></h:outputText>

</rich:column>

<rich:column width="180">

<f:facet name="header">

艺术家

</f:facet>

<h:outputText value="#{lib.album.artist.name}">

</h:outputText>

</rich:column>

</rich:orderingList>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

296

Library.java:

//省略了包的引用

//注意:这个类我们只是为了构建一个使用的数据实体

public class Library implements TreeNode {

private static final long serialVersionUID = -3530085227471752526L;

private Map artists = null;

private Object state1;

private Object state2;

private Map getArtists() {

if (this.artists==null) {

initData();

}

return this.artists;

}

public void addArtist(Artist artist) {

addChild(Long.toString(artist.getId()), artist);

}

public void addChild(Object identifier, TreeNode child) {

getArtists().put(identifier, child);

child.setParent(this);

}

public TreeNode getChild(Object id) {

return (TreeNode) getArtists().get(id);

}

public Iterator getChildren() {

return getArtists().entrySet().iterator();

}

//以下方法是为了实现 TreeNode接口

public Object getData() {

return this;

}

public TreeNode getParent() {

return null;

CHAPTER 第 6 章 使用 RichFaces

297

6

}

public boolean isLeaf() {

return getArtists().isEmpty();

}

public void removeChild(Object id) {

getArtists().remove(id);

}

public void setData(Object data) {

}

public void setParent(TreeNode parent) {

}

public String getType() {

return "library";

}

//定义一些后台要使用的方法

private long nextId = 0;

private long getNextId() {

return nextId++;

}

private Map albumCache = new HashMap();

private Map artistCache = new HashMap();

//使用艺术家名

private Artist getArtistByName(String name, Library library) {

Artist artist = (Artist)artistCache.get(name);

if (artist==null) {

artist = new Artist(getNextId());

artist.setName(name);

artistCache.put(name, artist);

library.addArtist(artist);

}

return artist;

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

298

}

//使用曲目

private Album getAlbumByTitle(String title, Artist artist) {

Album album = (Album)albumCache.get(title);

if (album==null) {

album = new Album(getNextId());

album.setTitle(title);

albumCache.put(title, album);

artist.addAlbum(album);

}

return album;

}

//初始化数据,在这里我们从一个文本文件里加载数据

private void initData() {

artists = new HashMap();

InputStream is = this.getClass().getClassLoader()

.getResourceAsStream("com/bluedot/ch05/richfaces/data.txt");

ByteArrayOutputStream os = new ByteArrayOutputStream();

byte[] rb = new byte[1024];

int read;

try {

do {

read = is.read(rb);

if (read>0) {

os.write(rb, 0, read);

}

} while (read>0);

String buf = os.toString();

StringTokenizer toc1 = new StringTokenizer(buf,"\n");

int index = 0;

while (toc1.hasMoreTokens()) {

index++;

String str = toc1.nextToken();

StringTokenizer toc2 = new StringTokenizer(str, "\t");

String songTitle = toc2.nextToken();

String artistName = toc2.nextToken();

String albumTitle = toc2.nextToken();

toc2.nextToken();

CHAPTER 第 6 章 使用 RichFaces

299

6

toc2.nextToken();

String albumYear = toc2.nextToken();

Artist artist = getArtistByName(artistName,this);

Album album = getAlbumByTitle(albumTitle, artist);

album.setYear(new Integer(albumYear));

Song song = new Song(getNextId());

song.setTitle(songTitle);

song.setTrackNumber(index);

album.addSong(song);

}

} catch (IOException e) {

throw new RuntimeException(e);

}

}

//一些 Bean的 get/set方法

public Object getState1() {

return state1;

}

public void setState1(Object state1) {

this.state1 = state1;

}

public Object getState2() {

return state2;

}

public void setState2(Object state2) {

this.state2 = state2;

}

public void walk(TreeNode node, List<TreeNode> appendTo,

Class<? extends TreeNode> type) {

if (type.isInstance(node)){

appendTo.add(node);

}

Iterator<Map.Entry<Object, TreeNode>> iterator = node.

getChildren();

while(iterator.hasNext()) {

walk(iterator.next().getValue(), appendTo, type);

}

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

300

}

public ArrayList getLibraryAsList(){

ArrayList appendTo = new ArrayList();

walk(this, appendTo, Song.class);

return appendTo;

}

}

faces-config.xml:

<managed-bean>

<managed-bean-name>library</managed-bean-name>

<managed-bean-class>demo.Library</managed-bean-class>

<managed-bean-scope>request</managed-bean-scope>

</managed-bean>

6.6.11 下拉菜单 <rich:dropDownMenu>

标签说明

本组件用于创建多级下拉式菜单。如图 6-34 所示为一个示例。

图 6-34 <rich:dropDownMenu>示例

标签属性

属 性 名 称 描 述

binding 组件绑定

direction

定义菜单列表弹出的方向。可以为下列值:

top-right:右上方

top-left:左上方

bottom-right:右下方

bottom-left:左下方

auto:自动,这个为默认值

CHAPTER 第 6 章 使用 RichFaces

301

6

续表

属 性 名 称 描 述

disabled 值为 true 时则该控件禁用

disabledItemClass 以空格分隔的禁用菜单项的样式类列表

disabledItemStyle 禁用控件时使用的样式

event 定义触发菜单项的行为,例如可以是 onclick

hideDelay 定义菜单失去焦点和菜单隐藏之间的时间

horizontalOffset 定义弹出菜单和标签之间的水平间距

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

itemClass 以空格分隔的菜单项的样式类列表

itemStyle 所要使用的菜单项的样式

jointPoint

定义弹出菜单和菜单项目之间的相对位置。可以是如下值:

auto:自动,这个为默认值

tr:右上方

tl:左上方

bl:左下方

br:右下方

oncollapse 菜单收起时触发的事件

onexpand 菜单展开时触发的事件

ongroupactivate 当组被激活时触发的事件

onitemselect 选项选中时触发的事件

onmousemove 鼠标光标在元素上移动时发生的事件

onmouseout 鼠标光标移开元素时发生的事件

onmouseover 当鼠标移动到组件上方时发的事件

popupWidth 设置弹出菜单的 小宽度

rendered 如果值为 false,则该组件将不渲染

selectItemClass 以空格分隔的样式类,用于被选中的项

selectItemStyle 用于被选中项的样式

showDelay 设定事件和菜单显示出来时的延时

style 当组件被渲染时的样式

styleClass 当组件被渲染时的样式类

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

302

续表

属 性 名 称 描 述

submitMode

设定菜单提交的模式,有如下 3 种:

ajax:以 ajax 方式提交。

server:普通的表单提交

none:action 和 actionListener 将被忽略,菜单将不会触发任何提交事件

value 定义组件值

verticalOffset 定义弹出菜单和标签之间的垂直间距

var 在页面上定义一个列表

标签示例

DropDownMenu.jsf:

<rich:toolBar>

<rich:dropDownMenu value="文件" jointPoint="auto">

<rich:menuItem submitMode="ajax" value="新建"

action="#{ddmenu.doNew}">

</rich:menuItem>

<rich:menuItem submitMode="ajax" value="打开"

action="#{ddmenu.doOpen}"/>

<rich:menuGroup value="另存为...">

<rich:menuItem submitMode="ajax" value="文本文件"

action="#{ddmenu.doSaveText}"/>

<rich:menuItem submitMode="ajax" value="PDF文件"

action="#{ddmenu.doSavePDF}"/>

</rich:menuGroup>

<rich:menuItem submitMode="ajax" value="关闭"

action="#{ddmenu.doClose}"/>

<rich:menuSeparator id="menuSeparator11"/>

<rich:menuItem submitMode="ajax" value="退出"

action="#{ddmenu.doExit}"/>

</rich:dropDownMenu>

</rich:toolBar>

CHAPTER 第 6 章 使用 RichFaces

303

6

Menu.java:

public class Menu {

private String current;

public String getCurrent() {

return this.current;

}

public void setCurrent(String current) {

this.current = current;

}

public String doNew() {

this.current="新建";

return null;

}

public String doOpen() {

this.current="打开";

return null;

}

public String doClose() {

this.current="关闭";

return null;

}

public String doSaveText() {

this.current="另存为文本文件";

return null;

}

public String doSavePDF() {

this.current="另存为 PDF文件";

return null;

}

public String doExit() {

this.current="退出";

return null;

}

}

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

304

faces-config.xml:

<managed-bean>

<managed-bean-name>ddmenu</managed-bean-name>

<managed-bean-class>demo.Menu</managed-bean-class>

<managed-bean-scope>request</managed-bean-scope>

</managed-bean>

6.6.12 模式对话框<rich:modalPanel>

标签说明

本组件实现了一个模式对话框窗口。当该对话框弹出时,主窗口将被锁定无法进行

任何操作。打开和关闭模式对话框都将通过 JavaScript 进行操作。如图 6-35 所示为一个

示例。

图 6-35 <rich:modalPanel>示例

标签属性

属 性 名 称 描 述

autosized 如果设定为 true,则模式对话框可自动调整大小

binding 组件绑定

controlsClass 应用于控件的 css 类

converter 转换器的 id

converterMessage 如果设定了该属性,converter 的消息将被此值所代替

CHAPTER 第 6 章 使用 RichFaces

305

6

续表

属 性 名 称 描 述

headerClass 用于窗口头部的样式类

height 定义组件的高度

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

keepVisualState 值为 true 时,在提交以后模式对话框的状态将被保存

left 定义组件的 X 坐标

minHeight 定义组件的 小高度

minWidth 定义组件的 小宽度

moveable 值为 true 时,模式对话框可以移动

onhide 关闭对话框时发生的事件

onshow 对话框弹出时发生的事件

rendered 如果值为 false,则该组件将不渲染

resizeable 值为 true 时可以变更对话框的大小

shadowDepth 对话框的阴影深度

shadowOpacity 对应于相应 HTML 的 opacity 属性

showWhenRendered 如果该值为 true,则对话框初始状态为打开状态

style 当组件被渲染时的样式

styleClass 当组件被渲染时的样式类

top 定义组件的 Y 坐标

tridentIVEngineSelectBehavior

定义如何处理基于 IE 6.0 的选择控件

disable:默认值,使用 disabled="true"的方式隐藏选择控件

hide:使用 visibility="hidden"的方式隐藏选择控件

validator 指定一个 validate 方法,在 JSF 生命周期的处理验证阶段将调用该方法

validatorMessage 用于显示验证信息

value 组件初始值

valueChangeListener 值变化的监听器

visualOptions 定义组件在客户端的选项

width 定义组件的宽度

zindex 等同于 HTML 中 z-index 概念

标签示例:

ModelPanel.jsf:

<h:form>

<a href="javascript:Richfaces.showModalPanel('demoShow',{width:200,

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

306

top:100})">模式对话框示例</a>

<rich:modalPanel id="demoShow" height="150" width="200"

zindex="2000" resizeable="false">

<f:facet name="header">

<h:outputText value="标题" />

</f:facet>

<f:facet name="controls">

<h:graphicImage value="/images/close.gif" style="cursor:

pointer" onclick="Richfaces.hideModalPanel('demoShow')" />

</f:facet>

<rich:panel>

<h:outputText value="请输入:"/>

<h:inputTextarea cols="10"></h:inputTextarea>

<a4j:commandButton value="确定"/>

</rich:panel>

</rich:modalPanel>

</h:form>

值得注意的是,RichFaces 是内置对象,只需在 JavaScript 中直接调用即可,RichFaces

框架会自动为你生成相应的 JavaScript 代码。如图 6-36 所示是通过 FireBug 查看到的

modalPanel 的 HTML 代码。

图 6-36 modalPanel 的 HTML 代码

6.6.13 面板条<rich:panelBar>

标签说明

面板条常用于组织客户端功能相关的多组控件,通常分类清晰,操作快捷。如图 6-37

CHAPTER 第 6 章 使用 RichFaces

307

6

所示为一个示例。

图 6-37 <rich:panelBar>示例

标签属性

属 性 名 称 描 述

binding 组件绑定

contentClass 用于组件内容的样式类

contentStyle 用于组件内容的样式

converter 转换器的 id

converterMessage 如果设定了该属性,converter 的消息将被此值所代替

headerClass 用于窗口头部的样式类

headerClassActive 当组件处于激活状态时口头部的样式类

headerStyle 用于窗口头部的样式

headerStyleActive 当组件处于激活状态时窗口头部的样式

height 定义组件的高度,可以是百分比或者像素,默认值是 100%

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

immediate

如果该组件被 Ajax 请求激活,标记该组件应该立即(在应用请求值阶段)被感兴趣的监

听器和动作处理,而不是等到调用应用程序阶段。同<h:commandButton>的 immediate

属性

onclick 点击时触发的事件

rendered 如果值为 false,则该组件将不渲染

required 是否是必需的(值为 true 或者 false)

requiredMessage 当不满足 requried 属性时所要显示的信息

selectedPanel 当前选择的 Panel 名

style 当组件被渲染时的样式

styleClass 当组件被渲染时的样式类

validator 指定一个 validate 方法,在 JSF 生命周期的处理验证阶段将调用该方法

validatorMessage 用于显示验证信息

value 组件初始值

valueChangeListener 值变化的监听器

width 定义组件的宽度

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

308

标签示例

PanelBar.jsf:

<h:form>

<rich:panel>

<rich:panelBar value="面板条示例">

<rich:panelBarItem label="分类 1">

<h:outputText value="这里是第 1个分类的内容" />

</rich:panelBarItem>

<rich:panelBarItem label="分类 2">

<h:outputText value="这里是第 2个分类的内容" />

</rich:panelBarItem>

<rich:panelBarItem label="分类 3">

<h:outputText value="这里是第 3个分类的内容" />

</rich:panelBarItem>

<rich:panelBarItem label="分类 4">

<h:outputText value="这里是第 4个分类的内容" />

</rich:panelBarItem>

<rich:panelBarItem label="分类 5">

<h:outputText value="这里是第 5个分类的内容" />

</rich:panelBarItem>

<rich:panelBarItem label="分类 6">

<h:outputText value="这里是第 6个分类的内容" />

</rich:panelBarItem>

</rich:panelBar>

</rich:panel>

</h:form>

6.6.14 可滚动的数据表格<rich:scrollableDataTable>

标签说明

可滚动的数据表格相对于普通的<h:dataTable>而言,具有以下特点:

高度灵活可配置的外观;

可变的表格内容;

滚动条滚动时动态从服务器端获取数据;

表格头部可以拖拽;

点击列表标题可以排序;

CHAPTER 第 6 章 使用 RichFaces

309

6

水平滚动滚动条时可以固定左边的一列或者多列;

有单选一行或者多选两种模式;

内建拖拽支持。

如图 6-38 所示为一个示例。

图 6-38 <rich:scrollableDataTable>示例

标签属性

属 性 名 称 描 述

activeClass 用于当前选择行的样式类

ajaxKeys 本属性用于定义 Ajax 请求后更新的行。

ajaxSingle 如果值为 true,则仅提交一个 field/link,而不是整个 form 中的内容

binding 组件绑定

bypassUpdates 如果值为 true,则在处理验证阶段后跳过更新模型值阶段到呈现阶段。可以用来验证组件的

输入值

captionClass 用空格分隔的用于标题的样式类列表

columnClasses 用逗号分隔的用于列的样式类

componentState EL 表达式,用于保存组件状态或者重定义状态

contentStyle 用于组件内容的样式

data 客户请求的序列化的数据(默认是 JSON),可以通过 data.foo 的方式访问

eventsQueue 用来避免在同一个事件上重复请求的队列的名字,可以用来减少周期事件(如按键,鼠标移

动)请求的次数

first 要显示的第一行,以 0 为起始值

focus 客户端请求完成后所要聚焦的控件 id

footerClass 用空格分隔的表格题脚样式类

frozenColCount 定义水平滚动时需要固定的左边的列,默认值是 0

headerClass 用于窗口头部的样式类

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

310

续表

属 性 名 称 描 述

height 定义组件的高度,可以是百分比或者像素,默认值是 500px

hideWhenScrolling 默认值是 false,如果设置为 true,则在滚动期间表格将隐藏,这样可以提高性能

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

ignoreDupResponses

如果请求响应与队列中 近一次的响应“相似”,则将该属性设置为 true 可以忽略该响

应。ignoreDupResponses="true"并没有取消服务器处理请求,它只是避免了服务器端不必

要的更新

limitToList 如果为 true,则仅更新 reRender 属性中指定的组件;否则(默认值)更新所有 Ajax Region

中的组件

onbeforedomupdate DOM 在客户端更新之前需要调用的 JavaScript 代码

oncomplete Ajax 请求完成时需要调用的 JavaScript 代码

onRowClick 点击表格内的一行时触发的事件

onRowDblClick 双击表格内的一行时触发的事件

onRowMouseDown 在行内按下鼠标时发生的事件

onRowMouseUp 在行内鼠标点击时发生的事件

onselectionchange 选择行变换时发生的事件

rendered 如果值为 false,则该组件将不渲染

requestDelay 在 JavaScript 事件上延迟(单位为 ms)发送 Ajax 请求。与事件队列一起使用,可以减少

键盘或者鼠标移动触发请求的次数

reRender

该组件调用 Ajax 请求后重新渲染的组件的 id(在 UIComopnent.findComponent()中

使用),可以是一个 id,也可以是用逗号分开的很多 id,还可以是数据或者集合的 EL

表达式

rowClasses 以逗号分隔的样式类,用于行

rowKey 标识行

rowKeyVar 可以在 request 范围内访问到的 rowKey 变量

rows 要显示列的数目,以 0 开始

scriptVar 用于 JavaScript 访问组件的变量

selectedClass 用于选中行的样式类

selection 标识当前选择的行

sortMode

定义排序的模式,可以有如下值:

single:仅针对一列排序

multi:多列排序

sortOrder 绑定一个类的属性,用于管理排序

stateVar 用于在客户端访问组件状态

status 请求状态组件的 id

CHAPTER 第 6 章 使用 RichFaces

311

6

续表

属 性 名 称 描 述

style 当组件被渲染时的样式

styleClass 当组件被渲染时的样式类

timeout Ajax 请求的超时时间(单位是 ms)

value 组件初始值

var 循环时当前行需要访问的值

width 定义组件的宽度

标签示例

ScrollableDataTable.jsf:

<h:form>

<rich:spacer height="30" />

<rich:scrollableDataTable rowKeyVar="rkv" frozenColCount="1"

height="400px"

width="700px" id="carList" rows="40" columnClasses="col"

value="#{dataTableScrollerBean.allCars}" var="category"

sortMode="single"

selection="#{dataTableScrollerBean.selection}">

<rich:column id="make">

<f:facet name="header">

<h:outputText styleClass="headerText" value="制造商" />

</f:facet>

<h:outputText value="#{category.make}" />

</rich:column>

<rich:column id="model">

<f:facet name="header">

<h:outputText styleClass="headerText" value="模型" />

</f:facet>

<h:outputText value="#{category.model}" />

</rich:column>

<rich:column id="price">

<f:facet name="header">

<h:outputText styleClass="headerText" value="价格" />

</f:facet>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

312

<h:outputText value="#{category.price}" />

</rich:column>

<rich:column id="mileage">

<f:facet name="header">

<h:outputText styleClass="headerText" value="哩数" />

</f:facet>

<h:outputText value="#{category.mileage}" />

</rich:column>

<rich:column width="200px" id="vin">

<f:facet name="header">

<h:outputText styleClass="headerText" value="识别码" />

</f:facet>

<h:outputText value="#{category.vin}" />

</rich:column>

<rich:column id="stock">

<f:facet name="header">

<h:outputText styleClass="headerText" value="库存" />

</f:facet>

<h:outputText value="#{category.stock}" />

</rich:column>

</rich:scrollabl�eDataTable>

<rich:spacer height="20px"/>

<a4j:commandButton value="显示当前用户的选择" reRender="table"

action="#{dataTableScrollerBean.takeSelection}"

oncomplete="javascript:Richfaces.showModalPanel('panel');"/>

</h:form>

<rich:modalPanel id="panel" autosized="true">

<f:facet name="header">

<h:outputText value="Selected Rows"/>

</f:facet>

<f:facet name="controls">

<span style="cursor:pointer"

onclick="javascript:Richfaces.hideModalPanel('panel')">X

</span>

</f:facet>

<rich:dataTable value="#{dataTableScrollerBean.selectedCars}"

var="sel" id="table">

<rich:column>

CHAPTER 第 6 章 使用 RichFaces

313

6

<f:facet name="header"><h:outputText value="Make" /></f:facet>

<h:outputText value="#{sel.make}" />

</rich:column>

<rich:column id="model">

<f:facet name="header"><h:outputText value="Model" /></f:facet>

<h:outputText value="#{sel.model}" />

</rich:column>

<rich:column id="price">

<f:facet name="header"><h:outputText value="Price" /></f:facet>

<h:outputText value="#{sel.price}" />

</rich:column>

<rich:column id="mileage">

<f:facet name="header"><h:outputText value="Mileage"/></f:facet>

<h:outputText value="#{sel.mileage}" />

</rich:column>

<rich:column id="stock">

<f:facet name="header"><h:outputText value="Stock"/></f:facet>

<h:outputText value="#{sel.stock}" />

</rich:column>

</rich:dataTable>

</rich:modalPanel>

….

DataTableScrollerBean.java:

public class DataTableScrollerBean {

private SimpleSelection selection = new SimpleSelection();

private ArrayList<DemoInventoryItem> selectedCars = new ArrayList

<DemoInventoryItem>();

private static int DECIMALS = 1;

private static int ROUNDING_MODE = BigDecimal.ROUND_HALF_UP;

private List <DemoInventoryItem> allCars = null;

public List <DemoInventoryItem> getAllCars() {

synchronized (this) {

if (allCars == null) {

allCars = new ArrayList<DemoInventoryItem>();

for (int k = 0; k <= 5; k++) {

try{

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

314

switch (k) {

case 0:

allCars.addAll(createCar("Chevrolet","Corvette", 5));

allCars.addAll(createCar("Chevrolet","Malibu", 8));

allCars.addAll(createCar("Chevrolet","S-10", 10));

allCars.addAll(createCar("Chevrolet","Tahoe", 6));

break;

case 1:

allCars.addAll(createCar("Ford","Taurus", 12));

allCars.addAll(createCar("Ford","Explorer", 11));

break;

case 2:

allCars.addAll(createCar("Nissan","Maxima", 9));

break;

case 3:

allCars.addAll(createCar("Toyota","4-Runner", 7));

allCars.addAll(createCar("Toyota","Camry", 15));

allCars.addAll(createCar("Toyota","Avalon", 13));

break;

case 4:

allCars.addAll(createCar("GMC","Sierra", 8));

allCars.addAll(createCar("GMC","Yukon", 10));

break;

case 5:

allCars.addAll(createCar("Infiniti","G35", 6));

break;

/*case 6:

allCars.addAll(createCar("UAZ","469", 6));

break;*/

default:

break;

}

}catch(Exception e){

System.out.println("!!!!!!loadAllCars Error: " +

e.getMessage());

e.printStackTrace();

}

CHAPTER 第 6 章 使用 RichFaces

315

6

}

}

}

return allCars;

}

public List<DemoInventoryItem> getTenRandomCars() {

List<DemoInventoryItem> result = new ArrayList<DemoInventoryItem>();

int size = getAllCars().size()-1;

for (int i = 0; i < 10; i++) {

result.add(getAllCars().get(rand(1, size)));

}

return result;

}

public int genRand() {

return rand(1,10000);

}

public List <DemoInventoryItem> createCar(String make, String model,

int

count){

ArrayList <DemoInventoryItem> iiList = null;

try{

int arrayCount = count;

DemoInventoryItem[] demoInventoryItemArrays = new Demo InventoryItem

[arrayCount];

for (int j = 0; j < demoInventoryItemArrays.length; j++){

DemoInventoryItem ii = new DemoInventoryItem();

ii.setMake(make);

ii.setModel(model);

ii.setStock(randomstring(6,7));

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

316

ii.setVin(randomstring(14,15));

ii.setMileage(new BigDecimal(rand(5000,80000)).setScale

(DECIMALS, ROUNDING_MODE));

ii.setMileageMarket(new BigDecimal(rand(25000,45000)).

setScale(DECIMALS, ROUNDING_MODE));

ii.setPrice(new Integer(rand(15000,55000)));

ii.setPriceMarket(new BigDecimal(rand(15000,55000)).

setScale(DECIMALS, ROUNDING_MODE));

ii.setDaysLive(rand(1,90));

ii.setChangeSearches(new BigDecimal(rand(0,5)).setScale

(DECIMALS, ROUNDING_MODE));

ii.setChangePrice(new BigDecimal(rand(0,5)).setScale

(DECIMALS, ROUNDING_MODE));

ii.setExposure(new BigDecimal(rand(0,5)).setScale

(DECIMALS,ROUNDING_MODE));

ii.setActivity(new BigDecimal(rand(0,5)).setScale

(DECIMALS,ROUNDING_MODE));

ii.setPrinted(new BigDecimal(rand(0,5)).setScale

(DECIMALS,ROUNDING_MODE));

ii.setInquiries(new BigDecimal(rand(0,5)).setScale

(DECIMALS,ROUNDING_MODE));

demoInventoryItemArrays[j] = ii;

}

iiList = new ArrayList<DemoInventoryItem>(Arrays.asList

(demoInventoryItemArrays));

}catch(Exception e){

System.out.println("!!!!!!createCategory Error: " +

e.getMessage());

e.printStackTrace();

}

return iiList;

}

public static int rand(int lo, int hi)

{

Random rn2 = new Random();

CHAPTER 第 6 章 使用 RichFaces

317

6

//System.out.println("**" + lo);

//System.out.println("**" + hi);

int n = hi - lo + 1;

int i = rn2.nextInt() % n;

if (i < 0)

i = -i;

return lo + i;

}

public static String randomstring(int lo, int hi)

{

int n = rand(lo, hi);

byte b[] = new byte[n];

for (int i = 0; i < n; i++)

b[i] = (byte)rand('A', 'Z');

return new String(b);

}

public SimpleSelection getSelection() {

return selection;

}

public void setSelection(SimpleSelection selection) {

System.out.println("Setting Started");

this.selection = selection;

System.out.println("Setting Complete");

}

public String takeSelection() {

getSelectedCars().clear();

Iterator<SimpleRowKey> iterator = getSelection().getKeys();

while (iterator.hasNext()){

SimpleRowKey key = iterator.next();

getSelectedCars().add(getAllCars().get(key.intValue()));

}

return null;

}

public ArrayList<DemoInventoryItem> getSelectedCars() {

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

318

return selectedCars;

}

public void setSelectedCars(ArrayList<DemoInventoryItem>

selectedCars) {

this.selectedCars = selectedCars;

}

}

DemoInventoryItem.java:

public class DemoInventoryItem implements Serializable {

private static final long serialVersionUID = 3467646494762783396L;

String make;

String model;

String stock;

String vin;

BigDecimal mileage;

BigDecimal mileageMarket;

Integer price;

BigDecimal priceMarket;

int daysLive;

BigDecimal changeSearches;

BigDecimal changePrice;

BigDecimal exposure;

BigDecimal activity;

BigDecimal printed;

BigDecimal inquiries;

//get/set方法

}

faces-config.xml:

<managed-bean>

<managed-bean-name>dataTableScrollerBean</managed-bean-name>

CHAPTER 第 6 章 使用 RichFaces

319

6

<managed-bean-class>demo.DataTableScrollerBean</managed-bean-class>

<managed-bean-scope>request</managed-bean-scope>

</managed-bean>

6.6.15 可折叠的面板<rich:simpleTogglePanel>

标签说明

这是一个可折叠的面板组件,用户可以通过点击标题头部打开或者折叠面板。

展开时如图 6-39 所示。

图 6-39 <rich:simpleTogglePanel>示例(一)

折叠后如图 6-40 所示。

图 6-40 <rich:simpleTogglePanel>示例(二)

标签属性

属 性 名 称 描 述

ajaxSingle 如果值为 true,则仅提交一个 field/link,而不是整个 form 中的内容

binding 组件绑定

bodyClass 定义面板内容的样式类

bypassUpdates 如果值为 true,则在处理验证阶段后跳过更新模型值阶段到呈现阶段。可以用来验

证组件的输入值

data 客户请求的序列化的数据(默认是 JSON),可以通过 data.foo 的方式访问

eventsQueue 用来避免在同一个事件上重复请求的队列的名字,可以用来减少周期事件(如按键、

鼠标移动)请求的次数

focus 客户端请求完成后所要聚焦的控件 id

headerClass 用于窗口头部的样式类

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

320

续表

属 性 名 称 描 述

height 定义组件的高度

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

ignoreDupResponses

如果请求响应与队列中 近一次的响应“相似”,则将该属性设置为 true 可以忽略该

响应。ignoreDupResponses="true"并没有取消服务器处理请求,它只是避免了服务器端

不必要的更新

immediate

如果该组件被 Ajax 请求激活,标记该组件应该立即(在应用请求值阶段)被感兴趣的

监听器和动作处理,而不是等到调用应用程序阶段。同<h:commandButton>的 immediate

属性

label 标签上要显示的标题

limitToList 如果值为 true,则仅更新 reRender 属性中指定的组件;否则(默认值)更新所有 Ajax

Region 中的组件

onbeforedomupdate DOM 在客户端更新之前需要调用的 JavaScript 代码

onclick 点击时发生的事件

oncomplete 客户端请求完成时发生的事件

ondblclick 双击时发生的事件

onkeydown 键按下时发生的事件

onkeypress 键按下并释放时发生的事件

onkeyup 键释放时发生的事件

onmousepress 按下鼠标按键时发生的事件

onmousemove 鼠标光标在元素上移动时发生的事件

onmouseout 鼠标光标移开元素时发生的事件

onmouseover 当鼠标移动到组件上方时发生的事件

onmouseup 当释放鼠标按键时发生的事件

opened 值为 false 时,面板默认为折叠状态

rendered 如果值为 false,则该组件将不渲染

requestDelay 在 JavaScript 事件上延迟 ( 单位为 ms )发送 Ajax 请求。与事件队列一起使用,可以

减少键盘或者鼠标移动触发请求的次数

status 请求状态组件的 id

style 当组件被渲染时的样式

styleClass 当组件被渲染时的样式类

switchType 可以是以下 3 种值:client、server 和 ajax

timeout Ajax 请求的超时时间(单位是 ms)

value 组件初始值

width 定义组件的宽度

CHAPTER 第 6 章 使用 RichFaces

321

6

标签示例

SimpleTogglePanel.jsf:

<h:form>

<rich:simpleTogglePanel switchType="client" label="尼康 S200">

<table>

<tr>

<td>

<h:graphicImage value="/images/s200.jpg"/>

</td>

<td>

<dl>

<dd><strong>参考价格:</strong><span class="red" id="ppckbj">

-</span>

<strong>商家报价:</strong><span class="red" id="ppsjbj">-</span>

</dd>

<dd><strong>有效像素:</strong>710万像素</dd>

<dd><strong>光学变焦倍数:</strong>3倍光学变焦</dd>

<dd><strong>传感器类型:</strong>CCD传感器</dd>

<dd><strong>传感器尺寸:</strong>1/2.5英寸</dd>

<dd><strong> 大分辨率:</strong>3072¡Á2304</dd>

<dd><strong>液晶屏尺寸:</strong>2.5英寸</dd>

<dd><strong>存储介质:</strong>SD卡,SDHC卡</dd>

</dl>

</td>

</tr>

</table>

</rich:simpleTogglePanel>

</h:form>

6.6.16 标签页<rich:tabPanel>

标签说明

使用标签页可以方便地显示一组内容,示例如图 6-41 所示。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

322

图 6-41 <rich:tabPanel>示例

标签属性

属 性 名 称 描 述

activeTabClass 当前活动标签的样式类

binding 组件绑定

contentClass 标签内容的样式类

contentStyle 标签内容的样式

converter 转换器的 id

converterMessage 如果设定了该属性,converter 的消息将被此值所代替

dir 文本方向

disabledTabClass 禁用标签的样式类

headerAlignment

设定标签头部的对齐方式,可以是下列值:

left:靠左对齐,默认值

right:靠右对齐

headerClass 用于窗口头部的样式类

headerSpacing 相邻标签间的间距

height 定义组件的高度

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

immediate

如果该组件被 Ajax 请求激活,标记该组件应该立即(在应用请求值阶段)被感兴趣的

监听器和动作处理,而不是等到调用应用程序阶段。同<h:commandButton>的 immediate

属性

inactiveTabClass 非活动标签的样式类

lang 标识生成该组件的语言

onclick 点击时发生的事件

ondblclick 双击时发生的事件

onkeydown 键按下时发生的事件

onkeypress 键按下并释放时发生的事件

onkeyup 键释放时发生的事件

onmousedown 按下鼠标按键时发生的事件

onmousemove 鼠标光标在元素上移动时发生的事件

CHAPTER 第 6 章 使用 RichFaces

323

6

续表

属 性 名 称 描 述

onmouseout 鼠标光标移开元素时发生的事件

onmouseover 当鼠标移动到组件上方时发生的事件

onmouseup 当释放鼠标按键时发生的事件

rendered 如果值为 false,则该组件将不渲染

required 是否是必需的(值为 true 或者 false)

requiredMessage 当不满足 requried 属性时所要显示的信息

selectedTab 定义被选中的标签名字,可以是表达式

style 当组件被渲染时的样式

styleClass 当组件被渲染时的样式类

switchType 可以是以下 3 种值:client、server 和 ajax

tabClass 用于所有标签的样式类

title 提示信息

validator 指定一个 validate 方法,在 JSF 生命周期的处理验证阶段将调用该方法

validatorMessage 用于显示验证信息

value 组件初始值

valueChangeListener 值变化的监听器

width 定义组件的宽度

标签示例

TabPanel.jsf:

<h:form>

<rich:tabPanel switchType="ajax" selectedTab="sony" title="数码相机">

<rich:tab label="奥林巴斯" name="olym">

<table>

<tr>

<td>

<h:graphicImage value="/images/olym.jpg"/>

</td>

<td>

这里是奥林巴斯的简介

</td>

</tr>

</table>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

324

</rich:tab>

<rich:tab label="索尼" name="sony">

<table>

<tr>

<td>

<h:graphicImage value="/images/sony.jpg"/>

</td>

<td>

这里是索尼的简介

</td>

</tr>

</table>

</rich:tab>

<rich:tab label="尼康" name="nikon">

<table>

<tr>

<td>

<h:graphicImage value="/images/nikon.jpg"/>

</td>

<td>

这里是尼康的简介

</td>

</tr>

</table>

</rich:tab>

</rich:tabPanel>

</h:form>

6.6.17 工具提示<rich:toolTip>

标签说明

该组件用于弹出提示信息,帮助用户进行操作。

如图 6-42 所示为一个示例。

CHAPTER 第 6 章 使用 RichFaces

325

6

图 6-42 <rich:toolTip>示例

标签属性

属 性 名 称 描 述

binding 组件绑定

direction 定义日期控件弹出时的方位。可以是如下值:top-left(左上)、top-right(右上)、bottom-left

(左下)、bottom-right(右下,这是默认值)和 auto(自动)

disabled 值为 false 时,组件将被渲染到页面上,但 JavaScript 函数将被禁用

event 触发工具提示框显示的事件,默认值是 onmouseover 事件

headerAlignment

设定标签头部的对齐方式,可以是下列值:

left:靠左对齐,默认值

right:靠右对齐

followMouse 如果该值设置为 true,提示框将跟随鼠标移动

hideDelay 提示框隐藏前的延时

horizontalOffset 设置提示框和鼠标之间的水平距离

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

layout

布局的方式,可以是下列值:

inline:提示框将以 div 的方式渲染

block:提示框将以 span 的方式渲染

mode 提示框内数据的加载模式,可以是 client 或者 ajax

onclick 点击时发生的事件

oncomplete 在客户端请求完成时调用的 JavaScript 代码

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

326

续表

属 性 名 称 描 述

ondblclick 双击时发生的事件

onhide 隐藏时发生的事件

onkeydown 键按下时发生的事件

onkeypress 键按下并释放时发生的事件

onkeyup 键释放时发生的事件

onmousedown 按下鼠标按键时发生的事件

onmousemove 鼠标光标在元素上移动时发生的事件

onmouseout 鼠标光标移开元素时发生的事件

onmouseover 当鼠标移动到组件上方时发生的事件

onmouseup 当释放鼠标按键时发生的事件

onshow 出现时发生的事件

rendered 如果值为 false,则该组件将不渲染

showDelay 设定事件和弹出框显示出来时的延时

required 是否是必需的(值为 true 或者 false)

requiredMessage 当不满足 requried 属性时所要显示的信息

selectedTab 定义被选中的标签名字,可以是表达式

style 当组件被渲染时的样式

styleClass 当组件被渲染时的样式类

value 组件初始值

verticalOffset 提示框和鼠标之间的纵向距离

zorder 等同于 CSS 中的 z-index

标签示例

ToolTip.jsf:

<h:form>

<h:outputText

value="提示信息是在服务器端渲染的,请把鼠标放在制造商这一列的任何一行."

id="tt3">

<rich:toolTip direction="top-right" mode="ajax" verticalOffset="5"

zorder="200" styleClass="tooltip" layout="block">

<f:facet name="defaultContent">

CHAPTER 第 6 章 使用 RichFaces

327

6

<strong>请稍后...</strong>

</f:facet>

<span>本消息是在服务器端 <strong>渲染</strong>的</span>

<h:panelGrid columns="2">

<h:outputText value="请求的提示信息:" />

<h:outputText value="#{toolTipData.tooltipCounter}"

styleClass="tooltipData" />

<h:outputText value=" 新请求:" />

<h:outputText value="#{toolTipData.tooltipDate}"

styleClass="tooltipData">

<f:convertDateTime pattern="mm:ss.SSS" />

</h:outputText>

</h:panelGrid>

</rich:toolTip>

</h:outputText>

<rich:dataTable value="#{toolTipData.vehicles}" width="400"

var="vehicle" rowKeyVar="row">

<rich:column>

<f:facet name="header">

<h:outputText value="序号" />

</f:facet>

<h:outputText value="#{row+1}" />

</rich:column>

<rich:column>

<f:facet name="header">

<h:outputText value="制造商" />

</f:facet>

<h:outputText id="make" value="#{vehicle.make}">

<rich:toolTip direction="top-right" mode="ajax" delay="30"

styleClass="tooltip" immediate="true" layout="block">

<a4j:actionparam name="key" value="#{row}"

assignTo="#{toolTipData.currentVehicleIndex}" />

<h:panelGrid columns="4">

<f:facet name="header">

<h:outputText value="车辆详细信息:" />

</f:facet>

<h:outputText value="制造商:" />

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

328

<h:outputText value="#{vehicle.make}"

styleClass="tooltipData" />

<h:outputText value="模型:" />

<h:outputText value="#{vehicle.model}"

styleClass="tooltipData" />

<h:outputText value="年份:" />

<h:outputText value="#{vehicle.year}"

styleClass="tooltipData" />

<h:outputText value="哩数:" />

<h:outputText value="#{vehicle.milage}"

styleClass="tooltipData" />

<h:outputText value="编码:" />

<h:outputText value="#{vehicle.zip}"

styleClass="tooltipData" />

<h:outputText value="列表时间:" />

<h:outputText value="#{vehicle.listed}"

styleClass="tooltipData">

<f:convertDateTime dateStyle="short" />

</h:outputText>

<f:facet name="footer">

<h:panelGroup>

<h:outputText value="识别码: " />

<h:outputText value="#{vehicle.vin}"

styleClass="tooltipData" />

</h:panelGroup>

</f:facet>

</h:panelGrid>

</rich:toolTip>

</h:outputText>

</rich:column>

<rich:column>

<f:facet name="header">

<h:outputText value="模型" />

</f:facet>

<h:outputText value="#{vehicle.model}" />

</rich:column>

<rich:column>

CHAPTER 第 6 章 使用 RichFaces

329

6

<f:facet name="header">

<h:outputText value="年份" />

</f:facet>

<h:outputText value="#{vehicle.year}" />

</rich:column>

</rich:dataTable>

</h:form>

ToolTipData.java:

public class ToolTipData {

private int tooltipCounter = 0;

private List vehicles = null;

private int currentVehicleIndex = -1;

public int getTooltipCounter() {

try {

Thread.sleep(500);

} catch (InterruptedException e) {

}

return tooltipCounter++;

}

public Date getTooltipDate() {

return new Date();

}

public List getVehicles() {

if (vehicles==null) {

vehicles = Vehicle.allVehicles(15);

return vehicles;

} else {

return vehicles;

}

}

public Vehicle getCurrentVehicle() {

if (currentVehicleIndex>0 &&

currentVehicleIndex<getVehicles().size()) {

return (Vehicle) getVehicles().get(currentVehicleIndex);

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

330

} else {

return null;

}

}

public int getCurrentVehicleIndex() {

return currentVehicleIndex;

}

public void setCurrentVehicleIndex(int currentVehicleIndex) {

this.currentVehicleIndex = currentVehicleIndex;

}

}

Vehicle.java:

public class Vehicle {

public String make;

public String model;

public Integer year;

public Integer milage;

public String vin;

public Integer zip;

public Date listed;

public Vehicle(String make, String model) {

this.make = make;

this.model = model;

this.listed = new Date();

}

public static Vehicle list[] = {

new Vehicle("Ford", "Taurus"),

new Vehicle("Ford", "Mustang"),

new Vehicle("Ford", "Focus"),

new Vehicle("Ford", "Thinderbird"),

new Vehicle("BMW", "Z3"),

new Vehicle("BMW", "323i"),

new Vehicle("BMW", "521"),

new Vehicle("BMW", "Mustang"),

new Vehicle("Audi", "A4"),

CHAPTER 第 6 章 使用 RichFaces

331

6

new Vehicle("Audi", "A6"),

new Vehicle("Toyota", "Camry"),

new Vehicle("Toyota", "Corolla"),

new Vehicle("Toyota", "Matrix"),

new Vehicle("Honda", "Accord"),

new Vehicle("Honda", "Civic")

};

public static List allVehicles(int size) {

List ret = new ArrayList();

for (int counter=0;counter<size;counter++) {

Vehicle car = (Vehicle)RandomDataHelper.random(list);

car.milage = new Integer(RandomDataHelper.random(10000, 100000));

car.vin = RandomDataHelper.randomString(32);

car.year = new Integer(RandomDataHelper.random(2000, 2005));

car.zip = new Integer(RandomDataHelper.random(94500,94600));

ret.add(car);

}

return ret;

}

}

faces-config.xml:

<managed-bean>

<managed-bean-name>toolTipData</managed-bean-name>

<managed-bean-class>demo.ToolTipData</managed-bean-class>

<managed-bean-scope>request</managed-bean-scope>

</managed-bean>

6.6.18 自动完成对话框<rich:suggestionbox>

标签说明

该组件类似于一个输入框,当用户输入时将触发 Ajax 事件向服务器端发送请求,服务

器端将返回一个列表供用户选择。如图 6-43 所示为一个示例。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

332

图 6-43 <rich:suggestionbox>示例

标签属性

属 性 名 称 描 述

ajaxSingle 如果值为 true,则仅提交一个 field/link,而不是整个 form 中的内容

bgcolor 不再提倡使用

binding 组件绑定

border 指定表格外框的宽度,仅接收像素值

bypassUpdates 如果值为 true,则在处理验证阶段后跳过更新模型值阶段到呈现阶段。可以用来验

证组件的输入值

cellpadding 等同于 HTML table 中的概念

cellspacing 等同于 HTML table 中的概念

converter 所要引用的转换器的 id

data 客户请求的序列化的数据(默认是 JSON),可以通过 data.foo 的方式访问

dir 文本方向

entryClass 定义自动完成对话框中行的样式类

eventsQueue 用来避免在同一个事件上重复请求的队列的名字,可以用来减少周期事件(如按键、

鼠标移动)请求的次数

fetchValue 当用户选择后定义返回到输入框内的值,如果该属性没有设置,则用户选择行的所

有数据都将返回

first 要显示的第一行,以 0 为起始值

focus 客户端请求完成后所要聚焦的控件 id

frame

设定边框,可以是如下值或者它们的组合

void:没有边框,这是默认值

above:仅有上边框

bellow:仅有下边框

hsides:仅有上边框和下边框

vsides:仅有左边框和右边框

lhs:仅有左边框

rhs:仅有右边框

box:4 个边框

border:所有边框

CHAPTER 第 6 章 使用 RichFaces

333

6

续表

属 性 名 称 描 述

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

ignoreDupResponses

如果请求响应与队列中 近一次的响应“相似”,则将该属性设置为 true 可以忽略

该响应。ignoreDupResponses="true"并没有取消服务器处理请求,它只是避免了服务

器端不必要的更新

immediate

如果该组件被 Ajax 请求激活,标记该组件应该立即(在应用请求值阶段)被感兴趣

的监听器和动作处理,而不是等到调用应用程序阶段。同<h:commandButton>的

immediate 属性

lang 标识生成该组件的语言

limitToList 如果为 true,则仅更新 reRender 属性中指定的组件;否则(默认值)更新所有 Ajax

Region 中的组件

minChars 激活查询的 小字符长度

nothingLabel 查询是空值时显示的信息

onselect 选择时触发的事件

onsubmit 提交时触发的事件

param 通过 HTTP 请求带的参数,参数值为输入框内的值

popupClass 弹出框的样式类

popupStyle 弹出框的样式

rendered 如果值为 false,则该组件将不渲染

rowClasses 以逗号分隔的行的样式类

rowNumber 行号

rules

定义表格中单元格的规则

none:没有规则,默认值

groups:规则仅用于行之间(参看 THEAD、TFOOT 和 TBODY)或者列之间(参

看 COLGROUP、COL)

rows:仅用于行

cols:仅用于列

all:运用于所有的行和列

selectedClass 定义被选中行的样式类

selectValueClass 定义被选中单元格的样式类

selfRendered 设置为 true 时,强制从存储的组件树中渲染响应,略过页面处理过程,可以用于

提高性能。在 dataTable 等具有循环功能的组件中必须设置为 true

shadowDepth 阴影深度

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

334

续表

属 性 名 称 描 述

shadowOpacity 阴影不透明程度

status 请求组件状态的组件 id

style 当组件被渲染时的样式

styleClass 当组件被渲染时的样式类

suggestionAction 获取查询列表的方法名,该方法含有一个 Object 参数,而且必须返回一个 collection

summary 简介

timeout Ajax 请求的超时时间(单位是 ms)

Title 提示信息

Tokens 一组符号列表。列表中的一个符号出现以后,自动完成框将再次生效

upValue upValue

value 组件初始值

var 循环时用于保存当前变量,等同于 dataTable 中的 var

width 定义组件的宽度

zindex 等同于 CSS 中的 z-index

标签示例

SuggestionBox.jsf:

<h:form id="suggestionbox_form">

<p>请在下面的输入框中输入首府的名字:</p>

<h:inputText value="#{capitalsBean.capital}" id="text" />

<rich:suggestionbox id="suggestionBoxId" for="text" tokens=",

["rules="#{suggestionBox.rules}"

suggestionAction="#{capitalsBean.autocomplete}" var="result"

fetchValue="#{result.name}" rows="#{suggestionBox.intRows}"

first="#{suggestionBox.intFirst}"

minChars="#{suggestionBox.minchars}"

shadowOpacity="#{suggestionBox.shadowOpacity}"

border="#{suggestionBox.border}" width="#{suggestionBox.width}"

height="#{suggestionBox.height}"

shadowDepth="#{suggestionBox.shadowDepth}"

cellpadding="#{suggestionBox.cellpadding}"

nothingLabel="没有查询结果" columnClasses="center">

<h:column>

CHAPTER 第 6 章 使用 RichFaces

335

6

<f:facet name="header">

<h:outputText value="123"></h:outputText>

</f:facet>

<h:graphicImage value="#{result.stateFlag}" />

</h:column>

<h:column>

<h:outputText value="#{result.name}" />

</h:column>

<h:column>

<h:outputText value="#{result.state}"

style="font-style:italic" />

</h:column>

</rich:suggestionbox>

<rich:spacer height="30px" />

<h:panelGrid columns="2" border="0">

<h:outputText value="边框:" />

<rich:inputNumberSlider minValue="1" maxValue="5"

value="#{suggestionBox.border}">

<a4j:support event="onchange" reRender="suggestionBoxId" />

</rich:inputNumberSlider>

<h:outputText value="宽度:" />

<rich:inputNumberSlider minValue="150" maxValue="350" step="50"

value="#{suggestionBox.width}">

<a4j:support event="onchange" reRender="suggestionBoxId" />

</rich:inputNumberSlider>

<h:outputText value="高度:" />

<rich:inputNumberSlider minValue="100" maxValue="300" step="50"

value="#{suggestionBox.height}">

<a4j:support event="onchange" reRender="suggestionBoxId" />

</rich:inputNumberSlider>

<h:outputText value="阴影深度:" />

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

336

<rich:inputNumberSlider minValue="3" maxValue="6"

value="#{suggestionBox.shadowDepth}">

<a4j:support event="onchange" reRender="suggestionBoxId" />

</rich:inputNumberSlider>

<h:outputText value="阴影不透明度:" />

<rich:inputNumberSlider minValue="1" maxValue="9"

value="#{suggestionBox.shadowOpacity}">

<a4j:support event="onchange" reRender="suggestionBoxId" />

</rich:inputNumberSlider>

<h:outputText value="单元格填充:" />

<rich:inputNumberSlider minValue="1" maxValue="20"

value="#{suggestionBox.cellpadding}">

<a4j:support event="onchange" reRender="suggestionBoxId" />

</rich:inputNumberSlider>

</h:panelGrid>

</h:form>

SuggestionBox.java:

public class SuggestionBox implements Serializable {

private static final long serialVersionUID = 1L;

private ArrayList tokens;

private String rows;

private String first;

private String cellspacing;

private String cellpadding;

private String minchars;

private String frequency;

private String rules;

private boolean check;

CHAPTER 第 6 章 使用 RichFaces

337

6

private String shadowDepth = Integer.toString(

SuggestionBoxRenderer.SHADOW_DEPTH);

private String border = "1";

private String width = "250";

private String height = "150";

private String shadowOpacity = "4";

public SuggestionBox() {

this.rows = "0";

this.first = "0";

this.cellspacing = "2";

this.cellpadding = "2";

this.minchars = "1";

this.frequency = "0";

this.rules = "none";

}

}

CapitalsBean.java:

public class CapitalsBean {

private ArrayList<Capital> capitals = new ArrayList<Capital>();

private String capital = "";

public List<Capital> autocomplete(Object suggest) {

String pref = (String)suggest;

ArrayList<Capital> result = new ArrayList<Capital>();

Iterator<Capital> iterator = getCapitals().iterator();

while (iterator.hasNext()) {

Capital elem = ((Capital) iterator.next());

if ((elem.getName() != null && elem.getName().toLowerCase().

indexOf(pref.toLowerCase()) == 0) || "".equals(pref))

{

result.add(elem);

}

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

338

}

return result;

}

public CapitalsBean() {

URL rulesUrl = getClass().getResource("capitals-rules.xml");

Digester digester = DigesterLoader.createDigester(rulesUrl);

digester.push(this);

try {

digester.parse(getClass().getResourceAsStream("capitals.xml"));

} catch (IOException e) {

throw new FacesException(e);

} catch (SAXException e) {

throw new FacesException(e);

}

}

public String addCapital(Capital capital) {

capitals.add(capital);

return null;

}

public ArrayList<Capital> getCapitals() {

return capitals;

}

public String getCapital() {

return capital;

}

public void setCapital(String capital) {

his.capital = capital;

}

}

CHAPTER 第 6 章 使用 RichFaces

339

6

Capital.java:

public class Capital implements Serializable {

private static final long serialVersionUID = -1042449580199397136L;

private boolean checked=false;

private String name;

private String state;

private String timeZone;

}

faces-config.xml:

<managed-bean-name>capitalsBean</managed-bean-name>

<managed-bean-class>demo.CapitalsBean</managed-bean-class>

<managed-bean-scope>request</managed-bean-scope>

</managed-bean>

<managed-bean>

<managed-bean-name>suggestionBox</managed-bean-name>

<managed-bean-class>demo.SuggestionBox</managed-bean-class>

<managed-bean-scope>request</managed-bean-scope>

</managed-bean>

6.6.19 树<rich:recursiveTreeNodesAdaptor>

标签说明

树形组件是 常用的组件之一,RichFaces 同样也实现了树形组件。<rich: recursiveTree

NodesAdaptor>适用于具有递归类型的数据结构。当然,如果你还需要更多自由度的话,则

可以使用<rich:treeNode>定制。如图 6-44 所示为一个示例。

图 6-44 <rich:recursiveTreeNodesAdaptor>示例

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

340

标签属性

属 性 名 称 描 述

binding 组件绑定

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

included 布尔表达式,两个集合被处理时用于定义哪个集合要处理

includedNode 布尔表达式,用于定义哪个元素要处理

includedRoot 布尔表达式,用于定义哪个集合元素要作为“根”

nodes 定义在非顶部循环时使用的集合

rendered 如果值为 false,则该组件将不渲染

roots 定义顶部要使用的集合

var request 范围内的循环变量

标签示例

Tree.jsf:

<rich:separator height="1" style="padding:10px 0" />

<h:form>

<rich:tree style="width:300px" switchType="ajax"

stateAdvisor="#{treeDemoStateAdvisor}">

<rich:recursiveTreeNodesAdaptor

roots="#{fileSystemBean.sourceRoots}" var="item"

nodes="#{item.nodes}" />

</rich:tree>

</h:form>

<rich:separator height="1" style="padding:10px 0" />

FileSystemBean.java:

public class FileSystemBean {

private static String SRC_PATH = "/";

private FileSystemNode[] srcRoots;

public synchronized FileSystemNode[] getSourceRoots() {

if (srcRoots == null) {

srcRoots = new FileSystemNode(SRC_PATH).getNodes();

}

CHAPTER 第 6 章 使用 RichFaces

341

6

return srcRoots;

}

}

FileSystemNode.java:

public class FileSystemNode {

private String path;

private static FileSystemNode[] CHILDREN_ABSENT = new FileSystemNode[0];

private FileSystemNode[] children;

private String shortPath;

public FileSystemNode(String path) {

this.path = path;

int idx = path.lastIndexOf('/');

if (idx != -1) {

shortPath = path.substring(idx + 1);

} else {

shortPath = path;

}

}

public synchronized FileSystemNode[] getNodes() {

if (children == null) {

FacesContext facesContext = FacesContext.getCurrentInstance();

ExternalContext externalContext = facesContext.getExternalContext();

Set resourcePaths = externalContext.getResourcePaths(this.path);

if (resourcePaths != null) {

Object[] nodes = (Object[]) resourcePaths.toArray();

children = new FileSystemNode[nodes.length];

for (int i = 0; i < nodes.length; i++) {

String nodePath = nodes[i].toString();

if (nodePath.endsWith("/")) {

nodePath = nodePath.substring(0, nodePath.length() - 1);

}

children[i] = new FileSystemNode(nodePath);

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

342

}

} else {

children = CHILDREN_ABSENT;

}

}

return children;

}

public String toString() {

return shortPath;

}

}

TreeDemoStateAdvisor.java:

public class TreeDemoStateAdvisor implements TreeStateAdvisor {

public Boolean adviseNodeOpened(UITree tree) {

if (!PostbackPhaseListener.isPostback()) {

Object key = tree.getRowKey();

TreeRowKey treeRowKey = (TreeRowKey) key;

if (treeRowKey == null || treeRowKey.depth() <= 2) {

return Boolean.TRUE;

}

}

return null;

}

public Boolean adviseNodeSelected(UITree tree) {

return null;

}

}

faces-config.xml:

<managed-bean>

CHAPTER 第 6 章 使用 RichFaces

343

6

<managed-bean-name>treeDemoStateAdvisor</managed-bean-name>

<managed-bean-class>

demo.TreeDemoStateAdvisor

</managed-bean-class>

<managed-bean-scope>request</managed-bean-scope>

</managed-bean>

<managed-bean>

<managed-bean-name>fileSystemBean</managed-bean-name>

<managed-bean-class>demo.FileSystemBean</managed-bean-class>

<managed-bean-scope>request</managed-bean-scope>

</managed-bean>

6.6.20 内容菜单<rich:contextMenu>

标签说明

这是一个内容菜单组件,你可以在页面的众多元素之上定义事件(例如:onmouseover

和 onclick 等),用于弹出内容菜单。如图 6-45 所示为一个示例。

图 6-45 <rich:contextMenu>示例

标签属性

属 性 名 称 描 述

attached 如果属性值为 true,则该组件依附在父组件之上

binding 组件绑定

disableDefaultMenu 禁用默认调整事件

disabledItemClass 以空格分隔的 CSS 样式类列表,该样式将用于组件中禁用的菜单项

disabledItemStyle 用于组件禁用项的样式

event 定义触发内容菜单的事件

hideDelay 定义失去焦点和菜单关闭之间的时间

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

itemClass 用于菜单项的以空格分隔的 CSS 样式类列表

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

344

续表

属 性 名 称 描 述

itemStyle 用于菜单项的样式

oncollapse 菜单关闭时发生的事件

onexpand 菜单展开时发生的事件

ongroupactivate HTML:脚本表达式,某些组激活时发生的事件

onitemselect HTML:脚本表达式,项被选择时发生的事件

onmousemove 鼠标移动时发生的事件

onmouseout 鼠标移出时发生的事件

onmouseover 鼠标在组件之上移动时发生的事件

popupWidth 设置宽度的 小值

rendered 如果值为 false,则该组件将不渲染

selectItemClass 用于选择项的以空格分隔的样式类列表

selectItemStyle 用于选择项的样式类

showDelay 触发事件以后和显示出菜单之间的时间

style 用于组件的样式

styleClass 用于组件的样式类

submitMode 提交样式,可以是如下 3 种:ajax、server(默认值)和 none

标签示例

ContextMenu.jsf:

<h:panelGrid columns="1" columnClasses="cent">

<h:outputText

value="请在图片上点击右键弹出菜单"

style="font-weight:bold" id="label" />

<h:panelGroup id="picture">

<h:graphicImage value="/images/pic1.jpg" id="pic" />

<rich:contextMenu event="oncontextmenu" attached="true"

submitMode="none">

<rich:menuItem value="Zoom In" onclick="enlarge();" id="zin">

</rich:menuItem>

<rich:menuItem value="Zoom Out" onclick="decrease();"id="zout">

</rich:menuItem>

</rich:contextMenu>

CHAPTER 第 6 章 使用 RichFaces

345

6

</h:panelGroup>

</h:panelGrid>

<script type="text/javascript">

function enlarge() {

document.getElementById('form1:pic').width = document. getElementById

('form1:pic').width * 1.1;

document.getElementById('form1:pic').height =

document.getElementById('form1:pic').height * 1.1;

}

function decrease() {

document.getElementById('form1:pic').width =

document.getElementById('form1:pic').width * 0.9;

document.getElementById('form1:pic').height =

document.getElementById('form1:pic').height * 0.9;

}

</script>

6.6.21 页面效果<rich:effect>

标签说明

该组件为页面提供一系列的特效,可以为用户提供一系列的丰富体验,其内部是通过

调用 script.aculo.us 的 JavaScript 库实现的。该文件位于 richfaces-ui-3.1.4.GA.jar 下的

org.richfaces.renderkit.html.scripts 包内。如图 6-46 所示为一个示例。

图 6-46 <rich:effect>示例

标签属性

属 性 名 称 描 述

binding 组件绑定

disableDefault 禁用对目标事件的动作(在 JavaScript 事件后添加 return false 实现)

event 触发效果的事件

for 目标组件的 id

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

346

续表

属 性 名 称 描 述

name 生成的 JavaScript 的名字

params 传递给效果的参数名。例如:{duration:0.2,from:1.0,to:0.1}

rendered 如果值为 false,则该组件将不渲染

targeted 目标组件的 id,如果 for 没有定义的话,则由该属性定义

type 定义效果的类型,可以是如下值:

Fade、Bind 和 Opacity

标签示例

Effect.jsf:

<h:panelGrid columns="3" columnClasses="cell">

<rich:panel id="fadebox" styleClass="box">

<f:facet name="header">褪色效果</f:facet>

<rich:effect event="onclick" type="Fade" />

<rich:effect event="onclick" for="fadebox" type="Appear"

params="delay:3.0,duration:0.5" />

<h:outputText value="点击查看" />

</rich:panel>

<rich:panel id="bdbox" styleClass="box">

<f:facet name="header">拉下效果</f:facet>

<rich:effect event="onclick" type="BlindDown" params="duration:0.8" />

<h:outputText value="点击查看" />

</rich:panel>

<rich:panel id="bubox" styleClass="box">

<f:facet name="header">拉上效果</f:facet>

<rich:effect event="onclick" type="BlindUp" params="duration:0.8" />

<rich:effect event="onclick" for="bubox" type="Appear"

params="delay:3.0,duration:0.5" />

<h:outputText value="点击查看" />

</rich:panel>

<rich:panel id="opacitybox" styleClass="box">

<f:facet name="header">透明效果</f:facet>

<rich:effect event="onclick" type="Opacity"

params="duration:0.8, from:1.0, to:0.1" />

<rich:effect event="onclick" for="opacitybox" type="Appear"

params="delay:3.0,duration:0.5" />

CHAPTER 第 6 章 使用 RichFaces

347

6

<h:outputText value="点击查看" />

</rich:panel>

<rich:panel id="switchbox" styleClass="box">

<f:facet name="header">关闭效果</f:facet>

<rich:effect event="onclick" type="SwitchOff" params="duration:0.8" />

<rich:effect event="onclick" for="switchbox" type="Appear"

params="delay:3.0,duration:0.5" />

<h:outputText value="点击查看" />

</rich:panel>

<rich:panel id="dobox" styleClass="box">

<f:facet name="header">退出效果</f:facet>

<rich:effect event="onclick" type="DropOut" params="duration:0.8"/>

<rich:effect event="onclick" for="dobox" type="Appear"

params="delay:3.0,duration:0.5" />

<h:outputText value="点击查看" />

</rich:panel>

<rich:panel id="highlightbox" styleClass="box">

<f:facet name="header">高亮效果</f:facet>

<rich:effect event="onclick" type="Highlight"

params="duration:0.8" />

<rich:effect event="onclick" for="highlightbox" type="Appear"

params="delay:3.0,duration:0.5" />

<h:outputText value="点击查看" />

</rich:panel>

<rich:panel id="foldbox" styleClass="box">

<f:facet name="header">折叠效果</f:facet>

<rich:effect event="onclick" type="Fold" params="duration:0.8" />

<rich:effect event="onclick" for="foldbox" type="Appear"

params="delay:3.0,duration:0.5" />

<h:outputText value="点击查看" />

</rich:panel>

<rich:panel id="squishbox" styleClass="box">

<f:facet name="header">压缩效果</f:facet>

<rich:effect event="onclick" type="Squish" params="duration: 0.8" />

<rich:effect event="onclick" for="squishbox" type="Appear"

params="delay:3.0,duration:0.5" />

<h:outputText value="点击查看" />

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

348

</rich:panel>

</h:panelGrid>

6.6.22 数据表格<rich:dataTable>

标签说明

JSF 的数据表格是一个令人激动的特性,RichFaces 更是加强了这一特性,还为其增加

了 Ajax 功能。如图 6-47 所示为一个含有分页的示例。

图 6-47 <rich:dataTable>示例

标签属性

属 性 名 称 描 述

ajaxKeys 本属性用于定义 Ajax 请求后更新的行

align 已不提倡使用。设置对齐的方式,可以是 left|center|right

bgcolor 已不提倡使用。设置背景颜色

binding 组件绑定

border 设置表格中隔线的宽度

captionClass 用空格分隔的用于标题的样式类列表

captionStyle 标题的样式

cellpadding 设置单元格和其内容之间的间距

cellspacing 设置单元格之间的间距

columnClasses 用逗号分隔的用于列的样式类

columns 设置列的数目

CHAPTER 第 6 章 使用 RichFaces

349

6

续表

属 性 名 称 描 述

columnsWidth 用逗号分隔的列的宽度,可以是像素或者百分数

componentState EL 表达式,用于保存组件状态或者重定义状态

contentStyle 用于组件内容的样式

dir 文本方向

first 要显示的第一行,以 0 为起始值

footerClass 用空格分隔的表格题脚样式类

frame 设置边框的可见性,可以是:void|above|below|hsides|lhs|rhs|vsides|box|border

headerClass 用于窗口头部的样式类

id 每个组件都应该有一个唯一的 id,如果没有指定,将会自动产生

lang 产生该组件标记所使用的语言

onlick 点击时发生的事件

ondbclick 双击时发生的事件

onkeydown 键按下时发生的事件

onkeypress 键按下并弹起时发生的事件

onkeyup 键弹起时发生的事件

onmousedown 鼠标按下时发生的事件

onmousemove 鼠标移动时发生的事件

onmouseout 鼠标移出时发生的事件

onmouseover 鼠标在组件上时发生的事件

onmouseup 鼠标按键弹起时发生的事件

onRowClick 点击行时发生的事件

onRowDblClick 双击行时发生的事件

onRowMouseDown 在行内鼠标按下时发生的事件

onRowMouseMove 在行内鼠标移动时发生的事件

onRowMouseOut 在行内鼠标移出时发生的事件

onRowMouseOver 鼠标在行内时发生的事件

onRowMouseUp 在行内鼠标弹起时发生的事件

rendered 如果值为 false,则该组件将不渲染

rowClasses 用逗号分隔的样式类,用于行

rowKey 标识行

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

350

续表

属 性 名 称 描 述

rowKeyVar 可以在 request 范围内访问到的 rowKey 变量

rows 要显示列的数目,以 0 开始

rules

定义表格中单元格的规则:

none:没有规则,默认值

groups:规则仅用于行之间(参看 THEAD、TFOOT 和 TBODY)或者列之间(参

看 COLGROUP、COL)

rows:仅用于行

cols:仅用于列

all:运用于所有的行和列

stateVar 用于在客户端访问组件状态

style 当组件被渲染时的样式

styleClass 当组件被渲染时的样式类

summary 表格的概述

title 该组件产生的标记元素的提示文字

value 组件初始值

var 循环时当前行需要访问的值

width 定义组件的宽度

标签示例

DataTable.jsf:

<rich:dataTable id="auction" value="#{auctionDataModel}"

columnClasses="col1,col2,col3,col4" var="item" rows="5" width="100%">

<rich:column>

<f:facet name="header">

<h:outputText value="Description" />

</f:facet>

<h:outputText value="#{item.description}" />

</rich:column>

<rich:column>

<f:facet name="header">

<h:outputText value="Highest Bid" />

</f:facet>

<h:outputText id="highestBid" value="#{item.highestBid}">

CHAPTER 第 6 章 使用 RichFaces

351

6

<f:convertNumber pattern="$#,##0.00" />

</h:outputText>

</rich:column>

<rich:column>

<f:facet name="header">

<h:outputText value="Your Bid" />

</f:facet>

<rich:message for="bid" />

<br />

<h:inputText id="bid" value="#{item.bid}" label="Bid">

<f:convertNumber />

<f:validateLongRange minimum="0" maximum="1000000" />

</h:inputText>

<br/>

<a4j:commandLink id="bid_link" actionListener="#{item.placeBid}"

value="Place a bid!" reRender="bid,amount,highestBid" />

</rich:column>

<rich:column>

<f:facet name="header">

<h:outputText value="Amount" />

</f:facet>

<h:outputText id="amount" value="#{item.amount}">

<f:convertNumber pattern="$#,##0.00" />

</h:outputText>

</rich:column>

</rich:dataTable>

<rich:datascroller for="auction" maxPages="5" />

<rich:spacer height="30" />

后台 Bean 的代码和配置请参看本书源代码。

6.7 优化 Ajax 请求

6.7.1 优化 Ajax 队列

eventsQueue属性定义了即将到来的 Ajax请求队列名称。在默认情况下,RichFaces

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

352

并不排队 Ajax 请求。如果事件是同时产生的,当把它们发送到服务器上时也会同时

发生。JSF 实现并不保证先来的请求先被处理,所以服务器端的处理顺序是不可预料

的。使用 eventsQueue 属性可以避免这一现象。在使用 eventsQueue 属性的情况下,

同一个队列中之后的 Ajax 请求将等待前一个 Ajax 请求处理完成之后再进行处理。另

外,RichFaces 将移除队列中“类似”的请求。“类似”的请求是指由同一事件生成的

请求。请看下例,如果你键入的速度非常快的话,那么只有 新的请求会发送到服务

器端。

...

<h:inputText value="#{userBean.name}">

<a4j:support event="onkeyup" eventsQueue="foo" reRender="bar" />

</h:inputText>

...

requestDelay 属性定义了请求在队列中等待的时间。当时间到达之后,如果队列中没

有“类似”的请求,则请求会被发送到服务器端;如果存在“类似”的请求,则请求会

被移除。

ignoreDupResponses 属性用于忽略那些已经在队列中的请求所产生的响应。

ignoreDupResponses=true 时并没有取消服务器端的处理,仅仅是避免客户端不必要的

渲染。

requestDelay 和 eventsQueue 属性一同使用有助于防止过多的频繁请求给服务器造成的

压力,并可以保证客户端的请求是有序的。

timeout 属性用于设置等待响应的时间,如果在等待时间内没有接收到响应,那么请求

将会被取消。

更多信息请参考如下链接:http://jboss.com/index.html?module=bb&op=viewtopic&t=105766

6.7.2 优化数据

RichFaces 是使用 form 提交的方式提交 Ajax 请求的,这就意味着每一次 Ajax 请求都

会带有整个 form 的数据,这在某些情况下是不必要的。使用 ajaxSingle 属性就可以解决这

个问题,ajaxSingle=true 时,仅仅当前组件的参数会被提交。

bypassUpdates 属性允许忽略 JSF 生命周期的更新模型阶段,这对于仅仅验证校验器而

不需要更新模型的情况非常有用。

CHAPTER 第 6 章 使用 RichFaces

353

6

6.8 异常处理

6.8.1 Ajax 请求错误处理

在客户端处理 Ajax 请求的异常,需要重新定义以下函数:

A4J.AJAX.onError = function(req,status,message) {

// Custom Developer Code

};

各参数的含义如下:

req:产生异常的请求字串。

status:服务器端返回的错误代码。

message:错误的默认信息。

6.8.2 Session 过期处理

重新定义以下函数,用于在客户端处理 Session 过期事件。

A4J.AJAX.onExpired = function(loc,expiredMsg){

// Custom Developer Code

};

各参数的含义如下:

loc:当前页的 URL。

expiredMsg:Session 过期的默认信息字串。

6.9 局限和不足

使用 RichFaces 时,要时刻记住以下内容:

不要在自渲染的组件中使用<f:verbatim>元素,因为它是临时的元素,并且不会保

存在 JSF 树中。

Ajax 请求是 XMLHttpRequest 以 XML 形式发送出去的,XML 在浏览器中生成并

且略过了它们的验证阶段,所以使用兼容 HTML 和 XHTML 标签的代码是值得推

荐的。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

354

6.10 本章小结

本章介绍了 RichFaces 原理、安装配置,以及如何使用它的常用组件进行开发。伴随

着 Java 技术的发展,特别是 JEE 技术的发展,良好的用户体验和快速开发的模型越来越受

到众多开发人员的追捧。在即将发表的 JEE 6.0 规范中,首次将 Ajax 明确地列入到 JSF 2.0

规范中,所有这些,都是在为创建复杂的应用系统铺平道路。JSF 作为一种先进的 UI 技术,

终将获得广大开发人员的喜爱。

参考资料

[1] RichFaces 官方网站:http://labs.jboss.com/jbossrichfaces

[2] RichFaces 用户论坛:http://jboss.com/index.html?module=bb&op=viewforum&f=261

[3] RichFaces 参考文档

[4] 从 Google 搜索到的互联网资料

CHAPTER

7

7.1 Seam 简介

Seam 是由 JBoss 公司推出的、面向 Java 企业应用的框架,它集成了 Ajax、JSF、JPA、

EBJ 3.0 和 BPM 等框架。开发人员使用带注解的 POJO、一套 UI 组件和少量的 XML 就可

在以构建复杂的 Web 应用。Seam 为企业 Web 应用组件提供了一套统一的、易于理解的编

程模型,它使得开发基于状态的应用和业务流程驱动的应用易如反掌。换句话说,Seam 致

力于开发者的生产力和应用的扩展性。

值得一提的是,Seam 的领导者正是 Hibernate 的作者、大名鼎鼎的 Gavin King。怎么

样,你是不是已经急不可待地想看看 Seam 的魅力了?

笔者编写本书时 Seam 的 GA 版本是 2.0.1,而 2.1 处于 Alpha 阶段。Seam 2 拥有如

下特性:

(1)大一统

Seam 为应用中的业务逻辑定义了统一的组件模型,在 Seam 中表示层和业务逻辑层的

组件是没有任何区别的。

第 7 章

Seam 入门

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

356

(2)整合 JSF 和 EJB 3.0

EJB 3.0 是成熟的事务组件模型,也是 新 JPA(Java 持久层 API)关注的技术,但支

持 EJB 3.0 的 Java Web 应用框架却很少,这样,就需要自己去写整合代码。但在大多数情

况下,开发人员整合的方式并不能充分利用 EJB 3.0 的优势。

EJB 3.0 是 Seam 使用的核心技术,你可以无处不在地使用整合的 EJB 3.0 组件模型。

因为在 Seam 应用中,每个类都是一个 EJB 组件。你甚至不用写任何整合代码,即可“美

化”自己的框架。

当然,你也可以选择不使用 EJB 3.0,JBoss Seam 使你“鱼与熊掌可兼得”。

(3)充分挖掘 JSF 特性的 佳方式

JSF 已经有许多组件库和插件以供使用,也当之无愧地成为 Java 界 有吸引力的 Web

开发框架。但是 JSF 过度地依赖于 XML 配置,并且创建丰富网页界面的特性并不多。Seam

将使用注解的方式代替烦琐的 XML 配置,减少代码量的同时也提高了开发效率。Seam 在

很多方面扩展了 JSF 的功能,包括多窗口操作、工作空间管理、基于模型的验证、基于 jBPM

的页面流、国际化和页面分段缓存等。Seam 甚至还加入了控制标签化页面的机制。JSF 没

有涉及访问事务资源,Seam 考虑到了这一点,将 JTA、JPA 和 EJB 3.0 与 JSF 无缝地整合

起来。

(4)集成 Ajax 功能

在功能上,Ajax 改变了 Web 的交互模型。传统的 Web 客户端所使用的同步、粗粒度

的请求,引发了很多服务器端应用吃掉少量的缓存,并且没有带来缓存级别的并发操作。

在许多情况下,“无状态”的架构只是基本的解决方案,却远远不够!Ajax 客户端通过很

多异步、并行、细粒度的请求和服务器连接,使开发人员可以轻易地连接和获取远端“数

据库”信息。可是如果状态在交互时被保存到内存,并行相关的 bugs 将会非常危险,因为

Java EE 平台并不处理会话级别的并发。 初设计时,Seam 被构想成在 Ajax 基础上,整合

并行模型和状态管理模型。Seam 1.1整合了许多基于 JSF的开源Ajax解决方案,像RichFaces

和 ICEfaces,同时加入了 Seam 的状态和并行管理引擎。开发人员可以轻松地在程序中应用

Ajax,甚至不需要学习 JavaScript;并且将远离单独使用 Ajax 时潜在的 bugs 和性能问题。

(5)方便、快捷地建立业务流程管理(BPM)

jBPM 是用于解决存在复杂人员协作或者复杂用户交互的 Web 应用程序的解决方案。

BPM 技术不仅方便开发人员和分析人员建模、应用及优化业务过程,而且可以在整个操作

和管理过程中评估业务流程的效率和发掘业务流程中会出现的问题。Seam 深度整合了

jBPM,使用 jBPM 进行任务管理和页面流管理将变得异常简单。Seam 和 jBPM 的结合将使

一些项目的开发时间减少一个月左右。

(6)可声明式的状态管理

可声明式的状态管理是通过 Seam 丰富的内容模型实现的。Seam 扩展了 Servlet

规范对象(request、session 和 application),新定义了两个内容对象 conversation 和

business process。

CHAPTER 第 7 章 Seam 入门

357

7

(7)双射

Seam 通过使用一个流行的、被称做依赖注入(DI)的设计模式联结所有 POJO 组件。在

这个模式下,Seam 框架管理所有组件的生命周期。当一个组件需要使用另外一个组件时,

它通过注解(annotation)向 Seam 声明此依赖。Seam 依据当前应用状态得到这个依赖组件,

并将它注入到所需要的组件中。通过拓展依赖注入概念,一个 Seam 组件 A 不但可以构造

另外一个组件 B,而且把此组件 B“抛还”给 Seam 以备其他组件(例如组件 C)以后使用。

这类双向依赖管理甚至都广泛地应用于简单的 Seam Web 应用中。在 Seam 术语中,我们称

之为“依赖双向映射”,简称“双射”。

(8)工作空间管理和多窗口浏览

Seam 可以使用户在浏览器的多个标签中自由切换,使每一个标签都关联一个不同

的、安全隔离的对话。Seam 不仅提供正确的多窗口操作,而且还可以在单一的窗口内

实现多窗口操作。

(9)使用注解胜于 XML

Seam 是第一个从始至终、从持久层到表示层都使用 Java 5 注解功能的程序模型,你将

不会再因为烦琐的 XML 配置而迷失方向。同时在 Seam 中,开发人员也可以使用 XML,

Seam 为此提供了成熟的基于 XML 的组件配置机制。

(10)集成自动测试

任何项目都需要自动单元测试,但是仅仅依靠单元测试是十分危险的。许多 bugs 出现

在组件之间或者组件和容器的交互过程中,单元测试无法捕捉容器的行为,也无法捕捉组

件间的交互。Seam 引入了一种新方式“自动化集成测试”,你可以模拟每个请求或者会话

的全部业务流,来测试应用中从持久层到表示层的所有 Java 代码。

(11)修正了 JEE 的“漏洞”

JEE 规范虽然全面但不完美,例如,Seam 修正了 JSF 生命周期中 Get 请求的局限。

(12)比你考虑得更全面

一个真正完整的 Web 应用框架应该考虑像持久化、并发、异步、状态管理、安全性、

E-mail、消息、PDF 转换、图表生成、工作流、WiKi 文本的渲染、Web Services、缓存等

众多方面,而 Seam 正在帮助你处理这些相关问题。

由于本书的范围限定为 Web 2.0 的 UI 编程,所以本章讨论的是 Seam 与 JSF 的集成。

7.2 配置 Seam

7.2.1 下载

请从 http://seamframework.org 下载 Seam 的 新版本,笔者完成本书时的 新版本是

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

358

2.1.0.A1。下载的 Seam 是一个 zip 文件,文件名为 jboss-seam-2.1.0.A1.zip。请将它解压缩,

解压完成以后的目录结构如图 7-1 所示。

图 7-1 Seam 的目录结构

7.2.2 安装和配置

让 Seam 与 JSF 协同工作其实很简单,请遵循以下步骤进行。

第一步:配置 web.xml

首先在 web.xml 中注册 Servelet 监听器,该监听器在部署应用程序时初始化 Seam。

CHAPTER 第 7 章 Seam 入门

359

7

<listener>

<listener-class>org.jboss.seam.servlet.SeamListener</listener-class>

</listener>

第二步:配置 faces-config.xml

将 JSF Phase 监听器添加到 faces-config.xml 中,该监听器将 Seam 集成到标准 JSF 生

命周期中。

<lifecycle>

<phase-listener>org.jboss.seam.jsf.SeamPhaseListener</phase-listener>

</lifecycle>

第三步:添加 seam.properties

将一个空的 seam.properties 文件放在类路径的根下,以便指示 Seam 进行加载。这个

空白文件被用做一个 JVM 类加载器优化,使 Seam 在类路径下更小的区域内搜索组件,

从而大大减少加载时间。

seam.properties

# The mere presence of this file triggers Seam to load

# It can also be used to tune parameters on configurable Seam components

第四步:添加 components.xml

components.xml 文件用于设置组件的属性,请参考如下的配置:

<?xml version="1.0" encoding="UTF-8"?>

<components xmlns="http://jboss.com/products/seam/components"

xmlns:core="http://jboss.com/products/seam/core"

xmlns:security="http://jboss.com/products/seam/security"

xmlns:transaction="http://jboss.com/products/seam/transaction"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation=

"http://jboss.com/products/seam/core

http://jboss.com/products/seam/core-2.1.xsd

http://jboss.com/products/seam/transaction

http://jboss.com/products/seam/transaction-2.1.xsd

http://jboss.com/products/seam/security

http://jboss.com/products/seam/security-2.1.xsd

http://jboss.com/products/seam/components

http://jboss.com/products/seam/components-2.1.xsd">

<core:init transaction-management-enabled="false"/>

<transaction:no-transaction/>

</components>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

360

第五步:拷贝 Java 包

后一步是将 Seam 所需的 jar 包拷贝到 WEB-INF/lib 下。

经过以上步骤以后,项目的目录结构如图 7-2 所示。

图 7-2 项目的目录结构

7.2.3 HelloWorld

为了突出 Seam 给 JSF 带来的便利,我们仍使用第 6 章中的 HelloWorld 作为本章的

CHAPTER 第 7 章 Seam 入门

361

7

HelloWorld 用例(如图 7-3 所示),请读者自己对比。

图 7-3 HelloWorld 应用界面

在 Eclipse 中创建动态 Web 项目,如图 7-4、图 7-5 和图 7-6 所示。

图 7-4 在 Eclipse 中创建 Web 项目

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

362

图 7-5 项目设置

图 7-6 Web 模块设置

CHAPTER 第 7 章 Seam 入门

363

7

首先按照第 6 章中所述配置 RichFaces,然后按照 7.2.2 节中所述配置 Seam 监听器。

完整的 web.xml 文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns="http://java.sun.com/xml/ns/javaee"

xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"

xsi:schemaLocation="http://java.sun.com/xml/ns/javaee

http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">

<context-param>

<param-name>javax.faces.CONFIG_FILES</param-name>

<param-value>/WEB-INF/faces-config.xml</param-value>

</context-param>

<context-param>

<param-name>javax.faces.DEFAULT_SUFFIX</param-name>

<param-value>.jsf</param-value>

</context-param>

<context-param>

<param-name>org.richfaces.SKIN</param-name>

<param-value>blueSky</param-value>

</context-param>

<context-param>

<param-name>org.ajax4jsf.VIEW_HANDLERS</param-name>

<param-value>com.sun.facelets.FaceletViewHandler</param-value>

</context-param>

<filter>

<display-name>RichFaces Filter</display-name>

<filter-name>richfaces</filter-name>

<filter-class>org.ajax4jsf.Filter</filter-class>

</filter>

<filter-mapping>

<filter-name>richfaces</filter-name>

<servlet-name>Faces Servlet</servlet-name>

<dispatcher>REQUEST</dispatcher>

<dispatcher>FORWARD</dispatcher>

<dispatcher>INCLUDE</dispatcher>

</filter-mapping>

<listener>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

364

<listener-class>

org.jboss.seam.servlet.SeamListener

</listener-class>

</listener>

<servlet>

<servlet-name>Faces Servlet</servlet-name>

<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>

<load-on-startup>1</load-on-startup>

</servlet>

<servlet-mapping>

<servlet-name>Faces Servlet</servlet-name>

<url-pattern>*.faces</url-pattern>

</servlet-mapping>

</web-app>

在 web.xml 文件的同名目录下创建 faces-config.xml,并配置 Seam Phase 监听器。

<?xml version="1.0" encoding="UTF-8"?>

<faces-config xmlns="http://java.sun.com/xml/ns/javaee"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://java.sun.com/xml/ns/javaee

http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd" version="1.2">

<!-- Facelets support -->

<application>

<view-handler>com.sun.facelets.FaceletViewHandler</view-handler>

</application>

<lifecycle>

<!-- JBoss Seam Phase listener -->

<phase-listener>

org.jboss.seam.jsf.SeamPhaseListener

</phase-listener>

</lifecycle>

</faces-config>

在 web.xml 的同名目录下创建 components.xml。

<?xml version="1.0" encoding="UTF-8"?>

<components xmlns="http://jboss.com/products/seam/components"

xmlns:core="http://jboss.com/products/seam/core"

CHAPTER 第 7 章 Seam 入门

365

7

xmlns:security="http://jboss.com/products/seam/security"

xmlns:transaction="http://jboss.com/products/seam/transaction"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation=

"http://jboss.com/products/seam/core

http://jboss.com/products/seam/core-2.1.xsd

http://jboss.com/products/seam/transaction

http://jboss.com/products/seam/transaction-2.1.xsd

http://jboss.com/products/seam/security

http://jboss.com/products/seam/security-2.1.xsd

http://jboss.com/products/seam/components

http://jboss.com/products/seam/components-2.1.xsd">

<core:init transaction-management-enabled="false"/>

<transaction:no-transaction/>

</components>

在 src 目录下创建一个空的 seam.properties 文件。

在 src 目录下创建 Bean。

MyBean.java:

package demo;

import java.io.Serializable;

import org.jboss.seam.ScopeType;

import org.jboss.seam.annotations.Name;

import org.jboss.seam.annotations.Scope;

@Name("bean")

@Scope(ScopeType.PAGE)

public class MyBean implements Serializable {

private String text;

public String getText() {

return text;

}

public void setText(String text) {

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

366

this.text = text;

}

}

这一步创建了一个普通的 Java Bean,唯一需要说明的是其中的两个注解标签@Name 和

@Scope。@Name 为一个类定义一个 Seam 组件,所有 Seam 组件都需要该注解;而@Scope

定义默认的组件上下文,可以定义的值由 ScopeType 枚举,其值为 EVENT、PAGE、

CONVERSATION、SESSION、BUSINESS_PROCESS、APPLICATION、STATELESS。当范围

没有显式定义时,默认的范围取决于组件类型。对于无状态会话 Bean,默认是 STATELESS;

对于 Entity Bean 和 Stateful Session Bean,默认是 CONVERSATION;对于 Java Bean,默

认是 EVENT。

在 WebContents 下创建 HelloWorld 页面。

HelloWorld.jsf:

<!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:a4j="http://richfaces.org/a4j"

xmlns:rich="http://richfaces.org/rich"

xmlns:c="http://java.sun.com/jstl/core">

<head>

<title>Hello World</title>

</head>

<body>

<f:view>

<h:form>

<h:panelGrid>

<h:outputText value="请输入:" />

<h:inputText size="30" value="#{bean.text}">

<a4j:support event="onkeyup" reRender="rep" />

</h:inputText>

<h:outputText value="Ajax响应:" />

<h:outputText value="#{bean.text}" id="rep" />

</h:panelGrid>

<a4j:log level="ALL" popup="true" width="400" hotkey="M" />

CHAPTER 第 7 章 Seam 入门

367

7

</h:form>

</f:view>

</body>

</html>

至此,完成了此示例。需要注意的是,我们并没有在 faces-config.xml中定义Backing Bean。

HelloWorld 的应用程序堆栈如图 7-7 所示。

图 7-7 HelloWorld 的应用程序堆栈

7.3 Seam 与 JSF

很多 Java 程序员都有这样的感受:迁移到 JSF 并不是一帆风顺的。采用组件模型会带

来一些全新的问题,首要的一个问题是你通常需要试着使应用程序符合基于动作的 Web。

很多时候,JSF 需要具有像基于动作的框架那样的行为,但是在标准 JSF 中这是不可行的,

至少不为每个请求使用 Phase 监听器就不行。

JSF 的其他主要缺点还包括对 HTTP 会话的依赖过重(尤其是在一系列的页面之间传

输数据时)、简陋的异常处理、缺少标签支持,以及太多的 XML 配置。通过与 JSF 自然

地集成,同时加入 JSF 规范委员会放弃的或者忽略掉的新功能,Seam 解决了很多这样的

问题。Seam 的框架鼓励使用紧凑的、易读的、可重用的代码,并且避免了所有为解决上

述问题而常常加入的“粘连(glue)”逻辑。

如图 7-8 所示是 Seam 对 JSF 生命周期的扩展。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

368

图 7-8 Seam 对 JSF 生命周期的扩展

让我们来了解一下其中的一些增强功能,因为它们适用于 JSF 开发中一些常见的挑

战。

(1)并不复杂的配置

Seam 演示了 Java 5 注释的一个非常实用的用法。Seam 的部署扫描程序检查所有包

含 seam.properties 文件的归档文件,并为所有标有 @Name 注释的类创建一个 Seam 组

件。由于 Java 语言缺乏用于在代码级添加元数据的一种公共语法,因此需要设计很多

XML 配置。当 Java 5 规范中加入注释后,就获得了一个更好的解决方案。由于大多数

Backing Bean 是为了在特定应用程序中使用而开发的,因此没有理由将这些 Bean 的配置

“抽象”到类本身以外的任何文件中。附带的好处是,你可以少处理一个文件。Seam 提

CHAPTER 第 7 章 Seam 入门

369

7

供了一组完整的注释来帮助将 Bean 集成到 JSF 生命周期中。

(2)页面动作和 RESTful URL

在不使用组件框架的情况下,另一个必须解决的问题是预先处理每个请求,就像在基

于动作的框架中那样。受此影响的用例是 RESTful URL、书签支持、通过 URL 模式获得

的安全性及页面流验证等。这也是学习使用 JSF 的开发人员容易感到困惑的主要原因之

一。有些 JSF 实现通过使用开发工具提供的 onPageLoad 功能来绕过这个问题,但这不是

核心规范的一部分。

比如,当用户直接从收藏夹里请求一个商品详细信息页面时,通常会发生什么事情呢?

由于 JSF 控制器采取被动方式,当页面开始呈现时,即使明显没有目标数据,也不能将用

户重新带到逻辑流的开始处。相反,这种情况下只能显示一个空页面,其中只有一些空值

或其他可能存在的假信号。

首先,你可能会本能地想要在页面的主 Backing Bean 上实现一个“prerender”方法。

然而,在组件框架中,Backing Bean 与页面之间的关系并不一定都是一对一的,每个页面

可能依赖于多个 Backing Bean,每个这样的 Bean 也可能在多个不同的页面上使用。必须

用某种方式将一个视图 ID(例如 /user/detail.jspx)与一个或多个方法关联起来,当选择呈

现相应的视图模板时就调用这个(些)方法。你可以使用 phase-listener 方法,但是这仍然

需要定制的逻辑来确定对于当前视图和阶段是否应该执行该功能。这种解决方案不但会导

致很多冗余逻辑,而且会将视图 ID(很可能是应用程序中 不确定的部分)硬编码到编译

后的 Java 代码中。

(3)页面动作来帮忙

Seam 的页面动作可以帮助你预先拦截呈现的假信号。页面动作是使用方法绑定指定

的,方法绑定在进入页面时、Render Response 阶段之前执行。对于 /WEB-INF/pages.xml

配置文件中一个给定的视图 ID,可以配置任意数量的方法绑定,(或者,可以通过将它

们放在视图模板邻近的一个文件中,复制它的名称,但是将文件扩展名更改为

*.page.xml,从而分解每个页面的定义)。对于页面动作,XML 是有必要的,因为视图 ID

非常容易变化。就像 JSF 通过 Apply Request Values 阶段的值绑定将 POST 数据映射到

模型对象一样,Seam 可以通过执行页面动作之前的值绑定将任意请求参数映射到模型对

象。这些请求参数注入的配置嵌套在页面动作 XML 声明中。如果页面动作方法调用返

回一个非空字符串值,则 Seam 将其当做一个导航事件。因此,即使不迁移到一个完全

基于动作的框架中,仍然可以使用 Seam 的这些特性。Seam 包括很多内置的页面动作,

它们通常跨应用程序使用,其中包括用于验证 conversation 是否建立的一个动作;可以

启动、嵌套和结束 conversation 的动作;处理预期异常的动作,以及确保适当的凭证的

动作。

页面动作是启用对 JSF 的书签支持的关键。Seam 的创立者允许在进入页面时请求参

数 actionMethod 触发一个方法调用,从而利用了这一特性。更妙的是,你不需要做任何额

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

370

外的工作就能为书签创建链接。Seam 提供了两个组件标记:s:link 和 s:button,用来处理

细节。这两个标记分别对应于 JSF 中的 h:commandLink 和 h:commandButton。不同之处

在于,Seam 组件标记组装的链接使用一个 HTTP GET 操作发出请求,而不是使用 JSF 的

HTTP POST 表单提交模型表示。因此,Seam 创建的链接对书签更 “友好”,对于开发人

员来说更方便。

你可能还注意到,当使用页面动作时,地址栏中的 URL 对应于正在显示的页面,

而不总是后台对应处理的页面(后一种情况之所以会发生,是因为 JSF 将表单处理页

面设置为生成它们的 URL。地址栏没有更新,用以反映执行动作后的新视图,因为 JSF

通过一个服务器端重定向使之前进)。如果你想演示页面动作的灵活性,那么可以使用

它们来创建 RESTful URL(例如 /faces/product/show/10)。为此,将页面动作方法映射

到视图 ID“/product/show/*”,其中 /faces 前缀是 JSF Servlet 映射部分。然后,该页

面动作方法利用请求 URL,以判断数据类型和数据标识符,加载数据,并导航到适当

的模板。这个例子很明显地演示了 JSF 请求 URL 与视图模板之间并不一定存在一对

一的关系。

(4)工厂组件

JSF 大的一个失败是没有在用户触发的动作或动作监听器方法以外的其他地方提供

可靠的机会来为视图准备数据。将逻辑放在一个动作方法中并不能保证该逻辑在视图呈现

之前得到执行,因为页面视图并不总是在用户触发的事件之后。

例如,当一个 JSF 应用程序的 URL 第一次被请求时,会发生什么情况?如果需

要在该页面上显示从服务层获得的一组数据,那么在 JSF 生命周期中始终没有好的机

会来取数据。你可能会认为,可以将逻辑放在映射到视图中值绑定表达式的 Backing

Bean 的 getter 方法中。但是,每当 JSF 解析那个表达式时,就会触发另一个调用,

新的调用又会访问服务层。即使页面上只有少数几个组件,getter 方法也可能被推后数

次执行。显然,就性能而言这不是 优的。即使通过使用受管 Bean 上的私有属性维

护状态,每当面对那样的情况时,仍然必须增加额外的管道。一种解决方案是使用

Seam 的页面动作。但是由于这种任务是如此常见,Seam 提供了一个更加容易的解决

方案。

Seam 引入了工厂数据提供者(Factory Data Provider)的概念,工厂数据提供者由

@Factory Java 5 注释指定。虽然有两种方法配置工厂,但是如果 终结果是同样的数据,

则只需在第一次被请求时准备一次即可。 Seam 确保随后对相同数据的请求将直接返回之

前创建的结果集,而不必再一次触发对查找方法的调用。通过与 conversation 相结合,工

厂数据提供者成为实现数据短期缓存非常强大的特性;否则,取这些数据的代价可能较高。

在 JSF 不负责减少它解析一个值绑定表达式的次数的情况下,Seam 的工厂特性常常变得

非常方便。

CHAPTER 第 7 章 Seam 入门

371

7

(5)有状态 conversation

关于 JSF 很容易引起困惑的一个地方是它的状态管理功能。JSF 规范解释了在接收一

个动作之后页面是如何“恢复(restored)”的,在此期间时间事件要进行排队,选择要注

册。仔细研究规范中的用词可以发现,虽然在 postback 上恢复了组件树,但是那些组件使

用的 Backing Bean 数据并没有被恢复。组件只是以字符串文字的形式存储值绑定(使用

#{value} 语法的 EL 表达式),只在运行时解析底层数据。这意味着如果一个值是短期作

用域存储的,例如页面或请求作用域,那么当 JSF 生命周期到达 Restore View 阶段时,

这个值将消失。

不将值绑定数据存储在组件树中的一个 值得注意的不利方面是虚幻事件效果,这

是由 UIData 家族中的临时父组件导致的。如果一个值绑定表达式引用的模型数据不再

可用,或者在组件树被恢复之前发生了更改,那么组件树的一些部分将被撤销。如果在

这些被撤销的分支中,有一个组件触发了一个事件,那么它将不能被发现,而且这种事

件丢失情况是难于觉察的(只是队列开发人员可能会惊呼:为什么我的动作没有被调

用?)。

虽然丢失的事件看上去像是异常状况,但并不会导致 JSF 生命周期中出现红色标

志。因为这些组件依赖底层数据,以保持稳定和适当地被恢复,所以 JSF 难于知道丢失

了什么。

遗憾的是,JSF 规范天真地引导开发人员将大多数 Backing Bean 放入 conversation

作用域中 —— 甚至可以在“方便的”作用域内调用它。然后,服务器管理员则必须处理

由此导致的 “内存溢出” 错误,集群环境中的服务器相似性,以及服务器重启时的串行

化异常。MyFaces Tomahawk 项目通过 t:saveState 标记的形式提供了对虚幻事件的一个解

决方案。MyFaces 标记允许将数据(包括整个 Backing Bean)存储为组件树的一部分,而

仅仅是值绑定。然而,这种解决方案有些简陋,很像使用隐藏的表单字段在请求之间传递

值;它还造成视图与控制器之间紧密耦合。Seam 的创立者意识到,Java Servlet 规范中三

个内置的上下文(请求、会话和应用程序)不足以构成数据驱动的 Web 应用程序的全部

作用域。在 Seam 中,他们引入了 conversation 作用域,这是一个有状态作用域,由页面

流的起止点界定。

(6)Seam 的 conversation 作用域

“Seam 强调使用有状态组件。” Seam 参考文档中的这句话体现了 Seam 的核心思

想。很长一段时间内,关于 Web 应用程序的看法是,它们是无状态的 —— 这种思想在

一定程度上要归因于 HTTP 协议的无状态性质。大多数框架为了迎合这一点,在结束页面

呈现之前提供 one-shot-processing。这种方法导致很大的阻力,因为任何大的应用程序都需

要长时间运行的 conversation 来满足某些用例。需要有状态上下文的应用程序的例子有很

多,例如存储检查过程、产品定制、多页表单向导和很多其他基于线性交互的应用程序。

虽然其中有些例子可以通过使用 URL 参数(aka RESTful URL)和隐藏字段在页面之间迁

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

372

移数据,但是这样做对于开发人员来说有些繁杂。而且,如今这种做法已经过时了,因为

大多数 Web 框架仍然在无状态模型下操作,所以你常常发现自己走出了这种框架,而 “开

辟” 出定制解决方案。

JSF 大量依赖于 HTTP 会话,试图引入有状态上下文。实际上,当和会话作用域的

Backing Bean 一起使用时,JSF 组件的行为要好得多。如果不小心设计,过度使用 HTTP 会

话则会导致严重的内存泄漏、性能瓶颈和安全问题。此外,在多标签浏览器环境中,使用

HTTP 会话可能导致非常奇怪的行为,破坏用户神圣的 Back 按钮。值得注意的是,JSF 只

是与你互作让步:它是一个有状态 UI,处理保存和恢复组件树的所有细节,但是它在保存

和恢复数据方面没有提供帮助。因此,JSF 带来有状态 UI,而你则带来有状态数据。遗憾

的是,需要由你来负责确保它们是相符的。

在 Seam 之前,使用有状态数据的唯一方便的方式是依赖于 HTTP 会话。Seam 纠

正了这个问题,它通过建立一个全新的 conversation 作用域,完善了 JSF 的状态管理。

随着将 Seam 添加到 JSF 生命周期中,conversation 上下文与一个浏览器窗口(或标

签页)联系在一起,这个浏览器窗口(或标签页)由随每个请求提交的一个标志来标识。

conversation 作用域使用 HTTP 会话的一个单独的区段在页面之间迁移数据。记住:

Seam 使用 HTTP 会话用于 conversation 持久性这一点是完全透明的。Seam 并不是不

负责任地将组件推卸到 HTTP 会话中,使其茫然地待在那里。相反,Seam 小心地管理

那个区段的会话数据的生命周期,并且当 conversation 终止时自动清除它们。Seam 聪

明地使用双射来允许以一种新的说明性方式使数据流入和流出一个“Web conversation”

的每个页面。Seam 的 conversation 作用域同时还克服了 HTTP 会话的限制,帮助开发

人员放弃使用 HTTP 会话。

(7)异常处理

Seam 的创立者曾说过:“在异常处理方面,JSF 非常有限”。这一点显然毫无争议。JSF

规范完全忽视异常管理,将责任完全放在 Servlet 容器上。允许 Servlet 容器处理异常的

问题在于,这严重限制了错误页面上显示的内容,并且禁止了事务回滚。由于错误页面是

在请求分发器转发之后显示的,FacesContext 不再在作用域中,因此这时执行业务逻辑就

太迟了。你的 后一线希望是使用 Servlet API,并抓住 javax.servlet.error.* 请求属性,以

搜索能表明出错处的信息。

在这一点上,Seam 再次扮演救世主,它提供了优雅的、说明性方式的异常处理。异

常管理可以通过注释指定,或者在配置文件中指定。可以将注释 @HttpError、@Redirect 和

@ApplicationException 放在异常类的上面,表明当异常被抛出时应该采取的动作。对于那

些不能修改的异常类,可以使用 XML 配置选项。Seam 中的异常管理器负责发送 HTTP

状态码、执行重定向、促使页面呈现、终止 conversation 和定制出现异常时的用户消息。

由于在开始呈现响应之后,JSF 不能改变动作的过程,一些固有的限制规定了何时才能处

理这些异常。通过适当使用其他 Seam 特性,例如页面动作,可以确保大多数异常情况在

CHAPTER 第 7 章 Seam 入门

373

7

呈现响应之前得到解决。

(8)Ajax remoting

由于 后发行的 JSF 规范几乎与 Ajax 重合,JSF 框架在异步 JavaScript 和局部页面

呈现(Partial Page Rendering)方面帮助不大。在某些时候,这两种类型的编程甚至不一致。

终的解决办法是,建议使用 JSF Phase Listener 或组件 Renderer 来处理局部页面更新。

即使如此,这一点仍然很明显:JSF 使得 Ajax 比过去更难于采用。有些项目,例如

ICEfaces,甚至用一个更好的、专为页面-服务器通信设计的技术(即 direct-to-DOM

rendering)来替代 JSF 生命周期。

Seam 为 JavaScript remoting(常常记在 Ajax 名下的一种技术)提供了一种独特的方

式,该方式与 Direct Web Remoting (DWR) 库的方式大致相似。Seam 通过允许 JavaScript

直接调用服务器组件上的方法,将客户机与服务器连在一起。Seam remoting 比 DWR 更

强大,因为它可以访问丰富的上下文组件模型,而不仅仅是一个孤立的端点。这种交互构

建在 JSF 的事件驱动设计的基础上,所以它可以更严格地遵从 Swing 范例。 妙的是,

提供这个功能的同时并没有增加开发方面的负担,只需在组件的方法上添加一个简单的注

释 @WebRemote,JavaScript 就可以访问该方法。当服务器组件的状态被修改之后,Ajax4jsf

组件库就可以处理局部呈现。简而言之:Seam remoting 库使 JSF 可以实现它的创立者一

向期望的交互设计。

7.4 再谈数据表格(dataTable)

数据表格的引入是 JSF 的一个令人兴奋的特性。通过使用该组件开发者可以方便地将

数据模型绑定到数据表格上,同时也可以在 Backing Bean 中方便地获取特定行的数据绑定

对象。为了说明 Seam 的引入的确为 JSF 的开发带来了便利,我们来看一个实际的例子对

比一下。

实际项目中单纯使用数据表格(<h:dataTable>)的少之又少,原因只有一个,就是太复杂、

难使用。在项目中,数据一般都比较大,使用内存分页的机制一般都无法满足需求,我们

就以分页为例来说明 Seam 的优势。请阅读下面的章节时思考一个问题:为什么不采用第 4

章中 4.6.3 节的 On-Demand 分页方式?

7.4.1 单纯 RichFaces 的解决方案

这是一个图书列表的例子,完成时的界面如图 7-9 所示。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

374

图 7-9 单纯 RichFaces 的解决方案示例

我们使用了<rich:dataTable>和<rich:datascroller>两个组件实现了以上功能,完整的 JSF

源码如下。

DataTable.jsf:

<!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:a4j="http://richfaces.org/a4j"

xmlns:rich="http://richfaces.org/rich"

xmlns:c="http://java.sun.com/jstl/core">

<ui:composition template="/templates/main.jsf">

<ui:define name="title">Data Table 深入研究</ui:define>

<ui:define name="body">

<h:form id="form1">

<rich:panel id="dataPanel">

<rich:dataTable id="dataTable" value="#{bookAction.dataList}"

binding="#{bookAction.dataTable}" var="item" rows="5"

align="center">

<f:facet name="header">

<rich:columnGroup>

<h:column>

<h:outputText value="书名"/>

</h:column>

<h:column>

<h:outputText value="作者"/>

CHAPTER 第 7 章 Seam 入门

375

7

</h:column>

<h:column>

<h:outputText value="ISBN"/>

</h:column>

<h:column>

<h:outputText value="发布日期"/>

</h:column>

</rich:columnGroup>

</f:facet>

<rich:column>

<h:outputText value="#{item.name}"/>

</rich:column>

<rich:column>

<h:outputText value="#{item.author}"/>

</rich:column>

<rich:column>

<h:outputText value="#{item.isbn}"/>

</rich:column>

<rich:column>

<h:outputText value="#{item.publishDate}"/>

</rich:column>

</rich:dataTable>

<rich:datascroller align="center" for="dataTable"

reRender="dataPanel"/>

</rich:panel>

</h:form>

</ui:define>

</ui:composition>

</html>

请读者注意加粗部分的代码,其中所关联的 BookAction 类如下所示。

BookAction.javaF:

public class BookAction extends PaginationPageAction {

//双击某行时获取该行绑定的数据.

public String rowDblClick(){

Book book = (Book)getDataTable().getRowData();

System.out.println("book name:" + book.getName());

return null;

}

}

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

376

BookAction 是由 PaginationPageAction 继承而来的,它本身并不负责分页的处理,仅关

心自己的业务逻辑。

PaginationPageAction.java:

import javax.faces.component.UIData;

public abstract class PaginationPageAction extends AbstractPageAction

implements IPaginationHandle{

//用于分页的数据模型

private PaginationDataModel pagedDataModel;

//用于页面的数据绑定

private UIData dataTable;

//每页的记录数

private int pageSize = Constants.PAGE_SIZE;

public javax.faces.model.DataModel getDataList() {

if (pagedDataModel == null)

{

System.out.println("<=====初始化数据模型======>");

pagedDataModel = new PaginationDataModel();

}

return pagedDataModel;

}

//其他 get/set方法

}

PaginationPageAction 的主要功能是提供了数据模型的初始化,而数据模型

(PaginationDataModel)则负责为页面提供数据。

PaginationDataModel.java:

//省略 import

public class PaginationDataModel extends SerializableDataModel {

//模拟的数据源

private MockDataProvider dataProvider;

//当前行的 PK

private Integer currentPk;

//当前包装的数据

CHAPTER 第 7 章 Seam 入门

377

7

private Map<Integer,Book> wrappedData = new HashMap<Integer,Book>();

//包装数据的 key值

private List<Integer> wrappedKeys = null;

//分离标识

private boolean detached = false;

@Override

public Object getRowKey() {

return currentPk;

}

/**

* 请求某行数据时,该方法被 visitor调用。关于 visitor请参考 walk方法

*/

@Override

public void setRowKey(Object key) {

this.currentPk = (Integer) key;

}

/**

* visitor模式的主体部分,在处理请求的过程中被框架多次调用

*/

@Override

public void walk(FacesContext context, DataVisitor visitor,

Range range, Object argument) throws IOException {

int firstRow = ((SequenceRange)range).getFirstRow();

int numberOfRows = ((SequenceRange)range).getRows();

if (detached) {

for (Integer key:wrappedKeys) {

setRowKey(key);

visitor.process(context, key, argument);

}

} else { // if not serialized, than we request data from data provider

wrappedKeys = new ArrayList<Integer>();

for (Book item:dataProvider.findBooks(new Integer(firstRow),

numberOfRows)) {

wrappedKeys.add(item.getId());

wrappedData.put(item.getId(), item);

visitor.process(context, item.getId(), argument);

}

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

378

}

}

private Integer rowCount; // better to buffer row count locally

@Override

/**

* 这个方法返回从数据库中检索出的实际记录数

*/

public int getRowCount() {

if (rowCount==null) {

rowCount = new Integer(dataProvider.getRowCount());

return rowCount.intValue();

} else {

return rowCount.intValue();

}

}

/**

* 获取行绑定的对象

*/

@Override

public Object getRowData() {

if (currentPk==null) {

return null;

} else {

Book ret = wrappedData.get(currentPk);

if (ret==null) {

ret = dataProvider.getItemById(currentPk);

wrappedData.put(currentPk, ret);

return ret;

} else {

return ret;

}

}

}

public SerializableDataModel getSerializableModel(Range range) {

if (wrappedKeys!=null) {

detached = true;

return this;

} else {

return null;

CHAPTER 第 7 章 Seam 入门

379

7

}

}

//省略了其他的非重要的方法

}

PaginationDataModel 根据页面的实际请求通过数据提供者(dataProvider)从数据库中获

取当前页数据供用户浏览。

MockDataProvider.java:

import java.util.ArrayList;

import java.util.Date;

import java.util.List;

public class MockDataProvider{

private static int counter = 0;

/**

* 初始化模拟数据

*/

private static List<Book> initialData() {

List<Book> data = new ArrayList<Book>();

for (int i = 0; i < 30; i++) {

Book book = new Book();

book.setId(i);

book.setName("Book[" + i + "]");

book.setAuthor("author[" + i + "]");

book.setIsbn("ISBN[" + i + "]");

book.setPublishDate(new Date());

data.add(book);

}

return data;

}

/**

* 模拟数据库的分页查找

* @param startRow 起始记录 index

* @param pageSize 每页记录数目

* @return

*/

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

380

public static List<Book> findBooks(int startRow, int pageSize) {

System.out.println("查询数据库开始(counter=" + counter++ +")");

List<Book> data = initialData();

List<Book> result = new ArrayList<Book>();

for (int i = startRow; i < startRow + pageSize; i++) {

result.add(data.get(i));

}

System.out.println("结束查询...");

return result;

}

/**

* 本次查询的总记录数目

* @return

*/

public static Integer getRowCount(){

return 30;

}

/**

* 根据 PK加载 PO

* @param id

* @return

*/

public static Book getItemById(Integer id){

List<Book> data = initialData();

return data.get(id);

}

/**

* 判定当前表内是否有该记录

* @param id

* @return

*/

public static Boolean hasItemById(Integer id){

return id>=0 && id <30;

}

/**

* 用于更新模型

*/

CHAPTER 第 7 章 Seam 入门

381

7

public static void update() {

}

}

为了简单,在示例中我们并没有使用 O/R Mapping,而只是使用了一个模拟的数据提

供者 MockDataProvider。在实际的项目中,请读者自行替换。

后是 Book.java:

import java.util.Date;

public class Book implements Serializable{

private Integer id;

private String name;

private String author;

private String isbn;

private Date publishDate;

//get/set方法

}

faces-config.xml:

<?xml version="1.0" encoding="UTF-8"?>

<faces-config xmlns="http://java.sun.com/xml/ns/javaee"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://java.sun.com/xml/ns/javaee

http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd"

version="1.2">

<managed-bean>

<managed-bean-name>bookAction</managed-bean-name>

<managed-bean-class>demo.BookAction</managed-bean-class>

<managed-bean-scope>request</managed-bean-scope>

</managed-bean>

</faces-config>

7.4.2 RichFaces + Seam 的解决方案

从上节内容中我们看出,要做一个分页的实现是非常困难的(当然采用内存分页机制

就另当别论了)。下面再看看 Seam 是如何让我们摆脱复杂的编程的。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

382

首先看一下 DataTable.jsf,它基本上没有任何变化,唯一的不同就是 dataTable 的

binding 属性被去掉了。这里需要一提的是 JSF 组件的绑定技术,除非绝对的必要,否

则 Seam 不推荐使用这一技术,因为它创建了从应用程序逻辑到视图的强依赖关系。在

一个 postback 请求中,组件绑定会在视图恢复阶段中且在 Seam 对话上下文恢复之前被

更新。

<!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:a4j="http://richfaces.org/a4j"

xmlns:rich="http://richfaces.org/rich"

xmlns:c="http://java.sun.com/jstl/core">

<ui:composition template="/templates/main.jsf">

<ui:define name="title">Data Table 深入研究</ui:define>

<ui:define name="body">

<h:form id="form1">

<rich:panel id="dataPanel">

<rich:dataTable id="dataTable" value="#{bookAction.dataModel}"

var="item" rows="5" align="center">

<f:facet name="header">

<rich:columnGroup>

<h:column>

<h:outputText value="书名"/>

</h:column>

<h:column>

<h:outputText value="作者"/>

</h:column>

<h:column>

<h:outputText value="ISBN"/>

</h:column>

<h:column>

<h:outputText value="发布日期"/>

</h:column>

</rich:columnGroup>

</f:facet>

CHAPTER 第 7 章 Seam 入门

383

7

<rich:column>

<h:outputText value="#{item.name}"/>

</rich:column>

<rich:column>

<h:outputText value="#{item.author}"/>

</rich:column>

<rich:column>

<h:outputText value="#{item.isbn}"/>

</rich:column>

<rich:column>

<h:outputText value="#{item.publishDate}"/>

</rich:column>

</rich:dataTable>

</rich:panel>

<rich:datascroller align="center" for="dataTable"

reRender="dataPanel"/>

</h:form>

</ui:define>

</ui:composition>

</html>

我们再来看看在 BookAction 中有什么变化。

BookAction.java:

import java.io.Serializable;

import org.jboss.seam.ScopeType;

import org.jboss.seam.annotations.In;

import org.jboss.seam.annotations.Name;

import org.jboss.seam.annotations.Scope;

import org.jboss.seam.annotations.datamodel.DataModel;

@Name("bookAction")

@Scope(ScopeType.PAGE)

public class BookAction implements Serializable{

private static final long serialVersionUID = -2188532450226972368L;

@DataModel("dataModel")

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

384

@In("#{dataModel}")

private PaginationDataModel dataModel;

public PaginationDataModel getDataModel() {

return dataModel;

}

public void setDataModel(PaginationDataModel dataModel) {

this.dataModel = dataModel;

}

}

请读者注意,并不像 7.4.1 节中的 BookAction,这个类并没有继承 PaginationPageAction。

那么是怎样实现分页的呢?奥妙全在@DataModel 注解和 PaginationDataModel 模型上。

@DataModel 用于将一个类型为 List、Map、Set 或 Object[] 的属性作为一个 JSF

DataModel 置入所属组件的 scope 中(如果所属组件是 STATELESS,则为 EVENT

scope )。 当它是 Map 时,DataModel 的每一行就是一个 Map.Entry。但这里并不是 List、

Map,也不是 Set,更不是 Object[],那么 Seam 是如何知道转换的呢?答案是

SeamDataModels 这个类。

SeamDataModels.java:

import java.util.List;

import java.util.Map;

import java.util.Set;

import javax.faces.model.DataModel;

import org.jboss.seam.ScopeType;

import org.jboss.seam.annotations.Install;

import org.jboss.seam.annotations.Name;

import org.jboss.seam.annotations.Scope;

import org.jboss.seam.annotations.intercept.BypassInterceptors;

import org.jboss.seam.faces.DataModels;

import org.jboss.seam.framework.Query;

import org.jboss.seam.jsf.ArrayDataModel;

import org.jboss.seam.jsf.ListDataModel;

import org.jboss.seam.jsf.MapDataModel;

import org.jboss.seam.jsf.SetDataModel;

@Name("org.jboss.seam.faces.dataModels")

@Install(precedence=Install.FRAMEWORK,

CHAPTER 第 7 章 Seam 入门

385

7

classDependencies="javax.faces.context.FacesContext")

@Scope(ScopeType.STATELESS)

@BypassInterceptors

public class SeamDataModels extends DataModels {

@Override

public DataModel getDataModel(Query query) {

return super.getDataModel(query);

}

@Override

public DataModel getDataModel(Object value) {

if (value instanceof List) {

return new ListDataModel((List) value);

} else if (value instanceof Object[]) {

return new ArrayDataModel((Object[]) value);

} else if (value instanceof Map) {

return new MapDataModel((Map) value);

} else if (value instanceof Set) {

return new SetDataModel((Set) value);

} else if (value instanceof PaginationDataModel) {

return (PaginationDataModel)value;

} else {

throw new IllegalArgumentException("unknown collection type: "

+ value.getClass());

}

}

}

SeamDataModels 告诉 Seam 正确转换 PaginationDataModel,如果不正确进行 DataModle

转换时将有一个 IllegalArgumentException 被抛出。

剩下的事情就相当简单了,对 PaginationDataModel 进行一下注解程序就能够正常工作

了。

PaginationDataModel.java:

import java.io.IOException;

import java.util.ArrayList;

import java.util.HashMap;

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

386

import java.util.List;

import java.util.Map;

import javax.faces.context.FacesContext;

import org.ajax4jsf.model.DataVisitor;

import org.ajax4jsf.model.Range;

import org.ajax4jsf.model.SequenceRange;

import org.ajax4jsf.model.SerializableDataModel;

import org.jboss.seam.ScopeType;

import org.jboss.seam.annotations.Name;

import org.jboss.seam.annotations.Scope;

@Name("dataModel")

@Scope(ScopeType.PAGE)

public class PaginationDataModel extends SerializableDataModel{

//此处省略,与 7.4.1节中的相同

}

7.5 本章小结

本书的第 4 章 4.6.3 节介绍的 On-Demand 分页也是一种分页方式,但这种方式在实际

项目中是不可取的。原因是使用原来的 DataModel 模型,由于 JSF 的生命周期机制,在一

次分页的过程中会向数据库发送两次请求,这是应该避免的。虽然增加缓存会在一定程度

上避免这个现象,但这并不能从根本上解决问题。

通过一个简单分页的例子,我们看到了 Seam 优雅的解决方案。Seam 功能远远比本书

中所介绍的功能要多得多,大家可以从 Seam 文档中获取更多信息。

在 JEE 6.0 新规范中,吸取了 Seam 的成功经验,首次定义了 WebBeans 的概念。

WebBeans 将 JSF、JPA 和 EJB 3 编程统一为一个整体,弥补了三者集成之间的不足,而当

前 Seam 解决的问题恰恰是这方面的内容。由于内容的限制,本章主要侧重于 Seam 在 UI

方面的内容。感兴趣的读者可以在 JBoss 的网站上了解更多关于 Seam 的内容,同时也可以

在 JCP 的官方网站上了解到 WebBeans 的更多特性。

CHAPTER 第 7 章 Seam 入门

387

7

参考资料

[1] Seam 官方网站[http://seamframework.org]

[2] Seam 论坛[http://www.jboss.com/index.html?module=bb&op=main]

[3] 满江红翻译的 Seam 文档[http://www.redsaga.com/opendoc/Seam2.0/html_single/]

[4] IBM DeveloperWorks [http://www-128.ibm.com/developerworks]

[5] RichFaces 官方网站[http://labs.jboss.com/jbossrichfaces]

[6] RichFaces 论坛[http://jboss.com/index.html?module=bb&op=viewforum&f=262]

第Ⅲ部分 Ext 应用开发

CHAPTER

8

8.1 JavaScript 面向对象编程

许多 Web 开发人员对 JavaScript 的了解仅仅停留在简单的表单数据操作,以及浏览

器 DOM 对象的简单操作上,以达到一些数据验证和动态页面的效果。所以当要实现的

功能比较复杂时,写出的代码就显得凌乱并且难以维护,更不用说实现一个基于

JavaScript 的 UI 框架了。事实上,JavaScript 提供了完善的机制来实现面向对象的开发

思想。

本章假设读者已经了解面向对象思想的基本概念,熟悉对象、类、继承等基本术语。

以此为基础,将重点介绍如何在 JavaScript 中使用面向对象的思想,包括实现的原理、机

制和技巧。我们将使用 JavaScript 来实现以下面向对象特性:

对象、类;

封装;

多态;

继承。

1.对象

在 JavaScript 中创建一个对象非常简单,我们可以使用内建的 Object 对象来创建一个

对象:

第 8 章

ExtJS 框架的介绍和使用

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

390

var myObj=new Object; //创建一个名为 myObj的对象

myObj.name='sam'; //给 myObj对象添加一个 name属性,其值为 sam

myObj.age=28; //给 myObj对象添加一个 age属性,其值为 28

我们也可以使用 JSON(JavaScript Object Notation)① 来创建一个对象:

var myObj={name:’sam’,age:28}

//创建一个包含 name属性值为 sam,age属性值为 28的对象

2.类

JavaScript 不同于 Java、C++、C#等面向对象语言,它通过构造函数和原型对象

(prototype)来实现类的创建。为了创建一个类,还需要创建一个构造函数:

function Person(name,age){

this.name=name;

this.age=age;

}

这样我们就创建了一个构造函数(类),它包含两个属性:name 和 age。

var sam=new Person('sam',28); //创建一个 Person对象,name为 sam,age为 28

我们可以通过“.”运算符来访问它的属性:

alert(sam.name); //输出结果为 sam

var bob=new Person('bob',30); //创建一个 Person对象,name为 bob,age为 30

alert(bob.age); //输出结果为 30

细心的读者可能会发现,到目前为止,我们通过函数创建的对象只是封装了数据成员,

并没有封装相应的方法。下面我们将在 Person 类中添加一个方法:

function Person(name,age){

this.name=name;

this.age=age;

this.introduceSelf=function(){

alert('I am '+name+' , I am '+age +' years old.');

}

}

var sam=new Person('sam',28);

sam.introduceSelf() //输出结果为:I am sam,I am 28 years old

① JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,易于阅读和编写,同时也易于机器解析和生成。

它基于 JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999 的一个子集。JSON 采用完全独

立于语言的文本格式,但是也使用了类似于 C 语言家族的习惯(包括 C, C++, C#, Java、JavaScript、Perl、Python 等)。 这

些特性使 JSON 成为理想的数据交换语言。

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

391

8

var bob=new Person('bob',30);

bob.introduceSelf() //输出结果为:I am bob,I am 30 years old

但是上面这种添加方法的方式不是很好。因为 introduceSelf 函数对于所有 Person 的实

例来说,都是一样的,我们不希望在实例级别加上一个对于所有实例来说都一样的方法。

这里我们要引入原型对象(prototype)的介绍。

每个 JavaScript 构造函数都有一个内部引用指向另一个称为原型对象(prototype)

的对象。对于这个构造函数所产生的对象实例,该构造函数的原型对象所有属性对于

它们来说都是可见的,并且是共享的。所以有时候也称该原型对象是这些实例的原型

对象。对于这些实例,对原型对象的属性的访问方式和对自身的属性的访问方式是一

致的。下面我们举例说明。

首先定义一个构造函数(类):

function Person(name,age){

this.name=name;

this.age=age;

}

然后,在函数的原型对象中添加一个属性 kind:

Person.prototype.kind='animal';

接着创建两个 Person 的实例:

var p1=new Person('sam',28);

var p2=new Person('bob',30);

访问这两个实例的 kind 属性:

alert(p1.kind); //输出 animal

alert(p2.kind); //输出 animal

通过上面的例子,我们可以看到函数的原型对象对于所有实例来说是共享的,并且属

性的访问方式和实例本身的属性的访问方式完全一致。

如果修改这个属性,会怎么样呢?让我们接着往下看:

p1.kind='male';

alert(p1.kind); //输出 male

alert(p2.kind); //输出 animal

这是怎么回事呢?原来对于原型对象的操作,读写是不对称的。通过实例对属性名

的引用并不能修改原型对象属性的值,它只是在实例本身添加了一个和原型对象属性名

一样的属性,并将该值赋给自身的那个属性。而对属性的访问,JavaScript 首先会从对象

本身开始查找,如果找到则返回该属性的值;如果没有找到,才会在其原型对象中查找。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

392

所以对于 p1,当执行了 p1.kind='male'之后,p1 本身就有了一个 kind 属性,所以当再次访

问 p1 的 kind 属性时,就会直接返回 p1 本身 kind 属性值“male”,而不是其原型对象里

的值“animal”。

在了解了原型对象之后,我们再回到原来的例子。我们需要在构造函数 Person 的原型

对象上添加一个方法,这样这个方法就会被所有的 Person 对象共享:

Person.prototype. introduceSelf=function(){

alert('I am '+this.name+' , I am '+this.age +' years old.');

}

var p1=new Person('sam',28);

var p2=new Person('bob',30);

p1.introduceSelf() //输出结果为:I am sam,I am 28 years old

p2.introduceSelf() //输出结果为:I am bob,I am 30 years old

3.多态

JavaScript 允许我们将任意一个函数(function)分配给对象的一个属性。当使用

obj.function 的语法调用函数时,将把函数原来定义 this 的指向当前这个对象 obj(就像

它在构造函数中的那样)。所以,我们可以通过定义有相同名字的方法的对象,来简单地

实现多态性(polymorphism)。

//定义一个 dogSpeek函数 function dogSpeek(){

alert('I am '+this.name);

}

//定义一个 Dog类

function Dog(){

this.name='dog';

this.speek= dogSpeek;//将 dogSpeek 函数赋给 Dog的 speek属性

}

//定义一个 catSpeek函数

function catSpeek(){

alert('I am '+this.name);

}

//定义一个 Cat类

function Cat(){

this.name='cat';

this.speek= catSpeek; //将 catSpeek ()函数赋给 Cat的 speek属性

}

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

393

8

var dog=new Dog;

dog.speek();//输出“I am dog”

var cat=new Cat;

cat.speek();//输出“I am cat”

对于同一个方法,不同类的对象就展现出不同的行为,这样就实现了多态性。

4.继承

继承是面向对象开发的又一个重要概念,在 JavaScript 中通过原型链机制来实现类的

继承,当然也可以通过将一个类的 prototype 拷贝到另外一个类来实现继承。

function Base(x) // 定义一个父类

{

this.x = x;

}

Base.prototype.doIt = function() //在父类中添加一个方法

{

this.x += 1;

}

function Sub(x,y) //定义一个子类

{

Base.call(this,x); // 调用父类的构造函数(非必需)

this.y = y;

}

Sub.prototype = new Base; // 将Sub的原型对象修改为Base的实例,这是实现继承的关键一步

//因为 Sub类的原型对象是由构造函数 Base产生的,所以它的 constructor属性是 Base,

//我们需要把它改成 Sub

Sub.prototype.constructor=Sub;

var obj = new Sub(1,1);

alert(obj.x); //输出 1

alert(obj.y); //输出 1

obj.doIt();

alert(obj.x); //输出 2

从上面的例子我们可以看到,Sub 类的实例 obj 继承了 Base 类的属性 x,以及方法 doIt。

我们还可以通过拷贝父类中的方法来实现继承。

function Inherit(superclass, subclass) {

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

394

var from = superclass.prototype; // 父类的原型对象

var to = subclass.prototype; // 子类的原型对象

for(m in from) { //搜索原型对象中的所有属性

if (typeof from[m] != "function") continue; // 忽略非函数

to[m] = from[m]; // 拷贝方法

}

}

下面我们用这个函数来实现继承。

function Base(x){

this.x=x;

}

Base.prototype.doIt=function(){

this.x+=1;

}

function Sub(x,y){

Base.call(this,x);

this.y=y;

}

Inherit(Base,Sub);

var obj = new Sub(1,1);

alert(obj.x); //输出 1

alert(obj.y); //输出 1

obj.doIt();

alert(obj.x); //输出 2

通过上面的方式,同样实现了继承。对于第二种方式,说是继承并不严格,因为它只

是借用 Base 类中的方法,它们之间没有真正的继承关系。我们可以使用 instanceof 方法来

证明这一点。

对于第一种方式:

alert(obj instanceof Base); //输出 true,说明 Sub类对象是一个 Base类的实例

对于第二种方式:

alert(obj instanceof Base); //输出false,说明Sub类对象不是一个Base类的实例

总结:JavaScript 不同于 Java、C++、C#等基于类的面向对象语言,它是基于原型对象

的面向对象语言。它通过原型对象和构造函数可以很好地实现面向对象的开发,这为使用

JavaScript 开发大型的、复杂的程序提供了可能。

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

395

8

8.2 ExtJS 简介及第一个例子(HelloWorld)

ExtJS 是基于纯 HTML/CSS+JavaScript 技术的 UI 开源框架,它 初由 Yahoo! UI 的

JavaScript 库扩展而来,发展到现在已经支持多种底层的 JavaScript 库,包括 YUI、jQuery、

Prototype 及它自己的底层实现。它提供了丰富的跨浏览器的 UI 组件,灵活地采用

JSON/XML 作为数据的交换格式,目前已经发行到版本 2.0(如无特别说明,本书的所有

描述及样例代码都是基于 ExtJS 2.0 的)。

ExtJS 支持的底层 JavaScript 库:

Ext Base

Yahoo! UI(.12+)

jQuery (1.1+)

Prototype (1.5+) / Scriptaculous (1.7+)

ExtJS 支持的浏览器:

Internet Explorer 6+

FireFox 1.5+ (PC、MAC)

Safari 2+

Opera 9+

在 ExtJS 的官方网站(http://extjs.com)上,我们可以看到很多知名企业都是 ExtJS 的

客户(如图 8-1 所示),其中不乏 IBM、SAP 之类的软件巨头。

图 8-1 ExtJS 客户列表

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

396

在开始使用 ExtJS 进行开发之前,首先我们需要从 http://extjs.com/deploy/ext-2.0.zip 下

载 Ext 2.0 的库。解压后其目录结构如图 8-2 所示。

图 8-2 ExtJS 的目录结构

目录 adapter 中存放的是不同底层的 JavaScript 的 Framework 的实现和适配器。

目录 build 中存放的是 Ext 源代码的 小化版本,用于提供用户定制选择相关的功

能。当用户只需要用到 Ext 中部分功能时,只要把该功能涉及的代码的 小化版本

引入即可。

目录 docs 中存放的是 Ext 的 API 文档。

目录 examples 中存放的是 Ext 的一些例子程序。

目录 resources 中存放的是 Ext 所用到的一些图片、样式等资源文件。

目录 source 中存放的是 Ext 的源代码,相比于 build 中的代码,它的可读性很好,

build 中的代码经过压缩,不具备可读性。

ext-all.js 是 source 目录下所有代码的合并压缩版本,在项目发布时使用它,可以节

省 JS 文件的载入时间。

ext-all-debug.js 和 ext-all.js 内容一样,都是 source 目录中代码的合并,但没有

进行压缩,具有较好的可读性,在项目开发时可以使用它,有助于调试和定位

错误。

ext-core.js 是 source/core 目录中所有代码的合并压缩版本。

ext-core-debug.js 是 source/core 目录中所有代码的合并非压缩版本。

如果程序中不需要用到其他的 JavaScript 库,建议导入 Ext 自己的实现;如果用到了其

他的 JavaScript 库,则导入相应的库即可。

在开始第一个程序之前,我们先创建一个动态的 Web 项目 EXTExamples。在项目的 src

目录下存放相应的 Java 源代码;在 WebContent 目录下,我们创建了一个 js 目录存放 JS 文

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

397

8

件,一个 pages 目录存放 HTML 文件,以及一个 lib 目录,我们将解压后的 ext2.0 目录拷贝

到该目录下。 终的目录结构如图 8-3 所示。

图 8-3 例子程序的目录结构

让我们开始编写第一个程序(HelloWorld)。

在 pages 目录中创建一个 helloworld.htm 文件,用文本编辑器打开它,添加如下

内容:

<link rel="stylesheet" type="text/css"

href="../lib/ext2.0/resources/css/ext-all.css" /> ○1

<script type="text/javascript"

src="../lib/ext2.0/adapter/ext/ext-base.js"></script> ○2

<script type="text/javascript" src="../lib/ext2.0/ext-all-debug.js">

</script> ○3

<script>

Ext.onReady(function(){

Ext.MessageBox.alert('Message', 'Hello World!'); ○4

});

</script>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

398

① 导入 ExtJS 的样式文件。

② 导入 ExtJS 的 Base 库。

③ 导入 ExtJS 库。

内容①②③是必需的,页面需要使用 ExtJS 时,必须引入这 3 个文件。注意②和③的

顺序不能颠倒,因为 JavaScript 的加载是有顺序的。

④ 是真正的代码部分。在这部分中,我们调用了 Ext.onReady()方法,该方法将一个函

数绑定到DocumentReady事件上,当一个页面就绪时(在 onload事件和图像文件载入之前),

其所绑定的函数将会执行。我们再看一下被绑定的函数:

function(){

Ext.MessageBox.alert('Message', 'Hello World!');

}

它是一个匿名函数,里面只有一句:

Ext.MessageBox.alert('Message', 'Hello World!');

这行代码的意思类似于 window.alert()函数,显示一个标题为 Message,内容为 Hello

World!的对话框,其运行结果如图 8-4 所示。

图 8-4 HelloWorld 例子的运行结果

通过这个简单的 HelloWorld 例子,我们了解了使用 ExtJS 框架进行开发的基本流程,

接下来需要学习的就是 ExtJS 的各种组件的使用了。在接下来的章节中,我们将一一介绍

ExtJS 中比较常用的组件。

8.3 ExtJS 布局(layout)

ExtJS 的布局非常美观而且强大,代码书写起来也非常简洁。不同于 ExtJS 1.1,在 ExtJS

2.0 中,布局组件不需要通过 new 运算符独立创建,而是在需要应用布局的容器(Container)

中,配置相应的 layout 属性和 layoutConfig 属性。对于 Container 的子类组件,同样拥有该

属性。ExtJS 组件继承层次结构图如图 8-5 所示。

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

399

8

图 8-5 ExtJS 组件继承层次结构图

下面我们创建一个简单的例子,来演示 layout 组件的使用。为了保持代码的清晰、整

洁,我们将 JavaScript 代码独立地写在一个文件中,然后在 HTML 文件中引用它。我们通

过如下几个步骤来实现一个简单的布局例子。

在 pages 目录中新建一个 layout.htm 文件,除了在其中添加和 HelloWorld 例子一样

的 3 行代码之外,还需要添加一行代码,导入自己的 JavaScript 文件:

<script type='text/Javascript' src='../js/layout.js'></script>

在 js 目录中创建一个 layout.js 文件,编辑其内容如下:

Ext.onReady(function(){

var viewport = new Ext.Viewport({

layout: 'border',

items: [{

title: 'north',

region: 'north',

split: true,

border: true,

collapsible: true,

height: 100

}, {

title: 'south',

region: 'south',

split: true,

border: true,

collapsible: true,

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

400

height: 100

}, {

title: 'east',

region: 'east',

split: true,

border: true,

collapsible: true,

width: 120

}, {

title: 'west',

region: 'west',

split: true,

border: true,

collapsible: true,

width: 120

}, {

title: 'center',

region: 'center'

}]

})

});

运行 layout.htm,结果如图 8-6 所示。

图 8-6 ExtJS 布局组件运行结果

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

401

8

除了中心区域外,东、西、南、北每个区域都可以通过单击其上面的 图标进行收缩

和展开,并且每个区域可以通过拖动分隔符来调整大小。

我们来看一下 layout.js 代码,它创建了一个 Ext.Viewport 对象。Viewport 对象相当于

所有其他组件的一个父容器,在一个页面中,只能有一个 Viewport 对象,它能根据浏览

器窗口的大小,自动调整自己的大小。我们还传递了一个对象给 Viewport 的构造函数,

它有两个属性:一个是 layout,指明 Viewport 中的内容采用何种布局;另一个是 items,

它是一个数组,罗列了 Viewport 对象中容纳的子组件。在 items 的每一个对象中,都有一

些属性的描述,通过这些属性的描述,决定了该组件的展现形式。比如 items 中的第一个

对象:

{

title: 'north', //指明了这个区域的标题是 north

region: 'north', //指明了这个属性所在的区域是 north

split: true, //指明了是否在区域之间有分隔栏

//用户可以通过拖拉分隔栏来动态地改变区域的大小

border: true, //区域是否有边框

collapsible: true, //是否可以收缩和展开

height: 100 //区域高度是 100

}

当然,每个组件还有很多其他属性,我们可以参考相应的 API 文档进行设置。

8.4 嵌套布局(NestedLayout)

如果需要在布局的某个区域中嵌套一个布局怎么办?实现起来也非常简单,因为继承

了 Container 类的每个组件都具有 layout 属性,对其进行设置相应的属性就可以了。我们通

过以下几步来实现一个嵌套布局的例子:

在 pages 目录中创建一个 nestedLayout.htm 文件,除了在其中添加和 HelloWorld 例

子一样的 3 行代码之外,还需要添加一行代码,导入自己的 JavaScript 文件:

<script type='text/Javascript' src='../js/nestedLayout.js'></script>

在 js 目录中创建一个 nestedLayout.js 文件,拷贝上面 layout 例子中的 layout.js 的内

容,然后对 items 中的 center 对象修改如下:

{

title: 'center',

region: 'center',

layout: 'border',

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

402

items: [{

title: 'nested north',

region: 'north',

split: true,

border: true,

collapsible: true,

height: 100

}, {

title: 'nested center',

region: 'center',

split: true,

border: true

}]

}

类似于 Viewport,我们在这个对象中添加了 layout 属性为 border,然后在其中加入了

两个子组件:nested north 和 nested center。其页面运行结果如图 8-7 所示。

图 8-7 ExtJS 嵌套布局运行结果

这样我们就在 center 区域中又嵌套了两个区域:nested north 和 nested center。

除了在例子中使用的 border 布局之外,Ext 还提供了很多其他的布局方式,如图 8-8

所示。

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

403

8

图 8-8 Ext 中布局对象的类层次结构

ContainerLayout

它是其他一切布局管理器的基类,容器若不指定某个布局管理器,则默认的管理器就

是这个 ContainerLayout。ContainerLayout 没有任何的外观表示,其主要职责是容纳子项目、

控制渲染和一些常见任务,如调节大小缓冲(resize buffering)。 ContainerLayout 常用于扩

展制定的布局,很少实例化直接使用。详细内容请参考 API 文档。

CardLayout

CardLayout 将容器中的每个组件当做一个卡片来处理。在某一时间,只有一个卡片是

可见的,容器像一个卡片堆栈一样工作。在大多数的情况下,用于向导(Wizards)、制定

的 tab 实现或其他多页面信息的场合。详细内容请参考 API 文档。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

404

AbsoluteLayout

这是一个非常简单的布局,通过 X/Y 坐标精确地定位包含各项的相关容器。详细内容

请参考 API 文档。

ColumnLayout

适用于多个列并排结构的布局风格,每个列的宽度需由像素值或百分比指定,但高度

自适应于内容的高度。

AccordionLayout

AccordionLayout 包含了一组像卡片垂直方向堆栈的面板,通过展开或收缩来显示内

容,在某一时间,只有一个卡片是可见的。

FitLayout

这是为一个简单的布局,主要是创建一个适应容器大小的布局区域。如果没有特定的

布局要求,这是容器 好的默认布局。

AnchorLayout

这是一些固定元素相对于容器 4 条边而设计的布局,元素可通过与边缘的百分比或指

定一个值来定位。详细内容请参考 API 文档。

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

405

8

FormLayout

FormLayout 是为创建一个要提交数据条目的表单而设计的布局风格。注意:一般来讲,

和 FormPanel 相似,该布局类都有表单提交的自动处理,你会更倾向使用前者。FormPanel

必须指定 layout:'form'(一定是这样),所以表单额外需要一个布局将其嵌套。详细内容请参

考 API 文档。

BorderLayout

与 Ext1.x 的 BorderLayout 的布局完全一致。布局区域支持嵌套、滑动条面板和可关闭、

微调的分隔区域,对于一些典型的业务程序的首要 UI 尤为适用。详细内容请参考 API 文档。

TableLayout

TableLayout 的主要目的是通过一个表格的形式划分区域。实际上,也是生成一个 table

的 HTML 代码片段。

通过上面对布局组件的介绍,我们看到 Ext 提供了丰富的布局组件,并且布局组件可

以灵活地嵌套,这样 Ext 将基本上能满足我们所有的布局需求。

8.5 表单组件(Ext.form.FormPanel)

在 B/S 系统中,与 Server 端的交互都是通过表单提交来完成的。Ext 也提供了相应的

表单组件来展现表单,以及客户端的数据验证。接下来我们利用表单组件来制作一个简单

的用户登录界面。

在 pages 目录中创建一个 form.htm 文件,除了在其中添加和 HelloWorld 例子一样

的 3 行代码之外,还需要添加一行代码,导入自己的 JavaScript 文件:

<script type='text/Javascript' src='../js/form.js'></script>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

406

在 js 目录中创建一个 form.js 文件,输入以下内容:

Ext.onReady(function(){

Ext.QuickTips.init();//初始化全局的提示信息,为显示提示信息做准备

var simple = new Ext.FormPanel({

labelWidth: 75,

url: '/checkUser',//表单提交地址

frame: true,

title: 'User Login',

bodyStyle: 'padding:5px 5px 0',

width: 350,

defaults: { //设置默认属性

width: 230,

allowBlank: false //输入框不允许为空

},

defaultType: 'textfield', //默认为文本输入框

items: [{ //添加两个输入框

fieldLabel: 'Username',

name: 'username'

}, {

fieldLabel: 'Password',

name: 'password',

inputType :'password'//设置表单的type为password,这样输入框为密码框

}],

buttons: [{ //添加两个按钮

text: 'Login'

}, {

text: 'Reset'

}]

});

simple.render(document.body);

});

在上述代码中,我们创建了一个 FormPanel 组件,其中包含两个输入框组件

Username 和 Password,以及两个按钮组件 Login 和 Reset。运行 form.htm 结果如图 8-9

所示。

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

407

8

图 8-9 用户登录界面运行结果

当用鼠标单击 Username 输入框后,不输入任何内容,然后单击 Password 输入框,使

得焦点离开 Username 输入框,表单就会显示验证的提示信息,如图 8-10 所示。

图 8-10 用户登录界面验证提示

这是我们在 form.js 中配置了 allowBlank: false 所产生的效果。

到目前为止,这个表单只能接收用户的输入,以及对数据进行验证,但还不能提交服

务器端,因为我们没有添加任何按钮的事件响应代码。所以单击“Login”按钮时没有任何

反应。为了实现表单的提交,我们给 Login 按钮添加一个 handler 属性,该属性的值为一个

函数的引用,这个函数用于响应按钮的单击事件。

修改 form.js 的部分代码如下:

buttons: [{ //添加两个按钮

text: 'Login'

handler:function(){simple.getForm().submit();}

}, {

text: 'Reset'

}]

handler 属性引用的函数的功能是获取 simple 这个 FormPanel 的 BasicForm 组件,然后

调用其 submit 方法。

现在单击“Login”按钮,这时表单就会提交。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

408

注意:

Ext 表单的提交是采用异步的 Ajax 请求来实现的,所以单击“Login”按钮不会

看到页面的跳转或刷新,但我们可以通过 FireBug 或者 HttpWatch 之类的工具来查

看。通过 FireBug 控制台看到的数据提交信息,如图 8-11 所示。

图 8-11 FireBug 控制台显示结果

同样,我们给 Reset 按钮添加 handler 属性,用于重置整个表单,这样整个表单的例子

就完整了。修改代码如下:

buttons: [{

text: 'Login',

handler:function(){simple.getForm().submit();}

}, {

text: 'Reset',

handler:function(){simple.getForm().reset();}

}]

通过上述代码可以看到,利用 Ext 的表单组件,可以轻松地创建表单,配置了 url 属性

就可以和服务器进行异步交互,不需要自己编写任何 Ajax 的代码。Ext 也提供了很多其他

格式的表单项,如日期选择框(Ext.form.DateField)、下拉框(Ext.form.ComboBox)、复选

框(Ext.form.Checkbox)、单选框(Ext.form.Radio)、文本域(Ext.form.TextArea)、html 编

辑器(Ext.form.HtmlEditor)等,读者可以根据具体的需求,选择使用相应的组件即可,具

体配置可参考 API 文档。

8.6 树组件(Ext.tree.TreePanel)

在现实环境中,很多数据结构都是树状的,比如文件系统、公司的组织架构等。对

于这种情况,在系统中用树来表现是合理的而且友好的,所以我们需要一个树组件来展

现这种数据结构。Ext 中也提供了树组件 TreePanel,用它可以很好地来展现这种树状数

据结构。

假设某公司的组织架构图如图 8-12 所示。

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

409

8

图 8-12 某公司的组织架构图

我们使用 Ext 树组件来实现这个组织架构图。

(1)在 pages 目录中创建一个 tree.htm 文件,除了在其中添加和 HelloWorld 例子一样

的 3 行代码之外,还需要添加一行代码,导入自己的 JavaScript 文件:

<script type='text/Javascript' src='../js/tree.js'></script>

(2)在 js 目录中创建一个 tree.js 文件,输入以下内容:

Ext.onReady(function(){

//定义树的数据

var json = [{

'text': '北京分公司', //树节点显示内容

'id': '1', //树节点标识

'leaf': false, //是否叶子节点

'cls': 'folder', //节点样式的 class

'children': [{ //子节点

'text': '营销中心',

'id': '11',

'leaf': true,

'cls': 'file'

}, {

'text': '行政人事部',

'id': '12',

'leaf': true,

'cls': 'file'

}, {

总公司

北京分公司 上海分公司

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

410

'text': '财务部',

'id': '13',

'leaf': true,

'cls': 'file'

}]

}, {

'text': '上海分公司',

'id': '2',

'leaf': false,

'cls': 'folder',

'children': [{

'text': '营销中心',

'id': '21',

'leaf': true,

'cls': 'file'

}, {

'text': '行政人事部',

'id': '22',

'leaf': true,

'cls': 'file'

}, {

'text': '财务部',

'id': '23',

'leaf': true,

'cls': 'file'

}]

}];

//定义 TreePanel

var tree = new Ext.tree.TreePanel({

el: document.body,

animate: true,

autoScroll: true,

loader: new Ext.tree.TreeLoader(),

autoHeight: true

});

//定义根节点

var root = new Ext.tree.AsyncTreeNode({

text: '总公司', //根节点显示内容

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

411

8

draggable: false,

id: 'root',

children: json //定义其子节点

});

tree.setRootNode(root);

tree.render(); //展现树

});

运行 tree.htm,结果如图 8-13 所示。

图 8-13 tree.htm 运行结果

上面的树中数据量比较小,而且内容都是固定的,所以静态的树可以满足要求。但是

如果整棵树的节点非常多,我们希望在展开某个节点时才获取该节点下面的数据,以减轻

对服务器的压力。Ext 对这种情况也提供了很好的支持,我们只需要在 TreeLoader 中指定

获取子节点数据的 url 即可。我们修改 tree.js 如下:

Ext.onReady(function(){

//定义 TreePanel

var tree = new Ext.tree.TreePanel({

el: document.body,

animate: true,

autoScroll: true,

loader: new Ext.tree.TreeLoader({url:’../treeServlet’}),

autoHeight: true

});

//定义根节点

var root = new Ext.tree.AsyncTreeNode({

text: '总公司', //根节点显示内容

draggable: false,

id: 'root'

});

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

412

tree.setRootNode(root);

tree.render(); //展现树

});

为了实现动态的数据输出,我们用一个 Servlet 来实现。编写一个 TreeServlet.Java 文件,

其 web.xml 中的配置如下:

<servlet>

<servlet-name>TreeServlet</servlet-name>

<servlet-class>org.bluelight.ch08.TreeServlet</servlet-class>

</servlet>

<servlet-mapping>

<servlet-name>TreeServlet</servlet-name>

<url-pattern>/treeServlet</url-pattern>

</servlet-mapping>

我们在 TreeServlet 的 doPost 方法中输出动态的 JSON 数据。其代码如下:

public void doPost(HttpServletRequest request, HttpServletResponse response)

throws ServletException, IOException {

response.setContentType("text/html");

response.setCharacterEncoding("utf-8");

PrintWriter out = response.getWriter();

//获取当前节点的标识

String node=request.getParameter("node");

//要返回的下级节点数据的 JSON

String json="";

//根节点

if("root".equals(node)){

json="[{ 'text': '北京分公司','id':'1','leaf':false,'cls':'folder'}," +

"{ 'text':'上海分公司','id':'2','leaf':false,'cls':'folder'}]";

}

//北京分公司

else if("1".equals(node)){

json="[{ 'text':'营销中心','id':'11','leaf': true,'cls':'file'}," +

"{ 'text': '行政人事部','id': '12','leaf': true,'cls': 'file'}," +

"{ 'text': '财务部','id': '13','leaf': true,'cls': 'file'}]";

}

//上海分公司

else if("2".equals(node)){

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

413

8

json="[{ 'text':'营销中心','id': '21','leaf': true,'cls':'file'}," +

"{ 'text':'行政人事部','id':'22','leaf': true,'cls': 'file'}," +

"{ 'text': '财务部','id': '23','leaf': true,'cls': 'file'}]";

}

out.print(json);

out.flush();

out.close();

}

我们通过 http 访问这个 tree.htm 文件,当点击总公司展开下级节点时,用 FireBug 查看

其向服务器提交的数据,如图 8-14 所示。

图 8-14 FireBug 控制台 Post 信息

它将当前节点的 id 以 node 为参数名传递给 Server 端,所以我们在 TreeServlet 中就可

以获取它的内容,然后输出其子节点的 JSON 数据。我们也可以通过 FireBug 查看服务器

输出内容,如图 8-15 所示。

图 8-15 FireBug 控制台 Response 信息

通过以上简单地设定 TreeLoader 的 url 属性,我们就可以异步、动态地获取树的节点

数据。

8.7 对话框组件(Ext.Window)

对话框在与用户进行交互时具有非常重要的作用。Ext 除了提供用于替代浏览器默认的

Alert、Prompt、Confirm 对话框之外,还提供了完全可自定义的对话框。我们将通过下面的

例子来对这些内容进行演示。

在 pages 目录下新建一个 dialog.htm 文件,在其中添加了对 Ext 的 CSS 文件及两个必

需的 JavaScript 文件后,在文件的<body>和</body>之间添加如下一行代码:

<input type='button' value='Alert' onclick="Ext.MessageBox.alert(

'example','This is Alert');">

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

414

这行代码类似于 HelloWorld 例子,但是它是在单击一个按钮时触发的,而不是在页面

载入时触发的。运行页面,单击“Alert”按钮,显示如图 8-16 所示。

图 8-16 单击“Alert”按钮后的运行结果

这个对话框类似于 window.alert,下面我们在 dialog.htm 中添加一行代码,用来显示一

个类似于 window.comfirm 的对话框,代码如下:

<input type='button' value='Confirm' onclick="Ext.MessageBox.confirm

('example','Are you sure',

function(btn){alert('you pressed: '+btn);});">

代码中 confirm 函数有 3 个参数,第 1 个参数是对话框的标题,第 2 个参数是对话框

的消息,第 3 个参数是一个回调函数,当单击对话框上的按钮时,它会显示你单击的是哪

个按钮。运行 dialog.htm,单击“Confirm”按钮,显示如图 8-17 所示。

图 8-17 单击“Confirm”按钮后的运行结果

当单击上面的“Yes”按钮时,则第 3 个参数所指定的回调函数会被执行,显示结果如

图 8-18 所示。

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

415

8

图 8-18 单击“Yes”按钮后的结果

同样,Ext 也提供了类似于 window.prompt 函数的实现。我们在 dialog.htm 中继续添加

一行代码如下:

<input type='button' value='Prompt' onclick="Ext.MessageBox.prompt

('example','Enter you name:',

function(btn,text){alert('the text is:'+text+' and you pressed : '+btn);});">

类似于 confirm 函数,prompt 函数也有 3 个参数,分别是窗口标题、消息内容和回调

函数,不同之处在于 prompt 的回调函数为两个参数,即 btn 和 text,其中 btn 代表用户所

单击的按钮,text 代表用户所输入的内容。运行 dialog.htm,单击 Prompt 按钮,显示如图

8-19 所示。

图 8-19 单击“Prompt”按钮后的运行结果

在输入框中输入 sam,然后单击“OK”按钮,则回调函数被调用,页面显示如图 8-20

所示。

图 8-20 单击“OK”按钮后的运行结果

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

416

至此,我们完成了对浏览器自带对话框的模拟实现,并且提供了回调函数来处理单击

后的事件。接下来我们要定制一个留言对话框,在 dialog.htm 中导入 customDialog.js 文件,

并且添加一个 Leave Message 按钮。添加代码如下:

导入 JavaScript 文件:

<script type='text/Javascript' src='../js/customDialog.js'></script>

添加按钮:

<input type='button' value='Leave Message' onclick="customDialog();">

在 js 目录下创建一个 customDialog.js 文件,添加代码如下:

function customDialog(){

var win = new Ext.Window({

layout: 'fit',

width: 500,

height: 300,

plain: true,

title: 'Leave Message',

items: new Ext.form.FormPanel({

labelWidth: 55,

url: ' saveMsg ',

defaultType: 'textfield',

bodyStyle: 'padding:15px 15px 0',

items: [{

fieldLabel: 'E-Mail',

name: 'email',

anchor: '100%'

}, {

fieldLabel: 'Title',

name: 'title',

anchor: '100%'

}, {

xtype: 'textarea',

hideLabel: true,

name: 'msg',

anchor: '100% -100'

}]

}),

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

417

8

buttons: [{

text: 'Submit'

}, {

text: 'Close'

}]

});

win.show();

}

在这段代码中,首先创建了一个 Window 对象,Ext 中的对话框实际上是用 Window

组件来实现的,这个 Window 组件采用 fit 的布局。我们在其中添加了一个 FormPanel 子

组件,在 FormPanel 组件中添加了两个表单输入区(email、title)、一个输入域(msg)和

两个按钮(Submit、Close)。运行 dialog.htm,单击 Leave Message 按钮,显示如图 8-21

所示。

图 8-21 留言对话框运行结果

通过 Leave Message 示例,我们可以定制任意形式的对话框,因为在 Window 中,可以

定义布局、添加子组件,如表格、树等,完全可以满足我们的需求。

8.8 表格组件(Ext.grid.GridPanel)

在 B/S 系统中,表格是一个重要的、运用非常广泛的组件。Ext 提供的表格组件可以完

美地展示表格,并且支持不同的数据交换格式,支持排序、分页。下面我们先看一个简单

的例子。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

418

(1)在 pages 目录下创建一个 grid.htm 文件,除了在其中添加和 HelloWorld 例子一样

的 3 行代码之外,还需要添加一行代码,导入自己的 JavaScript 文件:

<script type='text/Javascript' src='../js/grid.js'></script>

(2)在 js 目录中创建一个 grid.js 文件,输入以下内容:

Ext.onReady(function(){

//定义表格数据

var myData = [[1, 'sam', 'male'], [2, 'lucy', 'female'], [3, 'tom','male']];

//定义表格数据格式

var store = new Ext.data.SimpleStore({

fields: [{

name: 'id'

}, {

name: 'name'

}, {

name: 'gender'

}],

data: myData

});

//创建表格

var grid = new Ext.grid.GridPanel({

store: store,

//定义列格式

columns: [{

header: "编号",

dataIndex: 'id'

}, {

header: "姓名",

dataIndex: 'name'

}, {

header: "性别",

dataIndex: 'gender'

}],

title: 'Grid Example'

});

grid.render(document.body);

});

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

419

8

运行结果如图 8-22 所示。

图 8-22 grid.html 运行结果

要让表格支持排序,只需要在列格式的定义中添加一个属性:sortable=true 即可。修改

grid.js 文件,如下所示:

……

var grid = new Ext.grid.GridPanel({

store: store,

//定义列格式

columns: [{

header: "编号",

dataIndex: 'id',

sortable:true

}, {

header: "姓名",

……

刷新 grid.htm,在表头编号上单击,则可以看到表格中数据进行了排序,并且在表头

右侧出现下拉箭头,单击下拉箭头,会出现如图 8-23 所示的菜单,通过这个菜单,我们可

以对表格进行排序、筛选显示。

图 8-23 ExtJS 表格组件的排序、筛选功能

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

420

我们也可以定制单元格中内容的显示方式,只要定义自己的 renderer 函数就可以

了。下面我们把性别用男、女的头像来显示。在 grid.js 文件中添加一个函数,定义

如下:

var genderRenderer = function(value){

if (value == 'male') {

return '<img src=\'../lib/ext2.0/examples/shared/icons/fam/user.gif\'>';

}

else {

return '<img src=\'../lib/ext2.0/examples/shared/icons/fam/user_female.gif\'>';

}

}

然后在表格的 columns 定义中修改 gender 的 renderer 属性如下:

……

{

header: "性别",

dataIndex: 'gender',

renderer: genderRenderer

}

……

这样,当重新运行 grid.htm 时,其运行结果将会如图 8-24 所示。

图 8-24 自定义 renderer 运行结果

接下来举一个分页表格的例子。我们先复制一份 grid.htm,并将其重命名为

“pagingGrid.htm”;复制一份 grid.js,并将其重命名为“pagingGrid.js”。在这两个新文件的基

础上进行修改来添加表格的分页功能。

我们先将 pagingGrid.htm 中对 grid.js 的引用,改成对 pagingGrid.js 的引用,代码如下

所示:

<script type='text/Javascript' src='../js/pagingGrid.js'></script>

下面我们修改 pagingGrid.js。

首先把分页组件显示出来,只需要在 GridPanel 的定义中加上如下代码(粗体部分)即

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

421

8

可:

……

title: 'Grid Example',

bbar: new Ext.PagingToolbar({

pageSize: 10,

store: store,

displayInfo: true,

displayMsg: '显示 {0} - {1} 共 {2}',

emptyMsg: "内容为空"

})

……

再次运行 pagingGrid.htm,显示结果如图 8-25 所示。

图 8-25 添加分页组件后的运行结果

我们还需要对 pagingGrid.js 进行改进,让它能从服务器获取分页数据。 后修改结果

如下所示:

/**

* @author Administrator

*/

var genderRenderer = function(value){

if (value == 'male') {

return '<img src=\'../lib/ext2.0/examples/shared/icons/fam/user.gif\'>';

}

else {

return'<img src=\'../lib/ext2.0/examples/shared/icons/fam/user_female.gif\'>';

}

}

Ext.onReady(function(){

//定义表格数据格式

var store = new Ext.data.Store({

proxy: new Ext.data.HttpProxy({

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

422

url: '../pagingGridServlet'

}),

reader: new Ext.data.JsonReader({

totalProperty: 'totalProperty',

root: 'root'

}, [{

name: 'id'

}, {

name: 'name'

}, {

name: 'gender'

}])

});

store.load({

params: {

start: 0,

limit: 10

}

});

//创建表格

var grid = new Ext.grid.GridPanel({

store: store,

//定义列格式

columns: [{

header: "编号",

dataIndex: 'id',

sortable: true

}, {

header: "姓名",

dataIndex: 'name'

}, {

header: "性别",

dataIndex: 'gender',

renderer: genderRenderer

}],

title: 'Grid Example',

autoHeight: true,

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

423

8

bbar: new Ext.PagingToolbar({

pageSize: 10,

store: store,

displayInfo: true,

displayMsg: '显示 {0} - {1} 共 {2}',

emptyMsg: "内容为空"

})

});

grid.render(document.body);

});

在上面代码中,先重新创建了一个 Store 对象,之前的 SimpleStore 对于处理数组型的

数据会比较简便,现在我们不需要了,所以直接用 Store 对象就可以。然后在 Store 中定义

了一个 HttpProxy,其 url 指向返回数据的 Servlet,同时还定义了一个 JsonReader 来读取返

回的数据。Store 在 load 数据时,需要指定开始的编号(start),以及每页的记录数(limit)。

客户端这边这样就完成了,接下来实现服务器端。我们创建一个名为 PagingGridServlet 的

Servlet,在其 doPost 方法中添加以下代码:

public void doPost(HttpServletRequest request,

HttpServletResponse response)throws ServletException, IOException {

response.setContentType("text/html");

PrintWriter out = response.getWriter();

String start = request.getParameter("start");

String limit = request.getParameter("limit");

String json = "{totalProperty:100,root:[";

int iStart = Integer.parseInt(start);

int iLimit = Integer.parseInt(limit);

for (int i = iStart; i < iStart + iLimit; i++) {

json += "{id:" + i + ",name:'user" + i + "',gender:'"

+ (i % 2 == 0 ? "male" : "female") + "'}";

if (i != iStart + iLimit - 1) {

json += ",";

}

}

json += "]}";

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

424

out.print(json);

out.flush();

out.close();

}

在上述代码中,先获取请求的输入参数 start 和 limit,这是在 store.load 时指定的,然

后程序返回一个 JSON 的字符串,在该 JSON 中,定义了一个 totalProperty 属性,用来指定

总记录数;定义了一个 root数组,数组中的内容为记录的数据。它的格式需要和在 JsonReader

中定义的一致,这样数据才会被正确地解析。为了让 Servlet 能够运行,在 web.xml 中添加

配置如下:

<servlet>

<servlet-name>PagingGridServlet</servlet-name>

<servlet-class>org.bluelight.ch08.PagingGridServlet</servlet-class>

</servlet>

<servlet-mapping>

<servlet-name>PagingGridServlet</servlet-name>

<url-pattern>/pagingGridServlet</url-pattern>

</servlet-mapping>

这样,我们把程序发布到 Tomcat 上,运行结果如图 8-26 所示。

图 8-26 分页例子程序运行结果

单击 按钮,列表就会翻页,如图 8-27 所示。

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

425

8

图 8-27 单击分页按钮后的显示结果

8.9 菜单组件(Ext.menu.Menu)

在桌面程序中,菜单是 常见的组件,作为 RIA 的 JavaScript 的框架,菜单组件自然

不能少。Ext 提供的菜单组件可以完全模拟桌面的菜单功能和效果。

下面通过一个和 Windows 的记事本菜单一样的例子来展示 Ext 的菜单组件。

首先,在 pages 目录下创建一个 menu.htm 文件,除了在其中添加和 HelloWorld 例子一

样的 3 行代码之外,还需要添加一行代码,导入自己的 JavaScript 文件:

<script type='text/Javascript' src='../js/menu.js'></script>

然后,在 js 目录下新建一个 menu.js 文件,添加内容如下:

Ext.onReady(function(){

//定义“文件”菜单

var fileMenu = new Ext.menu.Menu({

id: 'fileMenu',

items: [{

text: '新建'

}, {

text: '打开...'

}, {

text: '保存'

}, {

text: '另存为...'

}, {

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

426

text: '页面设置...'

}, {

text: '打印...'

}, {

text: '退出'

}]

});

//定义“编辑”菜单

var editMenu = new Ext.menu.Menu({

id: 'editMenu',

items: [{

text: '撤销',

disabled: true

}, '-', {

text: '剪切',

disabled: true

}, {

text: '复制',

disabled: true

}, {

text: '粘贴'

}, {

text: '删除',

disabled: true

}, '-', {

text: '查找...',

disabled: true

}, {

text: '查找下一个',

disabled: true

}, {

text: '替换...'

}, {

text: '转到...',

disabled: true

}, '-', {

text: '全选'

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

427

8

}, {

text: '时间/日期'

}]

});

//定义“格式”菜单

var formatMenu = new Ext.menu.Menu({

id: 'formatMenu',

items: [{

text: '自动换行',

checked: true

}, {

text: '字体...'

}]

});

//定义“查看”菜单

var veiwMenu = new Ext.menu.Menu({

id: 'veiwMenu',

items: [{

text: '状态栏',

disabled: true

}]

});

//定义“帮助”菜单

var helpMenu = new Ext.menu.Menu({

id: 'helpMenu',

items: [{

text: '帮助主题'

}, '-', {

text: '关于记事本'

}]

});

var tb = new Ext.Toolbar();

tb.render(Ext.getBody());

tb.add({

id: 'file',

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

428

text: '文件',

menu: fileMenu

}, {

text: '编辑',

menu: editMenu

}, {

text: '格式',

menu: formatMenu

}, {

text: '查看',

menu: veiwMenu

}, {

text: '帮助',

menu: helpMenu

})

});

在上面的代码中,我们定义了 5 个菜单对象(Ext.menu.Menu),分别是“文件”菜

单 fileMenu、“编辑”菜单 editMenu、“格式”菜单 formatMenu、“查看”菜单 veiwMenu、

“帮助”菜单 helpMenu,然后定义一个工具栏 tb,把 5 个菜单都挂到了这个工具栏上。

在“编辑”菜单的“撤销”菜单项的定义中,我们添加了属性 disabled:true,这样这个菜

单项将呈灰色,为无效状态。在“格式”菜单中的“自动换行”菜单项的定义中,我们

添加了 checked: true 属性,则这个菜单项会成为一个 CheckItem(菜单前面带有

CheckBox)。在“编辑”菜单的“撤销”和“剪切”菜单项之间,我们添加了一个'-'的 item,

这个 item 会在这两个菜单项之间生成一条分隔线。运行 menu.htm,各个菜单如图 8-28

至图 8-32 所示。

图 8-28 “文件”菜单

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

429

8

图 8-29 “编辑”菜单

图 8-30 “格式”菜单

图 8-31 “查看”菜单

图 8-32 “帮助”菜单

在完成了常规的菜单组件例子之后,接下来添加上下文菜单和子菜单,我们还是以

Windows 的记事本菜单为例,来对上面这个菜单例子进行修改,让它具有上下文菜单和子

菜单。修改 menu.js 文件,添加如下内容:

var contextMenu = new Ext.menu.Menu({

id: 'contextMenu',

items: [{

text: '撤销',

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

430

disabled: true

}, '-', {

text: '剪切',

disabled: true

}, {

text: '复制',

disabled: true

}, {

text: '粘贴'

}, {

text: '删除',

disabled: true

}, '-', {

text: '全选',

disabled: true

}, '-', {

text: '从右到左的阅读顺序'

}, {

text: '显示 Unicode控制字符'

}, {

text: '插入 Unicode控制字符',

//子菜单

menu: {

items: [{

text: 'LRM left to right mark'

}, {

text: 'RLM right to left mark'

}]

}

}]

});

……

Ext.EventManager.on(Ext.getBody(), 'contextmenu', function(e){

e.preventDefault();

contextMenu.showAt(e.getXY());

});

在上述代码中,我们创建了一个菜单对象 contextMenu,其中在“插入 Unicode 控制字

符”菜单项的定义中,添加了一个 menu 属性,该属性中包含了一个 items 属性,items 属

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

431

8

性为有两个对象的数组,这段代码的含义是在“插入 Unicode 控制字符”这个菜单项下面

添加两个子菜单。代码的 后 3 行,我们通过 EventManager 将一个匿名函数绑定到文档的

body 对象的 contextmenu 事件上。在该匿名函数中,首先通过 e.preventDefault()来阻止默认

的浏览器事件,然后通过 contextMenu.showAt(e.getXY())把该上下文菜单显示在鼠标当前位

置。

运行 menu.htm,在页面上单击右键,显示如图 8-33 所示。

图 8-33 右键菜单

至此,菜单的显示部分已经完成,接下来添加一些代码,来演示一下如何添加菜单的

事件响应。继续修改 menu.js 文件,添加如下粗体部分:

……

var clickHandler=function(o){

alert('你点击了菜单:'+o.text);

}

Ext.onReady(function(){

……

items: [{

text: '新建',

handler:clickHandler

}, {

……

在上述代码中,我们添加了一个函数 clickHandler,该函数用 alert 来显示用户所点击

的菜单的名字。然后在“文件”菜单的“新建”菜单项中添加了一个 handler 属性,其值为

前面定义的 clickHandler 函数,这样就把 clickHandler 函数绑定到了“新建”菜单项的单击

事件上。运行 menu.htm,单击“新建”菜单,其结果如图 8-34 所示。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

432

图 8-34 菜单绑定事件处理后运行结果

在上述例子中,我们创建了一个和记事本菜单一样的菜单页面,包括右键菜单,并添

加了一个简单的 handler 来处理菜单的点击事件。读者可以参考本例子来创建自己的事件处

理函数,以完成更为丰富和复杂的功能。

8.10 Utility 组件

在本节中,将介绍一些 Utility 组件,它们不是用于 UI 的展示,但是通过使用它们,

我们能更轻松地完成工作。

8.10.1 Ajax 组件

Ext 的组件都是通过异步请求和服务器端通信的,如树组件异步装载节点的数据、表单

的提交等。也可以直接用 Ext 的 Ajax 类来发起 Ajax 请求。下面通过一个简单的小例子来

演示一下 Ajax 组件的使用。

首先,在 pages 目录下创建一个 ajax.htm 文件,除了在其中添加和 HelloWorld 例子一

样的 3 行代码之外,还需要添加一行代码,导入自己的 JavaScript 文件:

<script type='text/Javascript' src='../js/ajax.js'></script>

然后,在 ajax.htm 中,添加一个输入框和一个按钮:

<body>

Name:<input type='text' name='name' id='name'>

<input type='button' value='提交' onclick='test();'>

</body>

在 ajax.js 中,我们实现提交按钮的 onclick 事件的处理方法 test,代码如下:

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

433

8

function test(){

var name = document.getElementById('name').value;

Ext.Ajax.request({

url: '../AjaxServlet',

failure: showError,

success: sayHello,

params: {

name: name

}

});

function sayHello(response){

alert(response.responseText);

}

function showError(response){

alert('failure');

}

}

还需要定义一个叫做 AjaxServlet 的 Servlet 来和客户端交互,其 doPost 中的代码如下:

response.setContentType("text/html");

PrintWriter out = response.getWriter();

String name=request.getParameter("name");

String str="Hello "+name+"!";

out.print(str);

out.flush();

out.close();

运行 ajax.htm,在“Name“框中输入 sam,单击“提交”按钮,运行结果如图 8-35 所示。

图 8-35 Ajax 组件例子运行结果

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

434

8.10.2 Template 和 XTemplate 组件

Template 组件用来创建一个 HTML 的代码模板,在操作 DOM 对象时,使用代码片段

会带来很大的性能提升,所以它一般和 DomHelper 组件结合起来使用。举例如下:

var t = new Ext.Template(

'<div name="{id}">',

'<span class="{cls}">{name:trim} {value:ellipsis(10)}</span>',

'</div>'

);

t.append('my-div', {id: 'myid', cls: 'myclass', name: 'foo', value: 'bar'});

查看 Generated Source,可以看到执行后的源码如下:

<div id="my-div">

<div name="myid">

<span class="myclass">foo bar</span>

</div>

</div>

Template 组件只能简单地将数据填充到模板的占位符中,功能比较弱,有时候需要结

合 JavaScript 来实现一些如数据遍历、条件判断的功能。XTemplate 组件扩展了 Template,

支持上述的功能,下面这段代码使用 XTemplate 组件生成了一个表格。

var t = new Ext.XTemplate(

'<table border=1>',

'<tpl for="record">',

'<tr>',

'<td>{0}</td>',

'<td>{1:trim}</td>',

'<td>{2:trim}</td>',

'<td>{3:ellipsis(10)}</td>',

'</tr>',

'</tpl>',

'</table>'

);

var data ={ record:[

['1','male','name1','descn1'],

['2','female','name2','descn2'],

['3','male','name3','descn3'],

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

435

8

['4','female','name4','descn4'],

['5','male','name5','descn5']

]};

t.append('my-div',data);

运行结果如图 8-36 所示。

图 8-36 XTemplate 组件例子运行结果

8.10.3 DomHelper 组件

DomHelper 组件是用来操作 DOM 对象的,它支持 HTML 片段和 DOM 对象。举例如

下:

有一个 HTML 页面内容如下:

<body>

<div id='my-div'></div>

</body>

执行以下代码:

Ext.DomHelper.append('my-div',{tag:'div',id:'append-div'});

查看 Generated Source,可以看到执行后的源码如下:

<div id="my-div">

<div id="append-div">

</div>

</div>

可以看到,在 id=“my-div”元素内部被添加了一个 id=“append-div”的层。

当然,DomHelper 也支持使用 HTML 片段来进行 DOM 操作,执行以下代码:

Ext.DomHelper.insertHtml('my-div','<div id=\'insertHtml\'></div>');

查看 Generated Source,可以看到执行后的源码如下:

<div id="my-div">

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

436

<div id="appendHtml">

</div>

</div>

当然,DomHelper 支持在当前节点的前面或者后面插入,使用对应的 insertBefore、

insertAfter 方法即可(具体可参考其文档)。还可以使用 overwrite 来替换当前节点的内容。

在添加一个节点时,可同时添加该节点的下级节点,只要给 children 属性赋值即可。

执行如下代码:

var list = dh.append('my-div', {

tag: 'ul', cls: 'my-list', children: [

{tag: 'li', id: 'item0', html: 'List Item 0'},

{tag: 'li', id: 'item1', html: 'List Item 1'},

{tag: 'li', id: 'item2', html: 'List Item 2'},

{tag: 'li', id: 'item3', html: 'List Item 3'},

{tag: 'li', id: 'item4', html: 'List Item 4'}

]

});

查看 Generated Source,可以看到执行后的源码如下:

<div id="my-div">

<ul class="my-list">

<li id="item0">

List Item 0

</li>

<li id="item1">

List Item 1

</li>

<li id="item2">

List Item 2

</li>

<li id="item3">

List Item 3

</li>

<li id="item4">

List Item 4

</li>

</ul>

</div>

本节中介绍了 Ajax 组件、Template 和 XTemplate 组件及 DomHelper 组件的简单使用。

CHAPTER 第 8 章 ExtJS 框架的介绍和使用

437

8

在 Ext 中,还有其他一些 Utility 组件很好用,如 DomQuery 用于节点查询,Format 用于格

式化数据,JSON 用于对象和 JSON 的转换,读者可参考 API 进行使用。

8.11 国际化

在很多系统中,都会有国际化的要求,即根据用户的 locale 来选择显示不同的语言。

Ext 将相关的显示信息进行了抽取,我们只需要导入相应 locale 的 JS 文件,就可以实现国

际化。

这里以之前的表单组件的例子作为国际化演示的例子。在 pages 目录下,拷贝 form.htm,

并重命名为“form_zh_CN.htm”,在代码中导入 ext-lang-zh_CN.js,如下所示:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"

"http://www.w3.org/TR/html4/strict.dtd">

<html>

<head>

<meta http-equiv="Content-Type" content="text/html;

charset=utf-8" />

<title>Layout</title>

<link rel="stylesheet" type="text/css"

href="../lib/ext2.0/resources/css/ext-all.css" />

<script type='text/Javascript'

src='../lib/ext2.0/adapter/ext/ext-base.js'></script>

<script type='text/Javascript'

src='../lib/ext2.0/ext-all-debug.js'></script>

<script type='text/Javascript'

src='../lib/ext2.0/source/locale/ext-lang-zh_CN.js'></script>

<script type='text/Javascript' src='../js/form.js'></script>

</head>

<body>

</body>

</html>

在浏览器中打开 form_zh_CN.htm 文件,点击 Username 输入框,然后点击 Password 框,

将鼠标移动到 Username 框上,显示如图 8-37 所示。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

438

图 8-37 国际化后输入框的验证提示

我们看到这里的提示信息变成了中文,而不是之前在 8.5 节图 8-10 中显示的英文“The

field is required”。

所以在实际的应用中,我们只需要根据用户的 locale 来导入不同的 JS 文件;对于自己

定义的 JS 信息,也应该采用类似的方法,将这些信息提取出来,然后定义基于不同 locale

的 JS 文件,根据用户的 locale,来导入不同的 JS 文件即可。

8.12 开发工具

有很多开发工具可以帮助我们开发 JavaScript,也有很多工具支持 ExtJS。

其中有一些支持代码提示、语法高亮和格式化等,如 Aptana (http://www.aptana.com/)、

Spket (http://spket.com/)等。

还有一些是可视化编辑器,但是都不是很完善,如 extbuilder (http://code.google.com/p

/extbuilder/)。

在 Ext 3.0 发布时,Ext 官方将会同时推出 Ext 的设计器,此外借助 AIR,ExtJS 开发

的程序可以直接成为本地应用,既有 Web 的绚丽,又有本地程序的强大功能,值得我们

期待。

8.13 本章小结

本章首先介绍了 JavaScript 的面向对象的编程技术,让读者重新认识了 JavaScript,

在对 JavaScript 的面向对象编程技术有所了解后,我们对当前非常流行的 JavaScript 的 UI

框架 ExtJS 进行了介绍。通过实例,我们对 ExtJS 的常见组件有了比较深入的了解,并能

应用到实际项目中。 后,我们介绍了开发 JavaScript 及 ExtJS 的一些常用工具,可以简

化开发。

CHAPTER

9

当 B/S 应用采用 JavaScript 的组件库作为 UI 展示,并使用 Ajax 作为通信机制时,客

户端和服务器端的交互仅仅是页面中动态数据的交互,而不是基于页面请求,刷新页面的

模式。涉及数据交互,就不可避免地要谈到客户端提交的数据是如何转换成合适的 Java 对

象,然后进行调用,并将调用的结果转换成合适的格式返回给浏览器的。对于传统的非 Ajax

模式的应用,请求数据的封装由一些标签库或者框架来完成,如 JSP 的 Bean 标签、Struts

的自动组装 ActionForm 等,而数据的返回则是页面中的动态数据被替换,然后整个页面输

出到客户端。Ajax 的请求数据到 Java 对象的转换,在很多情况下,需要人为的转换,转换

方式取决于请求数据的格式,如字符串、JSON 格式、XML 格式等,而调用之后的返回数

据,也需要格式化成相应的格式,以使浏览器端的 UI 组件能够解析它,将页面的展示重新

呈现。比如前面 Ext 例子中的树组件和表格组件,请求数据采用字符串,返回数据采用自

己构造 JSON 格式的数据。随着 Ajax 的盛行,这种 JavaScript→Java→JavaScript 的转换框

架也应运而生,这种框架的产生,将使 JavaScript 能够调用 Java 方法,并处理返回结果。

比较优秀的有 DWR 和 JSON-RPC-Java,下面我们将对这两个框架进行介绍。

9.1 DWR 框架的介绍和使用

9.1.1 DWR 基本概念

DWR(远程调用框架)是一个 Java 开源库,帮助我们实现 Ajax 网站。

通过 DWR 可以使浏览器中的 JavaScript 代码调用 Web 服务器上的 Java,就像 Java 代

码就在浏览器中一样。

第 9 章

DWR 和 JSON-RPC-Java

的介绍及使用

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

440

DWR 主要包括两部分:

在服务器上运行的 Servlet(org.directwebremoting.servlet.DwrServlet),用于处理请

求,并把结果返回浏览器。

运行在浏览器上的 JavaScript,可以发送请求,并动态改变页面。

DWR 会根据 Java 类动态地生成 JavaScript 代码。这些代码的魔力是让用户感觉整个

Ajax 调用都是在浏览器上发生的,但事实上是服务器执行了这些代码,DWR 负责数据的

传递和转换。

这种 Java 和 JavaScript 之间的远程调用会让 DWR 用户感觉像是曾经习惯使用的 RMI

或 SOAP 的 RPC 机制,而且这一过程还不需要额外的浏览器插件。

Java 是同步的,而 Ajax 是异步的。所以当调用一个远程方法时,需要给 DWR 一个回

调函数,当数据从网络上回来时,DWR 会调用这个函数。

图 9-1 表现了 DWR 是如何在 onclick 事件中改变下拉列表的内容的。

图 9-1 DWR 功能示意图

DWR 为服务器端 AjaxService 类(Java)动态地生成了一个相应的客户端 AjaxService

类(JavaScript)。这个类被 eventHandler 调用,DWR 就会去处理整个远程调用的细节,包

括在 JavaScript 和 Java 之间转换参数和返回值。然后它会执行所提供的回调函数

(populateList),这个函数再利用 DWR 提供的工具函数来更改页面内容。

DWR 帮助制作出具有很好交互性的网站,它提供的一些 JavaScript 库帮助处理

DHTML,也提供了一些例子作为参考。

9.1.2 使用 DWR

9.1.2.1 安装 DWR 的 Jar 包

从 https://dwr.dev.java.net/files/documents/2427/87007/dwr.jar 下载 dwr.jar,把它放到

webapp 的 WEB-INF/lib 目录下。

9.1.2.2 编辑 web.xml 文件

需要把下面的代码加到 WEB-INF/web.xml 文件中。<servlet>部分需要和其他的

<servlet>在一起,<servlet-mapping>部分也一样。

CHAPTER 第 9 章 DWR 和 JSON-RPC-Java 的介绍及使用

441

9

<servlet>

<servlet-name>dwr-invoker</servlet-name>

<display-name>DWR Servlet</display-name>

<servlet-class>

org.directwebremoting.servlet.DwrServlet</servlet-class>

<init-param>

<param-name>debug</param-name>

<param-value>true</param-value>

</init-param>

</servlet>

<servlet-mapping>

<servlet-name>dwr-invoker</servlet-name>

<url-pattern>/dwr/*</url-pattern>

</servlet-mapping>

在 WEB-INF 目录下的 web.xml 旁边创建一个 dwr.xml 文件。可以从 简单的配置开始:

<!DOCTYPE dwr PUBLIC

"-//GetAhead Limited//DTD Direct Web Remoting 2.0//EN"

"chttp://getahead.org/dwr/dwr20.dtd">

<dwr>

<allow>

<create creator="new" Javascript="JDate">

<param name="class" value="Java.util.Date"/>

</create>

</allow>

</dwr>

DWR 配置文件定义了哪些 DWR 会创建提供远程调用的 JavaScript 类。在上面的例子

中我们定义了一个类来提供远程调用,并为其提供 JavaScript 类的名字。

在上面我们使用了 new 创建器,它会调用没有参数的构造函数来创建实例,但是所有

JavaBean 必须拥有这个构造函数。还要注意 DWR 有一些限制如下:

不要出现 JavaScript 保留关键字,和保留关键字同名的函数指定被排除。多数

JavaScript 的关键字和 Java 是相同的,所以不可能有一个方法叫做“try()”。但是

“delete()”对于 JavaScript 有着特殊意义,而对于 Java 则不没有。

JavaScript 方法重载是不支持的,所以尽量不要在 Java 中使用。

访问 http://localhost:8080/dwr/dwr/(dwr 为 Web 应用的 context root),可以看到如下页面:

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

442

这就是我们在 dwr.xml 中配置的叫做 JDate 的 JavaScript 对象,单击该链接,进入 JDate

对象的详细页面,如图 9-2 所示。

图 9-2 DWR 的测试页面

CHAPTER 第 9 章 DWR 和 JSON-RPC-Java 的介绍及使用

443

9

该页面说明:如果要使用 JDate 的 JavaScript 对象,则需要导入如下 JS 文件。

<script type='text/javascript' src='/dwr/dwr/interface/JDate.js'></script>

<script type='text/javascript' src='/dwr/dwr/engine.js'></script>

还有一个可选的 utility 的 JS 文件可以导入:

<script type='text/javascript' src='/dwr/dwr/util.js'></script>

接下来的是 Date 对象的可调用方法,在输入框中输入合适的参数,单击“Execute”按

钮就可以执行,执行后返回的结果显示在“Execute”按钮的后面;如果执行出错,则会显

示相应的错误信息框。

我们看到,有些方法下面还有红色的 Warning 提示信息,DWR 对这些提示信息都进行了

解释:

(1)Warning: overloaded methods are not recommended.

表示重载方法不推荐使用。原因是 JavaScript 不支持重载,所以重载方法将会被转换

成两个名字一样的 JavaScript 方法,并且第二个会覆盖第一个,而这个方法有可能不是你

想要的那个方法。具体来说,就是转换成 JavaScript 后,有可能你得到的 JavaScript 方法,

是对其父类方法的调用,而不是对象本身重载方法。

(2)Warning: No Converter for Java.lang.Object.

表示没有给 Object 指定转换器。原因是 DWR 出于安全的考虑,对于 Object 对象的转

换,默认是关闭的。

(3)Warning: getClass() is excluded: Methods defined in Java.lang.Object are not

accessible.

表示对 getClass()不进行 Java 到 JavaScript 的转换。原因是对于 Java.lang.Object 中的方

法,DWR 是不进行转换的,当然也可以使用<include> 或者 <exclude>标签来指定我们自

己定义的哪些方法可以转换,哪些方法不可以转换。

9.1.2.3 使用导出的 JavaScript 对象

在了解了以上信息后,我们来使用这个 JDate 对象。

创建一个简单的 JSP 页面,来显示当前时间。代码如下:

<%@ page language="Java" import="Java.util.*" pageEncoding="utf-8"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

<html>

<head>

<title>My JSP 'TestJDate.jsp' starting page</title>

<meta http-equiv="pragma" content="no-cache">

<meta http-equiv="cache-control" content="no-cache">

<meta http-equiv="expires" content="0">

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

444

<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">

<meta http-equiv="description" content="This is my page">

<!--

<link rel="stylesheet" type="text/css" href="styles.css">

-->

<script type='text/Javascript' src='/dwr/dwr/interface/JDate.js'>

</script>①

<script type='text/Javascript' src='/dwr/dwr/engine.js'></script>②

<script type="text/Javascript">③

function showTime(){

JDate.toString(function(value){document.getElementById('now').

innerHTML=value;});

}

</script>

</head>

<body onload='showTime();'>④

The time is: <span id='now' style="color:red"></span>

</body>

</html>

我们按照之前页面上的提示信息,在①、②处导入了相应的 JavaScript 文件,然

后在③处定义了一个 showTime 函数,在这个函数中,调用了 JDate 的 toString 函数,

并以一个匿名函数作为其参数。在 DWR 中,Java 方法被转换成 JavaScript 方法后,

除了含有 Java 方法中的参数列表之外,在参数列表的 后面会添加一个 callback 参

数,该参数用于传递一个回调的 JavaScript 方法,当 Java 中的方法被调用完成后,

则会执行这个回调方法。在本例中,当 JDate 的 toString 方法执行完成后,DWR 会

自动调用上面代码中传给它的那个匿名函数,该函数将 JDate 的 toString 方法的返回

值写到 id 为“now”的元素中。在④处,在这个页面 onload 事件中调用上面定义的

方法。

运行结果如图 9-3 所示。

图 9-3 JDate 调用结果

CHAPTER 第 9 章 DWR 和 JSON-RPC-Java 的介绍及使用

445

9

9.1.3 使用自定义对象

在上一节中,我们对 DWR 的使用有了一些简单的了解,接下来将进行更深一步的学习,

来了解如何创建自定义的 Java 对象,如何控制相应的方法导出,并且构建基于 DWR 的应用。

本节将综合 DWR 和 Ext 进行举例介绍。在这个例子中,将展示对人员信息的操作。

首先创建一个 Person 对象,代码如下:

package org.bluelight.ch09;

public class Person {

private String name;

private int age;

private String gender;

private int id;

public int getId() {

return id;

}

public void setId(int id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public int getAge() {

return age;

}

public void setAge(int age) {

this.age = age;

}

public String getGender() {

return gender;

}

public void setGender(String gender) {

this.gender = gender;

}

}

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

446

Person 对象包含 id(唯一标识)、name、age 和 gender属性。接下来创建一个 PersonService

对象,用来对 Person 对象进行添加、修改、删除,以及查询所有 Person 对象的操作。代码

如下:

package org.bluelight.ch09;

import Java.util.ArrayList;

import Java.util.List;

public class PersonService {

// 静态对象,用于存储 Person对象

private static List<Person> personList = new ArrayList<Person>();

// 大的 id编号

private static int maxId=0;

// 添加一个 Person

public void addPerson(Person p) {

p.setId(getMaxId());

personList.add(p);

}

// 获取所有 Person

public List<Person> getAllPerson() {

return personList;

}

// 更新一个 Person

public void updatePerson(Person p) {

for (int i = 0; i < personList.size(); i++) {

if (p.getId() == personList.get(i).getId()) {

personList.remove(i);

personList.add(i, p);

}

}

}

// 删除一个 Person

public void deletePerson(Person p) {

for (int i = 0; i < personList.size(); i++) {

if (p.getId() == personList.get(i).getId()) {

personList.remove(i);

CHAPTER 第 9 章 DWR 和 JSON-RPC-Java 的介绍及使用

447

9

}

}

}

// 根据 id获取一个 Person信息

public Person getPerson(int id) {

for (int i = 0; i < personList.size(); i++) {

if (id == personList.get(i).getId()) {

return personList.get(i);

}

}

return null;

}

// 使用静态方法初始化 personList

static {

for (int i = 0; i < 5; i++) {

Person p = new Person();

p.setId(i);

p.setName("person" + i);

p.setAge(10 + i);

p.setGender("male");

personList.add(p);

}

maxId=5;

}

public synchronized int getMaxId(){

return maxId++;

}

}

对于 dwr.xml 修改结果如下(粗体为新增加部分):

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE dwr PUBLIC "-//GetAhead Limited//DTD Direct Web Remoting 2.0//EN"

"http://getahead.org/dwr/dwr20.dtd">

<dwr>

<allow>

<create creator="new" Javascript="JDate">

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

448

<param name="class" value="Java.util.Date"/>

</create>

<create creator="new" Javascript="PersonService">

<param name="class" value="org.bluelight.ch09.PersonService"/>

</create>

<convert converter="bean" match="org.bluelight.ch09.Person"/>

</allow>

</dwr>

访问<script type='text/javascript' src='/dwr/dwr/util.js'></script>,可以看到方法列表中有

了以下的方法:

单击 getAllPerson 处的“Execute”按钮,弹出如图 9-4 所示的提示框。

图 9-4 方法调用测试结果

至此,DWR 的工作基本完成了。接下来编写客户端的代码,我们用 Ext 的 Grid 组件

来展示 Person 列表。在 Web 的根目录下创建一个 pages 目录,在该目录下创建一个

PersonService.jsp 文件,内容如下:

<%@ page language="Java" import="Java.util.*" pageEncoding="utf-8"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

<html>

<head>

CHAPTER 第 9 章 DWR 和 JSON-RPC-Java 的介绍及使用

449

9

<title></title>

<meta http-equiv="pragma" content="no-cache">

<meta http-equiv="cache-control" content="no-cache">

<meta http-equiv="expires" content="0">

<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">

<meta http-equiv="description" content="This is my page">

<link rel="stylesheet" type="text/css"

href="../lib/ext2.0/resources/css/ext-all.css" />

<script type='text/Javascript'

src='../lib/ext2.0/adapter/ext/ext-base.js'></script>

<script type='text/Javascript'

src='../lib/ext2.0/ext-all-debug.js'>

</script>

<script type='text/Javascript' src='../js/person.js'></script>

<script type='text/Javascript'

src='../dwr/interface/PersonService.js'> </script>

<script type='text/Javascript' src='../dwr/engine.js'> </script>

<script type='text/Javascript' src='../dwr/util.js'> </script>

</head>

</html>

接着在 Web 根目录下创建一个 js 目录,在该目录下创建一个 person.js 文件,内容如下:

var genderRenderer = function(value){

if (value == 'male') {

return '<img src=\'../lib/ext2.0/examples/shared/icons/fam

/user.gif\'>';

}

else {

return '<img src=\'../lib/ext2.0/examples/shared/icons/fam/

user_female.gif\'>';

}

}

function getAllPerson(){ ①

PersonService.getAllPerson(function(value){

showTable(value);

});

}

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

450

function showTable(myData){ ②

//定义表格数据格式

var store = new Ext.data.JsonStore({

data :myData,

fields: [{

name: 'id'

}, {

name: 'name'

}, {

name: 'age'

}, {

name: 'gender'

}]

});

//创建表格

var grid = new Ext.grid.GridPanel({

store: store,

//定义列格式

columns: [{

header: "编号",

dataIndex: 'id',

sortable: true

}, {

header: "姓名",

dataIndex: 'name'

}, {

header: "年龄",

dataIndex: 'age'

}, {

header: "性别",

dataIndex: 'gender',

renderer: genderRenderer

}],

title: 'Person Grid'

});

CHAPTER 第 9 章 DWR 和 JSON-RPC-Java 的介绍及使用

451

9

grid.render(document.body);

}

Ext.onReady(getAllPerson); ③

在①处,创建了一个 getAllPerson 方法,在该方法中,调用了 PersonService 的

PersonService.getAllPerson 方法,并在回调方法中将 PersonService.getAllPerson 的返

回值传给 showTable 方法。

在②处,定义了 showTable 方法,在该方法中创建了一个 Ext 的表格组件,并读取

传入的参数,用于展示。

在③处,在 onReady 事件时触发①处定义的 getAllPerson 方法。访问

http://localhost:8080/dwr/pages/PersonService.jsp,结果如图 9-5 所示。

图 9-5 PersonService 的调用结果

接下来,要添加对 Person 对象的添加和修改功能,为了完成这些功能,我们对 person.js

作了修改,其修改后的内容如下所示(粗体部分为修改部分):

var genderRenderer = function(value){

if (value == 'male') {

return '<img src=\'../lib/ext2.0/examples/shared/icons/fam/

user.gif\'>';

}

else {

return '<img src=\'../lib/ext2.0/examples/shared/icons/fam/

user_female.gif\'>';

}

}

function getAllPerson(){

PersonService.getAllPerson(function(value){

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

452

showTable(value);

});

}

function updatePerson(id){ ⑤

PersonService.getPerson(id, function(value){

showForm(value);

})

}

function showForm(person){ ②

person = person ||

{};

var win = new Ext.Window({

layout: 'fit',

width: 300,

height: 200,

plain: true,

title: 'Person Infomation',

items: new Ext.form.FormPanel({

id: 'personForm',

labelWidth: 55,

defaultType: 'textfield',

bodyStyle: 'padding:15px 15px 0',

items: [{

fieldLabel: 'id',

name: 'id',

value: person.id==null?'':person.id,

inputType: 'hidden'

}, {

fieldLabel: 'name',

name: 'name',

value: person.name || '',

anchor: '100%'

}, {

fieldLabel: 'age',

name: 'age',

value: person.age || 0,

CHAPTER 第 9 章 DWR 和 JSON-RPC-Java 的介绍及使用

453

9

anchor: '100%'

}, {

name: 'gender',

value: person.gender || '',

fieldLabel: 'gender',

anchor: '100%'

}]

}),

buttons: [{

text: 'Save',

handler: function(){

var person = Ext.getCmp('personForm').getForm().getValues();

if (person.id) {

PersonService.updatePerson(person, function(){(a)

win.destroy();

getAllPerson();

});

}

else {

PersonService.addPerson(person, function(){ (b)

win.destroy();

getAllPerson();

});

}

}

}, {

text: 'Close',

handler: function(){

win.destroy();

}

}]

});

win.show();

}

var grid; ③

function showTable(myData){

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

454

if (grid) {

grid.destroy();

}

//定义表格数据格式

var store = new Ext.data.JsonStore({

data: myData,

fields: [{

name: 'id'

}, {

name: 'name'

}, {

name: 'age'

}, {

name: 'gender'

}]

});

//创建表格

grid = new Ext.grid.GridPanel({

store: store,

//定义列格式

columns: [{

header: "编号",

dataIndex: 'id',

sortable: true

}, {

header: "姓名",

dataIndex: 'name'

}, {

header: "年龄",

dataIndex: 'age'

}, {

header: "性别",

dataIndex: 'gender',

renderer: genderRenderer

}],

title: 'Person Grid', ①

CHAPTER 第 9 章 DWR 和 JSON-RPC-Java 的介绍及使用

455

9

tbar: [{

text: 'Add',

handler: function(){

showForm();

}

}]

});

grid.on('rowdblclick', function(g, idx, e){ ④

var id = g.getStore().getAt(idx).data.id;

updatePerson(id);

})

grid.render(document.body);

}

Ext.onReady(getAllPerson);

首先在代码①处添加了一个工具栏,该工具栏位于表格组件的表头之上,工具栏上有

一个 Add 按钮,该按钮调用代码②处的 showForm 方法。

在代码②处创建了一个对话框,在对话框中创建了一个 Form,用于对 Person 对象的

编辑,该方法同时用于 Person 对象的添加和修改。当传入参数 person 不为空时,则该表

单用于编辑 Person 对象;否则用于添加一个新的 Person 对象。该表单中有一个 Save 按钮,

该按钮通过对传入的 Person 对象的判断,调用 PersonService 的不同方法。如果 Person 对

象的 id 属性存在,则调用 PersonService 的 updatePerson 方法(代码(a)处),调用返回

后,销毁对话框,重新展示下面的表格;如果 Person 对象的 id 属性不存在则调用

PersonService 的 addPerson 方法(代码(b)处),调用返回后,销毁对话框,重新展示下

面的表格。

在代码③处,我们将 grid 对象的声明挪到了函数之外,并在 grid 创建之前先判断其是

否已创建,如果已创建,则销毁它。这样做的主要原因是,当修改或者添加 Person 对象之

后,需要重新刷新列表,我们用返回的参数重新构建了 store 对象和 grid 对象,如果不销毁

上次创建的 grid 对象,则会在页面上显示多个列表。

在代码④处,我们给 grid 组件添加了一个 rowdblclick 事件处理函数,当对表格中的一

行进行双击时,该函数会获取该行所对应的 Person 对象的 id,然后将 id 传递给代码⑤处的

updatePerson 方法。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

456

在代码⑤处,我们根据 id,调用 PersonService 的 getPerson 方法,获取 id 对应的 Person

信息,然后弹出编辑对话框,并将 Person 的信息显示在对话框的相应位置。运行结果如图

9-6 所示。

图 9-6 添加了 Add 按钮后的运行结果

单击“Add”按钮,弹出如图 9-7 所示的“Person Infomation”对话框。

图 9-7 单击 Add 按钮后的运行结果

在对话框的表单中输入 name 为“sam”,age 为“26”,gender 为“male”,单击“Save”

按钮,结果如图 9-8 所示。我们可以在列表中看到刚才添加的那条记录。

图 9-8 添加了 Person 后的列表

CHAPTER 第 9 章 DWR 和 JSON-RPC-Java 的介绍及使用

457

9

双击列表中的第 1 条记录,显示如图 9-9 所示。我们看到 person0 成功地把 age 改成了

18,性别变成了 female。

图 9-9 编辑 Person 的运行结果

在这里我们可以对 person0 进行信息编辑,比如把 age 改成 18,gender 改成 female,

单击“Save”按钮,结果如图 9-10 所示。至此,完成了对 Person 对象的列表、添加、修改

操作。回顾一下 PersonService,我们定义了一个方法:deletePerson。我们不打算在目前这

个界面上暴露这个方法,不希望被用户访问到。但是在目前的配置下,该方法还是可以访

问到的,通过访问 http://localhost:8080/dwr/dwr/test/PersonService,就可以看到 deletePerson

方 法 , 在 该 方 法 的 输 入 框 中 输 入 {id:0} , 然 后 单 击 “ Execute ” 按 钮 , 刷 新

http://localhost:8080/dwr/pages/PersonService.jsp 页面,就可以看到 id 为 0 的记录已经被删除

了,如图 9-11 所示。

图 9-10 编辑 Person 后的列表

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

458

图 9-11 删除 Person 后的列表

当然,我们在发布应用时,会关闭 debug 模式,http://localhost:8080/dwr/dwr/test/

PersonService 这个路径将无法访问。但是,这不能从根本上解决问题,只要这个方法暴露

出来,用户只需要构造相应的参数,还是可以调用这个方法的。所以需要配置 dwr.xml 文

件,来屏蔽这个方法。修改 dwr.xml 配置如下(粗体部分为新增的):

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE dwr PUBLIC "-//GetAhead Limited//DTD Direct Web Remoting 2.0//EN"

"http://getahead.org/dwr/dwr20.dtd">

<dwr>

<allow>

<create creator="new" Javascript="JDate">

<param name="class" value="Java.util.Date"/>

</create>

<create creator="new" Javascript="PersonService">

<param name="class" value="org.bluelight.ch09.PersonService"/>

<exclude method="deletePerson"/>

</create>

<convert converter="bean" match="org.bluelight.ch09.Person"/>

</allow>

</dwr>

我们再次访问 http://localhost:8080/dwr/dwr/test/PersonService,可以看到在 deletePerson

方法下面有一行警告信息:

(Warning: deletePerson() is excluded: Method access is denied by rules

in dwr.xml. See below)

我们输入{id:0},单击“Execute”按钮,出现如图 9-12 所示的信息。

CHAPTER 第 9 章 DWR 和 JSON-RPC-Java 的介绍及使用

459

9

图 9-12 调用 deletePerson 测试的显示结果

我们刷新 http://localhost:8080/dwr/pages/PersonService.jsp 页面,发现编号为 0 的记录依

然在,没有被删除。

9.1.4 DWR 的配置

9.1.4.1 dwr.xml 的配置

dwr.xml 是 DWR 的配置文件。在默认情况下,应该把它放到 WEB-INF 目录(web.xml

的目录)下。

dwr.xml 文件的结构如下:

<!DOCTYPE dwr PUBLIC "-//GetAhead Limited//DTD Direct Web Remoting 2.0//EN"

"http://getahead.org/dwr/dwr20.dtd">

<dwr>

<!-- init is only needed if you are extending DWR -->

<init>

<creator id="..." class="..."/>

<converter id="..." class="..."/>

</init>

<!-- without allow, DWR isn't allowed to do anything -->

<allow>

<create creator="..." Javascript="..."/>

<convert converter="..." match="..."/>

</allow>

<!-- you may need to tell DWR about method signatures -->

<signatures>

...

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

460

</signatures>

</dwr>

术语

这里有一些必须理解的术语——参数会被 converted,远程 Bean 会被 created。所以如

果你有一个叫做 A 的 Bean,它有一个方法叫做 A.blah(B),那么你需要有一个 A 的 creator

和一个 B 的 converter。

<allow>

allow 段落里面定义的是 DWR 可以创建和转换的类。

Creators

我们要调用的每个类都需要一个<create ...>定义。有多种 creator,比较常用的是 new

和 spring。更多的信息可以参见[Creators]文档。

Converters

我们必须保证所有的参数都可以被转换。JDK 中的多数类型已经有转换器了,但是我

们需要给 DWR 转换代码的权利。一般来说,JavaBean 的参数需要一个<convert ...>定义。

在默认情况下,如下类型不需要定义就可以转换:

所有的原生类型,如 boolean、int、double 等;

原生类型的对象类型如 Boolean、Integer 等;

Java.lang.String;

Java.util.Date 和 SQL 中的 Date;

以上类型组成的数组;

以上类型的集合类型(Lists、Sets、Maps、Iterators、等);

DOM、XOM、JDOM 和 DOM4J 中的 DOM 对象(类似 Element 和 Document)。

要了解如何转换 JavaBean 或者其他类型的参数,请查看 Converters 文档。

<init>

可选的 init 部分用来声明创造 Bean 的类和转换 Bean 的类。在多数情况下不需要使用

它们;但如果需要定义一个新的 Creator 和 Converter,那么就需要在这里定义它们。但是

建议你先检查一下 DWR 是否支持。

在 init 部分有了定义,只是告诉 DWR 这些扩展类的存在,给出了如何使用的信息,

这时它们还没有被使用。这种方式很像 Java 中的 import 语句,多数类需要在使用前先 import

一下,但是只有 import 语句并不表明这个类已经被使用了。每一个 Creator 和 Converter 都

有 id 属性,以便后面使用。

<signatures>

DWR 使用反射来找出在转换时应该用哪种类型。有时类型信息并不明确,这时可以在

这里写下方法的签名来明确类型。Signatures 会在 9.1.4.4 节加以描述。

CHAPTER 第 9 章 DWR 和 JSON-RPC-Java 的介绍及使用

461

9

9.1.4.2 Creators(创造器)

dwr.xml 文件中 create 元素的结构如下:

<allow>

<create creator="..." Javascript="..." scope="...">

<param name="..." value="..."/>

<auth method="..." role="..."/>

<exclude method="..."/>

<include method="..."/>

</create>

...

</allow>

这里的多数元素都是可选的,我们真正必须知道的是指定一个 Creator 和一个 JavaScript

名字。

(1)creator 属性

creator 属性是必需的,用来指定使用哪种创造器。

在默认情况下,DWR 有 8 种创造器。它们是:

new:用 Java 的 new 关键字创造对象。

none:它不创建对象,看下面的原因。

scripted:通过 BSF 使用脚本语言创建对象,例如 BeanShell 或 Groovy。

spring:通过 Spring 框架访问 Bean。

jsf:使用 JSF 的 Bean。

struts:使用 Struts 的 FormBean。

pageflow:访问 Beehive 或 Weblogic 的 PageFlow。

ejb3:访问 EJB3 的 Session Bean。

如果需要写自己的创造器,则必须在 init 部分注册它。

(2)JavaScript 属性

该属性用于指定浏览器中这个被创造出来的对象的名字。不能使用 JavaScript 的关

键字。

(3)scope 属性

该属性非常类似于 Servlet 规范中的 scope。它允许我们指定这个 Bean 的生命范围,选

项有“application”、“session”、“request”和“page”,这些值对于 Servlet 和 JSP 开发者来

说应该相当熟悉了。

scope 属性是可选的。默认值是“page”。如果要使用“session”则需要 Cookies。当前

的 DWR 不支持 ULR 重写。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

462

(4)param 元素

此元素被用来指定创造器的其他参数,每种创造器各有不同。例如,“new”创造器需

要知道要创建的对象类型是什么。每种创造器的参数可以在各自的文档中找到,请参考

DWR 手册。

(5)include 和 exclude 元素

允许创造器来限制类中方法的访问。一种创造器必须指定 include 列表或 exclude 列表。

如果是 include 列表,则暗示默认的访问策略是“拒绝”;如果是 exclude 列表,则暗示默认

的访问策略是“允许”。

例如:要拒绝除了 setWibble()以外的所有方法,那么应该把如下内容添加到 dwr.xml

中。

<create creator="new" Javascript="Fred">

<param name="class" value="com.example.Fred"/>

<include method="setWibble"/>

</create>

对于加入到 create 元素中类的所有方法,都是默认可见的。

(6)auth 元素

此元素允许我们指定一个 J2EE 的角色用于将来的访问控制检查。

<create creator="new" Javascript="Fred">

<param name="class" value="com.example.Fred"/>

<auth method="setWibble" role="admin"/>

</create>

none 创造器

none 创造器不创建任何对象,它会假设我们不需要创建对象。这有可能是对的,有两种

情况。

一种情况是:可能使用的 scope 不是“page”,并在前面已经把这个对象创建到这个

scope 中了,这时就不需要再创建对象了。

另一种情况是:要调用的方法是静态的,这时也不需要创建对象。DWR 会在调用创

建器之前先检查一下这个方法是不是静态的。

对于上述两种情况,仍然需要 class 参数,用来告诉 DWR 它在操作的对象类型是什么。

使用静态方法

DWR 会在调用创建器之前先检查一下这个方法是不是静态的,如果是静态的,那么创

造器不会被调用。

适用单例类

对于单例类的创建, 好使用 BeanShell 和 BSF 来实例化对象。

CHAPTER 第 9 章 DWR 和 JSON-RPC-Java 的介绍及使用

463

9

9.1.4.3 Converters(转换器)

转换器在客户端和服务器之间转换数据。

(1)基础的转换器

原生类型,像 String,BigDecimal 这样简单对象的转换器已经有了,不需要在 dwr.xml

中<allow>部分的<convert>中定义。

默认支持的类型包括:boolean、byte、short、int、long、float、double、char、Java.lang.Boolean、

Java.lang.Byte、Java.lang.Short、Java.lang.Integer、Java.lang.Long、Java.lang.Float、Java.lang.Double、

Java.lang.Character、Java.math.BigInteger、Java.math.BigDecimal 和 Java.lang.String。

(2)Date 转换器

Date 转换器负责在 JavaScript 的 Date 类型与 Java 的 Date 类型(Java.util.Date、

Java.sql.Date、Java.sql.Times 或 Java.sql.Timestamp)之间进行转换。同基础的转换器一样,

Date 转换器默认是支持的,不需要在 dwr.xml 中<allow>部分的<convert>中定义。

如果有一个 JavaScript 的字符串(例如"01 Jan 2010"),想把它转换成 Java 的 Date 类型

有两种方法:在 JavaScript 中用 Date.parse()把它解析成 Date 类型,然后用 DWR 的

DateConverter 传递给服务器;或者把它作为字符串传递给 Server,再用 Java 中的

SimpleDateFormat(或者类似的)来解析。

同样,如果有 Java 的 Date 类型并且希望在 HTML 中使用它则可以先用

SimpleDateFormat 把它转换成字符串再使用;也可以直接将 Date 传递给 JavaScript,然后

用 JavaScript 格式化。第一种方法简单一些,但浪费了转换器,而且这样做也会使浏览器

上的显示逻辑受到限制。其实第二种方法更好,也有一些工具可以提供帮助。

(3)其他对象

创建自己的转换器也很简单。Converter 接口的 JavaDoc 包含了相关的信息,其实这种

需求很少出现。在创建自己的 Converter 之前先看看 BeanConverter,它可能已经能满足需

求,或者通过对其进行简单的扩展,就可以满足需求。

9.1.4.4 Signatures(签名)

signatures 段使 DWR 能确定集合中存放的数据类型。例如,从下面的定义中我们无法

知道 List 中存放的数据类型是什么。

public class Check

{

public void setLotteryResults(List nos)

{

...

}

}

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

464

signatures 段允许我们暗示 DWR 应该用什么类型去处理。应用格式对已经了解 JDK 5

的泛型的用户来说很容易理解。

<signatures>

<![CDATA[

import Java.util.List;

import com.example.Check;

Check.setLotteryResults(List<Integer> nos);

]]>

</signatures>

DWR 中有一个解析器专门来做这件事,所以即便环境是 JDK 1.3,DWR 也能正常工

作。

解析规则基本上和我们所预想的规则一样(有两个例外),所有 Java.lang 下面的类型

会被默认 import。

第一个是 DWR 1.0 中解析器的 bug,某些环境下不能返回正确类型。所以我们不

用管它。

第二个是这个解析器为“阳光(sunny day)”解析器,就是说它非常宽松,不像编

译器那样严格地保证一定要正确。所以有时它也会允许我们丢失 import。

<signatures>

<![CDATA[

import Java.util.List;

Check.setLotteryResults(List<Integer>);

]]>

</signatures>

signatures 段只是用来确定泛型参数中的类型参数。DWR 会自己使用反射机制或者运

行时类型确定类型,或者假设它是一个 String 类型。所以:

不需要 signatures——没有泛型参数

public void method(String p);

public void method(String[] p);

需要 signatures——DWR 不能通过反射确定

public void method(List<Date> p);

public void method(Map<String, WibbleBean> p);

不需要 signatures——DWR 能正确地猜出

public void method(List<String> p);

public void method(Map<String, String> p);

CHAPTER 第 9 章 DWR 和 JSON-RPC-Java 的介绍及使用

465

9

不需要 signatures——DWR 可以通过运行时类型确定

public List<Date> method(String p);

没有必要让 JavaScript 中所有对象的 key 都是 String 类型,也可以使用其他类型作为

key,但是它们在使用之前会被转换成 String 类型。DWR 1.x 用 JavaScript 的特性把 key 转

换成 String;DWR 2.0 可能会用 toString()方法,在服务器端进行转换。

9.2 JSON-RPC-Java 框架的介绍和使用

9.2.1 基本概念

JSON-RPC-Java 是一个用 Java 来实现动态 JSON-RPC 的框架,利用内置的一个轻量级

JSON-RPC JavaScript 客户端,可以让我们透明地在 JavaScript 中调用 Java 代码。

JSON-RPC-Java 可运行在 Servlet 容器中,如 Tomcat,也可以运行在 JBoss 与其他 J2EE 应

用服务器中。因此可以在一个基于 JavaScript 与 DHTML 的 Web 应用程序中,利用它来直

接调用普通的 Java 方法和 EJB 方法。与 DWR 类似,都是基于 Ajax 的远程调用框架,区

别在于交互的数据格式:DWR 是 JavaScript 代码,而 JSON-RPC-Java 采用的是 JSON 格式。

相比之下,DWR 更为盛行。

9.2.2 安装和配置

(1)我们可以从 http://oss.metaparadigm.com/jsonrpc-dist/json-rpc-java-1.0.1.zip 下载到

新的 JSON-RPC-Java 库。由于 JSON-RPC-Java 项目将和 jabsorb 项目合并,所以在之后

的版本中,包的命名空间将由 com.metaparadigm.jsonrpc 改成 org.jabsorb。为了和以后版本

保持兼容,我们可以通过 http://jabsorb.googlecode.com/files/jabsorb-1.2-minimal.zip 下载合并

到 jabsorb 项目中的库(本书中采用后者,后者采用 slf4j 作为日志工具,所以还需要下载

slf4j 库,下载地址为 http://www.slf4j.org/dist/slf4j-1.5.0.zip)。

(2)创建一个 Web 项目,解压 jabsorb-1.2-minimal.zip 文件,把 jabsorb-1.2.jar 拷贝到

WEB-INF/lib 目录下。解压 slf4j-1.5.0.zip 文件,把 slf4j-api-1.5.0.jar、slf4j-simple-1.5.0.jar

拷贝到 WEB-INF/lib 目录下。

(3)类似于 DWR,我们需要在 web.xml 中注册 JSON-RPC-Java 的 Servlet 类。

<?xml version="1.0" encoding="UTF-8"?>

<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://java.sun.com/xml/ns/javaee

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

466

http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

<servlet>

<servlet-name>org.jabsorb.JSONRPCServlet</servlet-name>

<servlet-class>org.jabsorb.JSONRPCServlet</servlet-class>

</servlet>

<servlet-mapping>

<servlet-name>org.jabsorb.JSONRPCServlet</servlet-name>

<url-pattern>/JSON-RPC</url-pattern>

</servlet-mapping>

</web-app>

(4)我们必须注册一个叫做 JSONRPCBridge 的对象到 HttpSession 中,该对象持有那

些暴露出来的被调用对象的引用,并对请求进行解码,分发到这些被调用对象中。

在 JSP 中,可以采用以下代码来注册 JSONRPCBridge:

<jsp:useBean id="JSONRPCBridge" scope="session"

class="com.metaparadigm.jsonrpc.JSONRPCBridge" />

在 Servlet 中,可以采用以下代码来注册 JSONRPCBridge:

……

HttpSession session = request.getSession();

JSONRPCBridge json_bridge = null;

json_bridge = (JSONRPCBridge) session.getAttribute("JSONRPCBridge");

if(json_bridge == null) {

json_bridge = new JSONRPCBridge();

session.setAttribute("JSONRPCBridge", json_bridge);

}

……

(5)我们需要创建自己的服务类。这里创建一个简单的 HelloWorld 类,代码如下:

package book.example.jsonrpc;

public class HelloWorld {

public String say(){

return "Hello World";

}

}

(6)我们创建一个 helloWorld.jsp 文件,在这个文件中,调用 HelloWorld 的 say 方法。

helloWorld.jsp 文件的代码如下:

CHAPTER 第 9 章 DWR 和 JSON-RPC-Java 的介绍及使用

467

9

<%@ page language="Java" import="Java.util.*" pageEncoding="utf-8"%>

<jsp:useBean id="JSONRPCBridge" scope="session"

class="org.jabsorb.JSONRPCBridge" />

<jsp:useBean id="helloWorld" scope="request"

class="book.example.jsonrpc.HelloWorld" />

<%

JSONRPCBridge.registerObject("helloWorld", helloWorld); %> ①

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

<html>

<head>

<title>Hello World</title>

<meta http-equiv="pragma" content="no-cache">

<meta http-equiv="cache-control" content="no-cache">

<meta http-equiv="expires" content="0">

<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">

<meta http-equiv="description" content="This is my page">

<!--

<link rel="stylesheet" type="text/css" href="styles.css">

-->

<script type="text/Javascript" src="../js/jsonrpc.js" ></script>②

<script type="text/Javascript">

function sayHello(){ ③

jsonrpc = new JSONRpcClient("../JSON-RPC");

jsonrpc.helloWorld.say(function(ret){

document.getElementById('msg').innerHTML=ret});

//alert(result);

}

</script>

</head>

<body>

<button onclick='sayHello()'>sayHello </button>

<span id='msg' style="color:red"></span>

</body>

</html>

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

468

在①处,把 HelloWorld 对象注册到 JSONRPCBridge 中,这样我们就可以调用它的

方法了。

在②处,引入了 JSON-RPC-Java 的 JS 库 jsonrpc.js,该文件位于 jabsorb-1.2.zip

中。

在③处,创建了一个 JSONRpcClient,然后就可以调用暴露出来的 helloWorld 中

的方法了。与 DWR 不同的是,JSON-RPC-Java 调用方法的回调函数需要放在参

数列表的第一个位置,如果不提供回调函数,则默认是同步调用,调用后直接

返回值。

访问 helloWorld.jsp,单击“sayHello”按钮,显示结果如图 9-13 所示。

图 9-13 HelloWorld 的运行结果

本节通过一个简单的例子,演示了 JSON-RPC-Java 框架的使用,使用风格与 DWR 大

体一致,主要是在后台服务的注册方面显得不够灵活,需要依靠编写代码来完成。

9.3 DWR 和 JSON-RPC-Java 的简单对比

综合前两节中关于 DWR 和 JSON-RPC-Java 的使用,我们将两者的简单对比列在表

9-1 中。

表 9-1 DWR 和 JSON-RPC-Java 的简单对比

对 比 选 项 DWR JSON-RPC-Java

返回数据格式 JavaScript JSON

后台服务的注册 dwr.xml 配置 JSP 或 Servlet

后台服务的访问控制 dwr.xml 配置 无

和其他框架集成度 高 低

流行程度 高 低

9.4 本章小结

本章介绍了服务器端框架 DWR 和 JSON-RPC-Java 的使用,通过对本章的学习,我们

能够在日常的开发中综合使用 DWR 和 JSON-RPC-Java,以及各种 Ajax 的库和框架,极大

地简化了开发工作。

第Ⅳ部分 JSF 与 Ext 的结合应用

CHAPTER

10

10.1 ExtFaces 简介

简单来讲,ExtFaces 是组件的一套 JSF 组件框架,借助 Facelets 的模板技术将 ExtJS、

JSF 和 RichFaces-Ajax4jsf 巧妙地结合在一起。综合了 JSF 页面编写简单和 Ext 组件外观

优雅的特点,简化了 Ext 开发的难度,为开发人员提供了快速开发 JSF 应用而又能充分发

挥 Ext 组件特色的一个解决方案。目前已经测试过可以采用的 JSF 实现是:MyFaces,Sun JSF

参考实现。

10.1.1 ExtFaces 的来源

ExtFaces 来源于一个实际的客户项目,在该项目中需要提供一个友好的用户界面:

类 window—style 的 UI 组件库。单纯的 ExtJS 其实是一个不错的选择,具体的读者可

以参考前面几章的例子。但是大部分团队开发人员对 JavaScript 不熟悉(很多 Java 程

序员也不愿意编写 JavaScript,有意思的是,Sun 的 JavaFX 其实和 JavaScript 是非常类

似的),所以整个 UI 的基础框架决定采用 JSF。RichFaces 是一个不错的选择,其中也

包含了对 Ajax 的支持。但是 RichFaces 在许多方面仍然不能满足项目的要求(如可编

辑、可自动调整列宽、可显示和隐藏列的表格等),因此也就有了将 Ext 封装成 JSF 组

件的想法。

第 10 章

基于 Ext 的 JSF 组件

——ExtFaces

CHAPTER 第 10 章 基于 Ext 的 JSF 组件——ExtFaces

471

10

10.1.2 ExtFaces 采用的技术

1. JSF(http://java.sun.com/javaee/javaserverfaces/)

Java Server Faces(JSF)是一种标准的 J2EE 表示层技术,其主旨是为了使 Java 开发

人员能够快速地开发基于 Java 的 Web 应用程序。它不同于其他 Java 表示层技术的最大

优势是,其采用组件模型、事件驱动。

2. Ext(http://www.extjs.com/)

一个基于 JavaScript、CSS、HTML 的 UI 组件库。

3. Facelets(https://facelets.dev.java.net/)

Facelets 是用来建立 JSF 应用程序的一个可供选择的表现层技术。Facelets 提供了一个

强有力的模板化系统,可以简单地对 UI 页面进行模板化和组件化,减少了组件整合进表现

层时冗余的代码和开发 JSF 组件的烦琐。

4. RichFaces/A4j(http://www.jboss.org/jbossrichfaces/)

JBoss 的基于 Ajax 的 JSF 组件库(主要是借助其中的 Ajax 交互)。

10.1.3 JSF 开发现状

下面的技术点,在前面章节中已经有所介绍,这里只是做一个简单的总结。

1. JSF

Java Server Faces(JSF)是一种标准的 J2EE 表示层技术,它提供了一种以组件为中

心、事件驱动的方式来开发 Java Web 用户界面。JSF 提供了大量的 UI 组件,允许开发人

员通过搭积木的方式使用组件搭建页面,在第三方可视化工具的支持下,能够降低开发的

难度,提高开发的速度。

但是目前的 JSF 也存在不少的缺陷,由于 JSF 过于复杂,其学习曲线明显长于其他框

架如 Struts 等;目前最主要的缺陷就是组件不够丰富,即使与当前包含比较丰富组件库的

RichFaces 和 Ext 相比,在组件渲染能力上也尚有一段差距。同时,JSF 还缺少一些关键的

组件,如可编辑表格,而这往往是用户的需求之一。

另外,虽然 JSF 提供了自定义组件的功能,但是由于自定义组件难度很大,一般的开

发人员难以迅速上手。这些缺陷,阻碍了 JSF 的迅速使用。

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

472

2. Ext

Ext 是基于纯 HTML/CSS+JavaScript 技术的 UI 开源框架。它最初由 Yahoo-UI 的

JavaScript 库扩展而来。目前,它的底层可以支持多种 JavaScript 库,包括 YUI、jQuery、

Prototype 及其自己的实现。Ext 提供了丰富的跨浏览器的 UI 组件,灵活地采用 JSON/XML

作为数据的交换格式,目前已经发行到版本 2.X。

Ext 框架是目前 Web 开发中的一朵奇葩,其极具震撼力的 UI 吸引了很多人的眼球。但

是正如最漂亮的青蛙往往是毒蛙一样,Ext 漂亮的外表下面,是艰难的 JavaScript 和富客户

端架构,需要对 JavaScript 的面向对象思想有很好的理解,这使得项目经理在决定采用 Ext

之前总是犹豫再三。曲高和寡,项目成员中精通 JavaScript 的寥寥无几,而纯客户端的架

构又与当前 Java 社区流行的 SSH 架构颇不匹配,在项目时间有限的情况下,如何抉择,是

比较困难的。

3. Facelets

Facelets 是 JSF 的渲染器之一,尽管 JSP 是 JSF 的默认渲染器,但是 JSF 渲染在设

计上是独立于任何技术的,只要提供合适的 RenderKit,JSF 就可以将任何输出作为渲染

目标,如 PDF 等。并且由于 JSP 本身是基于 Servlet 之上的,JSF 在渲染阶段和 JSP 的

Weave过程本身是不太匹配的。而 Facelets就是 JSF的另一个渲染器,与 JSP相比,Facelets

的渲染速度更快一些,并且 Facelets 提供了模板和组件封装功能,这些都极大地方便了

JSF 的开发。

Facelets 是一个全面的解决方案:专为 JSF 组件模型度身定制的模板化语言。

Facelets 可以很容易地将 HTML 样式模板化,也可以很容易地将页面转换成可重用的

复合组件。

Facelets 具有以下吸引人的特性:

模板化(像 Tiles);

复合组件;

支持 JSTL;

表达式语言;

友好的页面开发;

快速创建组件库。

4. A4j-RichFaces

RichFaces 是用于 Java Server Faces(JSF)的一种用户界面组件套件。它内置了 Ajax 功

能,从而使 JSF 具备了 Rich Internet Application(RIA)功能。

CHAPTER 第 10 章 基于 Ext 的 JSF 组件——ExtFaces

473

10

RichFaces 具有如下特性:

使用 JSF 和 Facelets;

和 JSF RI(Reference Implementation)组件一起使用;

灵活一致的感观;

足够丰富的组件集,以覆盖大多数应用程序需求;

易于使用;

提供灵活的 API,具有动态创建组件能力;

活跃的开发和社区。

目前 Ajax4jsf 项目已经被集成到 RichFaces 项目中,在 ExtFaces 中主要使用的是

RichFaces 的 Ajax 功能,也就是 Ajax4jsf。

Ajax4jsf 是一个将 Ajax 功能添加到 JSF 项目中的开源框架,使用该框架开发人员不

必编写 JavaScript 代码就可以支持 Ajax 功能。

Ajax4jsf 自然地融合到 JSF 框架中,充分利用了 JSF 框架的优点,如:生命周期、

验证、转换的灵活性和受管理的静态及动态资源。通过 Ajax4jsf,标准的 JSF 组件

很容易就具备了 Ajax 功能。

RichFaces 的不足之处还在于它的组件不够漂亮,在看过 Ext 的组件后,很难再对

RichFaces 的组件有信心。另外,一些关键组件如表格,做得不是很出色。

10.1.4 Ext 和 JSF 的结合方案

1. OperaMasks (http://www.operamasks.org)

OperaMasks 是金蝶提出的一个 JSF 实现和组件库 AOM,通过自定义组件的方式将 Ext

的组件封装成 JSF 组件,添加了自定义的渲染器及相关的渲染组件,并将 Annotation、IoC

结合到 JSF 的开发中。结合金蝶的设计器,AOM 为 JSF 开发人员提供了完整的开发套装,

降低了 JSF 企业开发中的难度。它以 JSF 为基础引擎,辅以独创的原生 Ajax 技术,同时提

供丰富的组件和集成开发环境,使 Web 开发变得简单、方便。下面是网上的一些关于

OperaMasks 特点的介绍资料。

开箱即用:使用 Apusic OperaMasks 进行 Web 开发,开发人员只需掌握基础的 J2EE

知识即可。

引擎:提供标准的 JSF 引擎,JSF 是已经得到业界认同的一种主流组件技术,能够

很容易地适应各种未来需求的变化。

原生 Ajax 支持:从容器级别对 Ajax 予以支持,对 Apusic OperaMasks 用户来说,

要使应用具备 Ajax 特性,付出的成本是零。

IoC 编程思想:OperaMasks 2.0 中提出了一种全新的编程模型,即 IoVC(Inversion of

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

474

View-Control,视图控制反转),IoVC 能够把对“View(即 UI 视图)的控制力”

注入到后台业务逻辑中。这样,用户在编写业务逻辑的过程中,会对 View 拥有足

够的控制力,从而能够将展现层与业务逻辑更好地解耦。

Rich Components:Apusic OperaMasks 给用户提供了丰富的 Rich Components,用户

可以用各种组件来组织数据、展现数据、编辑数据。

开发工具:提供开发工具 Apusic Studio 的支持,“所见即所得”的可视化设计,一

体化的开发、配置、部署、调试、监控的集成开发环境。

跨浏览器支持:Apusic OperaMasks 完全兼容业界主流浏览器,如 IE、Firefox 等。

开源社区:所有源码甚至开发过程自身,全部通过OperaMasks.org开放,代表了 J2EE

社区开放和敏捷的力量。

但是,仔细研究一下,也会发现一些不是很方便的地方:

提供的组件依赖于自己的 JSF 实现,将组件迁移到其他的 JSF 实现上难度很大(或

许有商业考虑)。

虽然内置了 Ajax 的支持,但是不是业界流行的 A4j 实现,学习难度大。

对 ExtJS 组件的封装采用标准的 JSF 组件开发模型封装,这就使得 JSF 组件的开

发难度对于普通的开发人员比较困难,造成项目的参与难度比较大。本质上,金

蝶的 AOM 和 JBoss 的 Seam 走的是一条路子,即从 Runtime Server 到组件,乃至

中间的资源注入,都提供了完整的解决方案。就框架本身而言是非常出色的,但

是和 Seam 一样,AOM 存在着和底层绑定太死的缺点,AOM 的组件库如果离开

了其配套的环境单独使用,则会有很多的问题,这也从另一方面限制了 AOM 的

推广。

2. Lilya(http://sharesource.org/project/lilya/)

Lilya 也是对 Ext 组件进行封装的一个框架,基于 Ext 1.1 封装。由于采用 RichFaces 对

Ext 1.1 进行封装,所以实现方式其实就是基于 RichFaces 定制自己的组件。但是目前该项

目更新缓慢,远未达到实用的程度。

10.2 ExtFaces 原理

如图 10-1 所示是 ExtFacs 的基本架构。简单来说,就是采用 Facelets 将 Ext 组件封

装成 JSF 组件的框架。它在前端利用 Facelets 提供的强大的定制标签能力,把 Ext 的组

件封装为一个个简单的 JSF 风格的组件,如<ext:tabPanel>、<ext:window>等。当请求

一个这样的页面时,Ext 首先将这些标签翻译成 JSF 组件,并附加 Ext 风格的皮肤,然

后再提交给 JSF 实现来处理,JSF 处理完后渲染到页面上,就形成了具有 Ext 风格的

JSF 树。

CHAPTER 第 10 章 基于 Ext 的 JSF 组件——ExtFaces

475

10

图 10-1 ExtFaces 的基本架构

Facelets 是一个 ViewHandler,它用于页面的输出是完美的,所以我们可以用它来定

制标签,展现任何类似 Ext 的效果。但是在和服务器端 bean 对象绑定时,由于不同于

JSF 组件,它没有 Decode 阶段,所以无法实现从页面中获取数据的行为。而这个过程

的最好实现方式就是直接借助 JSF 的绑定机制来完成,所以在实现 Ext 风格的表单域时,

后端通常都对应一个标准的 JSF 组件 h:inputText。对于 Ext 按钮,采用生成

a4j:commandButton 来实现 Ajax 方式进行的提交。

我们可以看一下<ext:textField>组件的源码,使用 Facelets 创建一个组件,由两部分组

成:一个是 TagHandler,另一个是模板页面 field.xhtml。我们先看一下 TagHandler 的代码:

public class FieldTag extends TagHandler {

private static final String

xhtml="/WEB-INF/facelets/tags/form/field.xhtml";

public FieldTag(TagConfig config) {

super(config);

}

public void apply(FaceletContext ctx, UIComponent parent)

throws IOException, FacesException, FaceletException, ELException {

Map config=new HashMap();

TagAttribute[] attributes = this.tag.getAttributes().getAll();

for (int i = 0; i < attributes.length; i++) {

TagAttribute attribute = attributes[i];

String name = attribute.getLocalName();

String value = attribute.getValue();

if(StringUtil.isBinding(value)){

String[] strs=StringUtil.getBindingInfo(value);

ctx.setAttribute("object", ctx.getFacesContext().

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

476

getApplication().createValueBinding("#{"+strs[0]+"}").

getValue(ctx.getFacesContext()));

ctx.setAttribute("property", strs[1]);

}

else{

config.put(name, value);

ctx.setAttribute(name,value);

}

}

ctx.setAttribute("field_config", config);

Object formId=ctx.getAttribute("formId");

if(formId==null){

ctx.setAttribute("formId", parent.getId());

}

Map map=(Map)ctx.getAttribute("config");

this.nextHandler.apply(ctx, parent);

ctx.includeFacelet(parent,

ctx.getFacesContext().getExternalContext().getResource(xhtml));

}

}

上述代码比较容易理解,就是读取标签中配置的属性,然后放入 FaceletContext 中以供

模板页面使用。我们再来看一下模板页面的代码:

<?xml version="1.0" encoding="UTF-8"?>

<!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:a4j="http://richfaces.org/a4j"

xmlns:rich="http://richfaces.org/rich"

xmlns:f="http://java.sun.com/jsf/core" xml:lang="en" lang="en">

<ui:composition>

<script>

var #{id};

Ext.onReady(

function(){

var #{id}_config=#{ext:getConfigDefByMap(field_config)};

#{id}_config.applyTo='#{formId}:#{id}';

CHAPTER 第 10 章 基于 Ext 的 JSF 组件——ExtFaces

477

10

#{id}=new Ext.form.TextField(#{id}_config);

}

);

</script>

<div tabindex="-1" class="x-form-item">

<label class="x-form-item-label" style="width: 75px;" for="#{id}">

#{fieldLabel}:

</label>

<div class="x-form-element">

<h:inputText value="#{object[property]}"

id="#{id}"></h:inputText>

</div>

</div>

</ui:composition>

</html>

在上述代码中,我们主要使用 JavaScript 创建一个 Ext.form.TextField 对象,同时还创

建了一个<h:inputText />组件。注意 Ext.form.TextField 对象的 applyTo 属性,它指向了

<h:inputText />组件,这样,Ext.form.TextField 对象和<h:inputText />组件就结合在一起了。

通过这种方式,我们可以获得 Ext 组件的外观,同时通过普通的 JSF 组件和后端的数据进

行了绑定。其他的组件实现方式也是遵循这个思想,有兴趣的读者可以参考其源码。

10.3 ExtFaces 目标

目前提供 ExtFaces 的基础代码,大家如果有兴趣,可以在此基础上进行修改、封装、

扩展,构造自己的组件库、框架。

在这里我们只是演示一种思路,并提供几个简单的小例子供大家参考。其实该思路也

同样不受制于具体 UI 库的实现,可以采用 Ext,也可以采用其他的组件库如 dojo 等。

10.4 ExtFaces 组件介绍

10.4.1 简单组件

1. <ext:panel>组件

该组件对应于 Ext.Panel,对 Ext.Panel 的绝大多数配置属性都支持。

参考例子:SimplePanel.xhtml

<ext:panel width="400" title="Simple Panel" height="200" />

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

478

显示效果如图 10-2 所示。

图 10-2 效果图

2. <ext:tabPanel>组件

该组件对应于 Ext.TabPanel,对 Ext.TabPanel 的绝大多数配置属性都支持。

参考例子:SimpleTabPanel.xhtml

<ext:tabPanel width="400" activeTab="0" frame="true" height="200">

<ext:panel title="Tab1" />

<ext:panel title="Tab2" />

</ext:tabPanel>

显示效果如图 10-3 所示。

图 10-3 效果图

3. <ext:window>组件

该组件对应于 Ext.Window,对 Ext.Window 的绝大多数配置属性都支持。

参考例子:SimpleWindow.xhtml

<ext:window id="win" layout="fit" width="500" renderTo=""

title="Sample Window" height="300" plain="true" closeAction= "hide">

</ext:window>

CHAPTER 第 10 章 基于 Ext 的 JSF 组件——ExtFaces

479

10

显示效果如图 10-4 所示。

图 10-4 效果图

4. <ext:formPanel>组件

该组件对应于 Ext.form.FormPanel,对 Ext.form.FormPanel 的绝大多数配置属性都支持。

参考例子:SimpleForm.xhtml

<ext:formPanel id="fp" width="300" height="160" title="Simple Form"

layout="form" contentEl="form1" frame="true">

<h:form id="form1">

<ext:textField fieldLabel="Name" name="name" allowBlank= "false"

value="#{person.name}" id="name" />

<ext:textField fieldLabel="Age" name="age" allowBlank= "false"

value="#{person.age}" id="age" />

<ext:dateField fieldLabel="Birthday" name="birthday"

allowBlank="false" value="#{person.birthday}"

id="birthday" format="Y-m-d" />

<ext:button value="save" id="save"

action="#{person.savePerson}" />

</h:form>

</ext:formPanel>

显示效果如图 10-5 所示。

图 10-5 效果图

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

480

上述例子代码中同时还使用了<ext:textField>组件(对应于 Ext.form.TextField)、

<ext:dateField> 组 件 ( 对 应 于 Ext.form.DateField ) 和 <ext:button> 组 件 ( 对 应 于

Ext.Button)。

注意事项:

<ext:formPanel>组件需要嵌入 <h:form>标签来包装整个表单域,同时

<ext:formPanel>组件的 contentEl 属性必须和<h:form>的 id 属性一致,并且两者是

必需的。

对于<ext:textField>、<ext:dateField>值绑定,方式和<h:inputText>一致。

对于<ext:button>事件绑定,类似于<h:commandButton>,绑定到 action 即可

(表单的提交采用的是 Ajax 方式)。

10.4.2 嵌套组件

1. 嵌套 Panel

Panel 中可以嵌套 Panel。

参考例子:NestedPanel.xhtml

<ext:panel width="400" title="Simple Panel" height="200">

<ext:panel width="200" title="Nested Panel" height="100">

</ext:panel>

</ext:panel>

显示效果如图 10-6 所示。

图 10-6 效果图

2. TabPanel 中嵌套表单

参考例子:TabPanelWithForm.xhtml

<ext:tabPanel width="400" activeTab="0" frame="true" height="200">

<ext:formPanel id="fp" width="300" height="160" title="Simple Form"

CHAPTER 第 10 章 基于 Ext 的 JSF 组件——ExtFaces

481

10

layout="form" contentEl="form1" frame="true">

<h:form id="form1">

<ext:textField fieldLabel="Name" name="name" allowBlank="false"

value="#{person.name}" id="name" />

<ext:textField fieldLabel="Age" name="age" allowBlank="false"

value="#{person.age}" id="age" />

<ext:dateField fieldLabel="Birthday" name="birthday"

allowBlank="false" value="#{person.birthday}"

id="birthday" format="Y-m-d" />

<ext:button value="save" id="save" action="#{person.savePerson}" />

</h:form>

</ext:formPanel>

<ext:panel title="Tab2" />

</ext:tabPanel>

显示效果如图 10-7 所示。

图 10-7 效果图

10.5 开发计划

第一阶段:核心基础框架 2007.12 ~ 2008.6 状态完成,目前可以采用的 JSF 实现包

括 SUN-RI 和 MyFaces。

第二阶段:基础组件开发 2008.6 ~ 2008.10 状态完成,包括组件如下:

Panel

TabPanel

Window

FormPanel

TextField

DateField

企业级 Web 开发实战——JSF/RichFaces,ExtJS 实战剖析

482

Compoment

第三阶段:正在开发的组件 2008.11 ~ 目前,如下:

Combox

Table

Tree

第四阶段:与 JBoss IDE、MyEclipse、WTP 等结合,支持目前流行的与 JSF 相关的可

视化开发工具(计划中)。

10.6 本章小结

本章主要是结合实际应用开发中出现的问题,提出一个综合的解决方案,并且提供一

个基本的范例。章节中提出的 ExtFaces 只是一种实现方式,其实,大家也可以结合自己的

UI 技术偏好提出其他的 JSF 解决方案,比如 DojoFaces、..Faces 等,希望此方案能对我们

实际的开发项目有所帮助和启示。

反侵权盗版声明

电子工业出版社依法对本作品享有专有出版权。任何未经权利人书面许可,

复制、销售或通过信息网络传播本作品的行为;歪曲、篡改、剽窃本作品的行为,

均违反《中华人民共和国著作权法》,其行为人应承担相应的民事责任和行政责任,

构成犯罪的,将被依法追究刑事责任。

为了维护市场秩序,保护权利人的合法权益,我社将依法查处和打击侵权盗

版的单位和个人。欢迎社会各界人士积极举报侵权盗版行为,本社将奖励举报有

功人员,并保证举报人的信息不被泄露。

举报电话:(010)88254396;(010)88258888

传 真:(010)88254397

E-mail: [email protected]

通信地址:北京市万寿路 173 信箱

电子工业出版社总编办公室

邮 编:100036

www.phei.com.cn www.broadview.com.cn

《企业级 Web 开发实战——JSF/RichFaces,

ExtJS 实战剖析》读者交流区

尊敬的读者:

感谢您选择我们出版的图书,您的支持与信任是我们持续上升的动力。为了使您能通过本书更

透彻地了解相关领域,更深入的学习相关技术,我们将特别为您提供一系列后续的服务,包括:

1. 提供本书的修订和升级内容、相关配套资料;

2. 本书作者的见面会信息或网络视频的沟通活动;

3. 相关领域的培训优惠等。

请您抽出宝贵的时间将您的个人信息和需求反馈给我们,以便我们及时与您取得联系。

您可以任意选择以下三种方式与我们联系,我们都将记录和保存您的信息,并给您提供不定

期的信息反馈。

1.短信

您只需编写如下短信:B08476+您的需求+您的建议

发送到1066 6666 789(本服务免费,短信资费按照相应电信运营商正常标准收取,无其他信息收费)

为保证我们对您的服务质量,如果您在发送短信24小时后,尚未收到我们的回复信息,请直接拨打

电话 (010)88254369。

2.电子邮件

您可以发邮件至[email protected][email protected]

3.信件

您可以写信至如下地址:北京万寿路173信箱博文视点,邮编:100036。

如果您选择第2种或第3种方式,您还可以告诉我们更多有关您个人的情况,及您对本书的意

见、评论等,内容可以包括:

(1)您的姓名、职业、您关注的领域、您的电话、E-mail地址或通信地址;

(2)您了解新书信息的途径、影响您购买图书的因素;

(3)您对本书的意见、您读过的同领域的图书、您还希望增加的图书、您希望参加的培训等。

如果您在后期想退出读者俱乐部,停止接收后续资讯,只需发送"B08476+退订"至10666666789即可,或

者编写邮件"B08476+退订+手机号码+需退订的邮箱地址"发送至邮箱:[email protected]亦可取消该

项服务。

同时,我们非常欢迎您为本书撰写书评,将您的切身感受变成文字与广大书友共

享。我们将挑选特别优秀的作品转载在我们的网站(www.broadview.com.cn)上,或

推荐至CSDN.NET等专业网站上发表,被发表的书评的作者将获得价值50元的博文视

点图书奖励。

我们期待您的消息!

博文视点愿与所有爱书的人一起,共同学习,共同进步!

通信地址:北京万寿路 173 信箱 博文视点(100036) 电话:010-51260888

E-mail:[email protected][email protected]