JSF 2 简介: HTML5 复合组件,第 1 部分

开始使用 JSF 2 实现 HTML5 组件库

HTML5 为基于浏览器的应用程序提供可与桌面软件相媲美的丰富功能。在这一期的 JSF 2 fu 中,我们将使用 JavaServer Faces (JSF) 2 实现 HTML5 复合组件,您将明白如何最高效地利用 Java™ 和 HTML5。

David Geary, 总裁, Clarity Training, Inc.

David Geary 的照片David Geary 是一名作家、演讲家和顾问,也是 Clarity Training, Inc. 的总裁,他指导开发人员使用 JSF 和 Google Web Toolkit (GWT) 实现 Web 应用程序。他是 JSTL 1.0 和 JSF 1.0/2.0 专家组的成员,与人合作编写了 Sun 的 Web Developer 认证考试的内容,并且为多个开源项目作出贡献,包括 Apache Struts 和 Apache Shale。David 的 Graphic Java Swing 一直是关于 Java 的畅销书籍,而 Core JSF(与 Cay Horstman 合著)是关于 JSF 的畅销书。他还是 GWT Solutions 一书的作者。David 经常在各大会议和用户组发表演讲。他从 2003 年开始就一直是 NFJS tour 的定期演讲人,并且在 Java University 教授课程,三次当选为 JavaOne 之星。



2010 年 12 月 27 日

HTML5 显而易见是软件开发的下一大热门。起初以 Web 应用程序而为人所知的 HTML5 最终将桌面应用程序的强大功能 — 通过拖放、画布、视频和音频等设施完成 — 引入了浏览器。HTML5 集各种技术(特别是规范)于一体,形成了一个囊括 HTML、JavaScript 和层叠样式表(CSS)在内的功能强大的 API。以下是一些 HTML5 要点:

关于本系列

JSF 2 fu 系列是继 David Geary 同名的 三篇文章简介 推出的,像一个功夫大师一样帮助您发展和磨练您的 JSF 2 框架技能。本系列深入专研框架及其周围的生态系统,并从外部视角展示一些 Java EE 技术,比如 Contexts and Dependency Injection,如何与 JSF 集成。

  • 画布
  • 拖放
  • 地理位置*
  • 内联编辑
  • Web workers*
  • Web 存储*
  • 消息传递
  • 离线应用
  • 视频和音频*
  • Web 套接字*

要注意地理位置和离线应用这样的前瞻性功能。(我用星号标记的功能从技术上讲不是 HTML5 规范的一部分呢,不过一般通俗地使用术语 HTML5 来包含我列出的所有功能。参见 参考资料 了解更多信息。)

从某些方面看,HTML5 就是下一个 Java。在 20 世纪 19 年代后期,Java 语言大受欢迎,主要是因为其编写一次,到处运行 的功能使开发人员免于具体选择(或移植到)Windows®、Mac 或 Linux®。HTML5 允许您编写一次,在任何(现代)浏览器中运行,因此您无需在 iOS、Android 和 Chrome 之间做出选择。

编写一次,随处调试

Java 技术允许您为多个操作系统编写一个应用程序,但事实上并非完全如此。HTML5 也是这样。HTML5 不提供本机操作系统提供的一些设施,比如与加速计进行交互。(尽管有工具包 — 比如 PhoneGap [参见 参考资料] — 可以弥补空缺。这些缺点总是使一些开发人员避开 HTML5 而垂青本机应用程序。但是对于大部分应用程序,HTML5 提供更好的投资回报。

Java 技术

HTML5 可能就是下一个 Java,但是不会取代它。Java 技术为服务器端编程提供丰富的生态系统。而最初基于 HTML 的 JSF 允许您便捷地使用 HTML5,如同至今为止通过 JSF 使用 HTML4 一样。您将获得所有强大的 JSF 功能,比如,除了 HTML5 之外,还有 facelets 模板、复合组件和内置 Ajax。

在本文中,您将学习如何使用 JSF2 创建 HTML5 复合组件。在下一篇 JSF 2 fu 文章中,我将向您展示如何创建 HTML5 组件的一个库。

开始使用 HTML5

使用 HTML5 其实更多地是涉及到 JavaScript 而非 HTML。也就是说,您需要有一个很好的 JavaScript 调试器。我建议使用 Google Chrome 的内置开发工具中自带的调试器(参见 参考资料),如图 1 所示:

图 1. 使用 Chrome 开发工具调试 JavaScript
Google Chrome 的内置开发工具自带的 JavaScript 调试器屏幕截图

图 1 所示的 Chrome 调试器中,在 canvas 组件下面有一个包含 JavaScript 代码的面板。

现在,有了一个较好的 JavaScript 调试器,您只需要一个可支持 HTML5 的浏览器。较流行的浏览器的大部分最新版本都可以很好地支持 HTML5。(微软似乎在其即将发布的 Internet Explorer 9 中具有良好的 HTML5 支持。)


使用 HTML5 canvas

HTML5 canvas 是一种成熟的 2D 绘图界面,足以完成 Plants vs. ZombiesQuake II 这样的游戏。我对 HTML5 canvas 的使用,如图 2 所示,可能没有那么深入,但是作为指导已经足够了:

图 2. 一个简单的 HTML5 canvas 示例
浏览器屏幕截图,显示通过使用 HTML5 canvas 的绘画应用程序创建的蓝色卡通人物

我向 HTML5 canvas 添加了一些 JavaScript 来实现 图 2 所示的简单绘画应用程序。在移动鼠标时,画布左上角的读出器中显示鼠标坐标。在画布中拖动鼠标时,使用蓝色画笔绘图。

图 2 中所示的应用程序是一个 JSF 应用程序。图 3 显示其目录结构:

图 3. canvas 示例的目录结构
canvas 示例的目录结构屏幕截图

应用程序的 facelet 在 web/WEB-INF/index.xhtml 中定义,且应用程序的 JavaScript 位于 web/resources/application/paintingCanvas.js 中。清单 1 显示 index.xhtml:

清单 1. 使用 <canvas> tag (WEB-INF/index.xhtml)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml"
   xmlns:h="http://java.sun.com/jsf/html">

   <h:head>
      <title>#{msgs.windowTitle}</title>
   </h:head>
   
   <h:body style="background: #fefeef">
      <h:outputScript library="application" name="paintingCanvas.js" 
                       target="head" />
      
      <h3>#{msgs.heading}</h3>
   
   
      <canvas width="400px" height="400px" id="paintingCanvas">

         Canvas not supported. 
         
      </canvas>     
   </h:body>  
   
</html>

清单 1 中的 index.xhtml 文件是一种 HTML5 多语言文档(参见 参考资料),因为它有一个 HTML5 文档类型/名称空间和格式标准的 XHTML 语法 — 这正是我酝酿 facelets 和 HTML5 所必需的。

我使用 <h:outputScript> 标记导入相应的 JavaScript。最后,我开始使用 HTML5 canvas 元素。如果浏览器不明白 <canvas> 标记,它会显示一条 Canvas not supported 消息。<canvas> 标记不会高亮显示;所有有趣的代码在相应的 JavaScript 中,如清单 2 所示:

清单 2. 绘图画布 JavaScript (resources/application/paintingCanvas.js)
window.addEventListener("load", function() {
   var canvas, context, painting;
   
   function init() {
      canvas = document.getElementById("paintingCanvas");
      if (canvas == null) return;
      
      context = canvas.getContext("2d");
      if (context == null) return;
   
      painting = false;
      
      context.strokeStyle = "#00f";
      context.lineWidth = 3;
      context.font = "15px Helvetica";
   }

   init();
   
   canvas.addEventListener("mousedown", function(ev) {
      painting = true;
      context.beginPath();
      context.moveTo(ev.offsetX, ev.offsetY);
   }, false);
   
   canvas.addEventListener("mousemove", function(ev) {
      updateReadout(ev.offsetX, ev.offsetY);

      if (painting) {
        paint(ev.offsetX, ev.offsetY);
      }
      function updateReadout(x, y) {
         context.clearRect(0, 0, 100, 20);
         context.fillText("x: " + x + ",  y: " + y, 5, 15);
      }
      function paint(x, y) {
         context.lineTo(ev.offsetX, ev.offsetY);
         context.stroke();
      }
         
   }, false);
   
   canvas.addEventListener("mouseup", function() {
      painting = false;
      context.closePath();
   }, false);
   
   
 }, false);

运行样例代码

本系列的代码基于运行于企业容器(比如 GlassFish 或 Resin)内的 JSF 2。参阅本系列第一期 “JSF 2 fu: Ajax 组件”,这是个分步教程,讲解如何使用 GlassFish 安装和运行本系列的代码。参见 下载 部分,获取本文的样例代码。

清单 2 使用鼠标光标读出器实现简单的绘画,如 图 2 所示。当页面加载时,我使用 document.getElementById() 获取对画布的引用。从画布中,我获取对画布上下文的引用。我在后续的事件处理程序中使用该内容,我使用 JavaScript 闭包或 Java 开发人员所指的匿名内部类实现它。

如果您使用过 Abstract Window Toolkit (AWT),就会了解画布内容是对 AWT 的图形内容的模仿。毕竟,绘制二维的形状、图像和文本有这么多方式。在 清单 2 中,我使用蓝色的描边风格 初始化上下文,并设置了线宽和字体。之后所需的操作就只是移动、描边、重复、鼠标上下拖动。

学习了 HTML5 canvas 基础知识之后,我将向您展示如何创建一个 JSF 2 HTML5 复合组件。


一个 JSF 2 HTML5 canvas 组件

接下来,我要实现一个使用 HTML5 canvas 的 JSF 2 复合组件。我需要复合组件实现以下要求:

  • 拥有可配置的(通过标记属性)宽度、高度、画笔颜色、线宽和 CSS 样式
  • 将组件标记的正体作为 canvas not supported 消息
  • 自动包括 canvas 的 JavaScript
  • 在一个页面中支持多个 canvas 组件

使用 canvas 复合组件的应用程序如图 4 所示:

图 4. 动态 canvas 复合组件
动态 canvas 复合组件屏幕截图

图 4 中所示的应用程序有三个 canvas 组件,每个组件的配置都有所不同。从左边开始,前两个是绘图画布,类似于 图 2 中显示的那一个。最右边的画布仅绘制一个笑脸。

清单 3 显示 图 4 中显示的页面的标记:

清单 3. 使用 canvas 复合组件(WEB-INF/index.xhtml)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml"
   xmlns:h="http://java.sun.com/jsf/html"
   xmlns:h5="http://java.sun.com/jsf/composite/html5">

   <h:head>
      <meta charset="UTF-8" />
      <title>#{msgs.windowTitle}</title>
   </h:head>
   
   <h:body style="background: #fefeef">
      
      <h3>#{msgs.heading}</h3>
   
      <h5:canvas id="paintingCanvas" width="300px" height="300px" 
                 penColor="#7F7" lineWidth="7">
              
         #{msgs.canvasUnsupported}

      </h5:canvas>     
      
      <h5:canvas id="secondPaintingCanvas" width="300px" height="300px"
                 style="border: thick solid red">

         #{msgs.canvasUnsupported}

      </h5:canvas>  
      
      <h5:canvas id="smileyCanvas" library="application" script="smiley.js"
                 width="300px" height="300px"
                 style="border: thin solid red">

         #{msgs.canvasUnsupported}

      </h5:canvas>
   </h:body>  
   
</html>

清单 3 中,canvas 组件导入适当的 JavaScript — 不同于 清单 1,其中我手动使用 HTML5,且必须显式地导入相关的 JavaScript。页面创建者可以使用 canvas 组件的可选 libraryscript 属性来制定一个画布的 JavaScript,或者他们可以依赖于默认的 JavaScript。在 清单 3 中,我为笑脸画布使用 script 属性。我为示例中最左边的两个画布使用默认的 JavaScript(resources/html5/canvasDefault.js,实现绘图画布)。

清单 4 显示 canvas 复合组件的实现:

清单 4. canvas 复合组件(resources/html5/canvas.xhtml)
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:composite="http://java.sun.com/jsf/composite">

  <composite:interface>
    <composite:attribute name="id"/>  
    <composite:attribute name="width"/>
    <composite:attribute name="height"/>   
    <composite:attribute name="library"  default="html5"/>
    <composite:attribute name="script"   default="canvasDefault.js"/>
    <composite:attribute name="style"    default="border: thin solid blue"/>
    <composite:attribute name="penColor" default="#7777FF"/>
    <composite:attribute name="lineWidth" default="2"/>
  </composite:interface>
  
  <composite:implementation>
      <canvas id="#{cc.id}"
           width="#{cc.attrs.width}" 
          height="#{cc.attrs.height}"
           style="#{cc.attrs.style}">
                                
      <composite:insertChildren/>
           
     </canvas>
    
    <h:outputScript library="#{cc.attrs.library}" 
                       name="#{cc.attrs.script}"/>
                       
    <script>
      #{cc.attrs.script}.init('#{cc.id}', 
                              '#{cc.attrs.penColor}', 
                              '#{cc.attrs.lineWidth}')
    </script>                                                 
  </composite:implementation>
</html>

清单 4 中,我为 canvas 复合组件声明了 8 个属性,其中 5 个具有默认值。组件的实现包含一个 HTML5 canvas,带有从组件的相关属性中配置的 ID、宽度、高度和样式。这满足了我对 canvas 组件的第一个要求(可配置属性)。

canvas 复合组件将其子元素— 即将 <h5:canvas> 标记正体中的任何内容 — 插入到 HTML5 canvas 标记(<canvas>)中。这表示,如果浏览器不支持 HTML5 canvas,标记正体中的文本会显示出来。这满足了第二个要求(将组件标记的正体作为 canvas not supported 消息)。

canvas 组件包含一个 <h:outputScript> 标记,该标记导入 canvas 的 JavaScript,这由 canvas 组件的 libraryscript 属性指定。注意,libraryscript 属性的默认设置分别为 html5canvasDefault.js。这满足了第三个要求(自动导入 canvas 的 JavaScript)。

最后,canvas 组件调用一个名为 init() 的 JavaScript 方法,传递 canvas 的 ID、画笔颜色和线宽。init() 方法获取并初始化 canvas 的上下文。注意我说的是方法,而非函数,因为 init() 方法属于一个对象。该对象名源自于 canvas 组件的 script 属性。例如,对于 清单 3 中的笑脸画布,我指定 smiley.js 的一个 script 值,因此笑脸画布组件会调用 smiley.js.init()js 对象的 init() 方法,包含在一个名为 smiley 的对象中。如果您不显式地指定 script 值,其默认设置为 canvasDefault.js,因此 JavaScript 方法会是 canvasDefault.js.init()。调用这些方法,而非全局函数,这使我满足了第四个要求:在一个页面中支持多个画布。

canvas 组件的默认 JavaScript 如清单 5 所示:

清单 5. 默认的 canvas JavaScript (resources/html5/canvasDefault.js)
if (!canvasDefault) var canvasDefault = {}

if (!canvasDefault.js) {
  canvasDefault.js = {
    init : function(canvasId, penColor, lineWidth) {
      var canvas, context, painting;

      canvas = document.getElementById(canvasId);
      if (canvas == null) {
        alert("Canvas " + canvasId + " not found")
      }

      context = canvas.getContext("2d")
      if (context == null)
        return;

      painting = false;

      context.strokeStyle = penColor
      context.lineWidth = lineWidth
      context.font = "15px Helvetica"

      canvas.addEventListener("mousedown", function(ev) {
        painting = true
        context.beginPath()
        context.moveTo(ev.offsetX, ev.offsetY)
      }, false)

      canvas.addEventListener("mousemove", function(ev) {
        updateReadout(ev.offsetX, ev.offsetY)

        if (painting) {
          paint(ev.offsetX, ev.offsetY)
        }
        function updateReadout(x, y) {
          context.clearRect(0, 0, 100, 20)
          context.fillText("x: " + x + ",  y: " + y, 5, 15)
        }
        function paint(x, y) {
          context.lineTo(ev.offsetX, ev.offsetY)
          context.stroke()
        }

      }, false)

      canvas.addEventListener("mouseup", function() {
        painting = false
        context.closePath()
      }, false)
    }
  }
}

清单 5 中,我创建一个名为 canvasDefault 的对象,包含一个名为 js 的对象,该对象包含一个 init() 方法。我这样做是为了为 init() 方法创建名称空间,这样它就不会被另一个全局 init() 函数所覆盖。这样一来,我就可以在一个页面中含有多个画布,所有画布都有其自己的 init() 函数的实现。

清单 6 显示笑脸画布的 JavaScript:

清单 6. 笑脸画布的 JavaScript (resources/application/smiley.js)
if (!smiley) var smiley = {}

if (!smiley.js) {
  smiley.js = {
    init : function(canvasId, penColor, lineWidth) {
      var canvas, context

      canvas = document.getElementById(canvasId);
      if (canvas == null) {
        alert("Canvas " + canvasId + " not found")
      }

      context = canvas.getContext("2d");
      if (context == null)
        return

      // smiley face code originally downloaded
      // from thinkvitamin.com
        
      // Create the face

      context.strokeStyle = "#000000";
      context.fillStyle = "#AAAAFF";
      context.beginPath();
      context.arc(100,100,50,0,Math.PI*2,true);
      context.closePath();
      context.stroke();
      context.fill();
      
      // eyes              
      context.strokeStyle = "#000000";
      context.fillStyle = "#FFFFFF";
      context.beginPath();
      context.arc(80,80,8,0,Math.PI*2,true);
      context.closePath();
      context.stroke();
      context.fill();
      
      context.fillStyle = "#0000FF";
      context.beginPath();
      context.arc(80,80,5,0,Math.PI*2,true);
      context.closePath();
      context.fill();
      
      context.strokeStyle = "#000000";
      context.fillStyle = "#FFFFFF";
      context.beginPath();
      context.arc(120,80,8,0,Math.PI*2,true);
      context.closePath();
      context.stroke();
      context.fill();
      
      context.fillStyle = "#0000FF";
      context.beginPath();
      context.arc(120,80,5,0,Math.PI*2,true);
      context.closePath();
      context.fill();
      
      // nose
      context.fillStyle = "#000000";
      context.beginPath();
      context.moveTo(93,100);
      context.lineTo(100,93);
      context.lineTo(107,100);
      context.closePath();
      context.fill();

      // smile              
      context.strokeStyle = "#000000";
      context.beginPath();
      context.moveTo(70,110);

      context.quadraticCurveTo(100,150,130,110);       
      context.closePath();
      context.stroke();    
    }
  }
}

清单 6 继续使用 清单 5 中使用过的同一命名空间约定。其他画布的后续 JavaScript 必须遵循相同的约定。


结束语

在本文中,我介绍了 HTML5 并展示了如何实现一个 JSF 2 HTML5 canvas 复合组件,使 JSF 开发人员和页面创建者更便捷地使用 HTML5 canvas。我展示了如何将从 JSF 表达式语言获取的信息传递给与复合组件相关的 JavaScript,以及如何为 JavaScript 函数创建名称空间,以便相同命名的函数不会相互冲突。在下一期的 JSF 2 fu 中,我将向您展示如何实现另一个 HTML5 组件,以及如何将多个 HTML5 组件放在一个可重用的库中,您可以在一个 JAR 文件中将该库分发给其他开发人员。


下载

描述名字大小
本文样例代码j-jsf2fu-1010.zip49KB

参考资料

学习

讨论

  • 加入 My developerWorks 社区。查看开发人员推动的博客、论坛、组和 wikis,并与其他 developerWorks 用户交流。

条评论

developerWorks: 登录

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


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


忘记密码?
更改您的密码

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

 


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

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

选择您的昵称



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

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

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

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

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

 


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


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development
ArticleID=605051
ArticleTitle=JSF 2 简介: HTML5 复合组件,第 1 部分
publish-date=12272010