若干年前,很少有人会想到一家生产电脑的公司会凭借一款功能设计上存在着不少缺陷的手机在市场上取得巨大的成功。也很少有人会想到一家曾经 占据着智能手机大部分市场份额的厂商会落入今天这样举步维艰的局面。人们不禁惊讶的发现,精美的界面、方便的操作对于消费者的吸引力要大于完善的功能及稳 定的系统。用户体验的优劣在一定的程度上决定了一个面向终端用户的产品能否在市场上生存。移动设备的设计如此,互联网应用的设计亦如此。现在,互联网上充 斥着各种精美的 CSS 式样、JavaScript 特效、Flash 动画等等,来吸引大众的眼球以获得更多的用户关注和经济收益。这其中存在着两种流行的设计趋势。一种是以高级的 JavaScript 技术和 CSS 技术为基础的 DHTML,以此来实现美观和交互性强的用户界面。这种技术的优势是浏览器能够提供天然的支持而不依赖于任何插件而且能够很灵活的访问页面上的内容,但是 这种技术的不足是浏览器自身的局限使得一些功能实现起来很困难。比如若要实现网页上的矢量绘图,虽然有 VML、SVG 等技术,但是它们不是广泛支持的标准,需要在不同的浏览器上做不同的处理。另一种是在浏览器上面安装某种包含运行环境的插件来运行某些 RIA 的应用,如 Flash、Silverlight、JavaFX 等,这些技术都是基于矢量绘图,能够呈现绚丽的用户界面和灵活多变的用户交互。但它们的缺点就是需要在浏览器上再安装插件,而且运行效率往往也会存在着一 定的问题。新一代的 Web 页面标准 HTML5 则可以帮助我们很好的解决这一问题,它不但提供了很多诸如 Web 套接字、Web 存储等技术,而且提供了 Canvas 以便在 Web 页面上直接进行矢量绘图。作为 HTML5 标准的一部分,Canvas 将天然地被各种浏览器支持,而且便于与 JavaScript 进行交互。从某种意义上说可以作为当前流行的 Flash 技术的替代品。所以,HTML5 与 Flash 技术孰优孰劣的争论这两年就一直不断。
现在 Web 前端开发领域流行着不少 JavaScript 类库,如 YUI Library、Ext JS、Dojo Toolkit 等,其中一些封装了各种前端控件。这些控件的实现是基于 HTML4 的标准和复杂的 JavaScript 及 CSS 技术。但随着 HTML5 技术的发展,它的各种强大特性为这些控件的结构和功能提供新的设计和实现方式。因此,如何将 HTML5 的特性灵活的运用到前端控件开发就是本文的关注点。由于 HTML5 的特性很多,而流行性的 JavaScript 库中的前端控件也五花八门,本文只能举例说明。读者可以根据自身的需求结合 HTML5 中的特性开发出各种强大的前端页面控件。
HTML5 是新一代的 HTML 标准,它里面包含了很多 HTML4 中没有的新标签和应用程序接口,如 audio 标签、video 标签、矢量绘图、Web 套接字、离线数据存储等。这些新特性可以使 Web 页面具有更丰富的功能和更好的用户体验,其中的很多都可以用在网页控件设计上,从而使得网页上的内容更加丰富。在 HTML5 众多的功能中,有一个功能非常重要,它不仅是一项被众多网页设计人员期待已久的功能,而且为网页的功能和外观设计留下了巨大的空间,它就是 HTML5 中的矢量绘图。现在,不仅不少的业内人士将 HTML5 的矢量绘图视作 Flash 的挑战者,甚至连 Flash 的支持厂商 Adobe 都推出了基于 HTML5 矢量绘图的动画制作工具。本文后面将会介绍借助 HTML5 的矢量绘图技术实现 Dojo Widget。在此之前,为了帮助读者能够更好的理解本文的内容,这里先对 HTML 中的矢量绘图做一些简要的说明。HTML5 的矢量绘图的功能由 Canvas 标签和各种绘图 API 构成。在 JavaScript 的脚本中,通过 Canvas 节点可以获得绘图上下文,通过它调用 API 就可以绘制各种矢量图,如下所示。
<html> <head> <script> window.onload = function() { var canvas = document.getElementById('canvas1'); var ctx = canvas.getContext('2d'); ctx.fillRect(25,25,100,100); ctx.clearRect(45,45,60,60); ctx.strokeRect(50,50,50,50); ctx.beginPath(); ctx.moveTo(125,125); ctx.lineTo(205,125); ctx.lineTo(125,205); ctx.fill(); } </script> </head> <body> <canvas id="canvas1" height="600" width="600"> </canvas> </body> </html> Nach dem Login kopieren |
在上面的例子中,我们在一个 HTML 的文档中加入了一个 Canvas 标签,利用基于 JavaScript 的 API 来获得绘图上下文(Context),并在上面绘制了我们所要的图形。除了绘制 2D 图形,HTML5 还支持 3D 矢量绘图,它与 2D 的使用方式类似,此处不再详述。
值得注意的是,HTML5 还是一个发展的标准,至今并没有被所有主流浏览器全面支持。但是,即使是曾经是对 HTML5 支持较少的 IE 浏览器也会在新版本 IE9 中支持 Canvas 绘图等 HTML5 关键标签技术。所以相信在不久的将来,HTML5 的普及就会实现。
近些年,页面设计的易用性、功能性和交互性已经成为了业界的主流趋势。网页的功能越来越丰富,用户体验也越来越舒适。这一切都离不开前端以 JavaScript 和 CSS 为基础的 DHTML 技术的迅猛发展。但是,前端大规模的 JavaScript 和 CSS 开发的复杂度比较高,而且还要支持不同的浏览器平台,于是诞生了很多 JavaScript 库用来帮助前端开发者完成较为复杂的页面逻辑同时屏蔽浏览器的差异,如 jQuery、YUI Library、Ext JS 等。另外,越来越多的互联网公司也将自己的 JavaScript 库发布出来,如淘宝的 KISSY、豆瓣的 Do 等等。每种库都支持封装前端复杂的控件,如 jQurey UI、Dojo Widget 等,但方式却不相同。本文选取 Dojo Toolkit 作为控件实现的基础来介绍基于 HTML5 的控件的设计思想,当然,这种设计并不是只能在 Dojo Toolkit 上得到实现,其它的类库也可以作为实现基础。Dojo Toolkit 是当前页面前端开发领域流行的 DHTML 库,它不但包括丰富的页面基础功能,如 CSS 选择器、DOM 节点操作、动画效果等,还包括良好的面向对象的封装结构和以此为基础的 Dojo 控件技术 Dojo Widget(简称 Dijit)。Dojo Widget 中包含了对网页控件的生命周期管理,包括初始化渲染、属性映射、事件绑定、控件销毁等。清单 2 中给出了一个简单的 Dojo Widget 的实现。
dojo.declare("com.shy.widget.MyWidget", [dijit._Widget, dijit._Templated] ,{ templateString : "<div dojoAttachEvent=\"onclick:onClick\">${text}</div>", text : "", onClick : function() {alert('onClick');} }); Nach dem Login kopieren |
清单 2 定义的 Dojo Widget 会在页面上生成一个 DIV 标签并将属性 text 的值作为 DIV 中的内容。同时,一个 onclick 事件响应被绑定到这个 DIV 上。
Dojo Widget 的使用有两种方法:一种是通过 HTML 标记的方式将 Dojo Widget 添加到页面上;另一种是通过类型实例化的方式来初始化一个实例。清单 3 和清单 4 分别给出了这两种方法各自的例子。
<div dojoType="com.shy.widget.MyWidget" text="Hello World" /> Nach dem Login kopieren |
<div id="testNode" /> <script type="text/JavaScript" > var myWidget = new com.shy.widget.MyWidget({text:'Hello World'}, document.getElementById("testNode")); </script> Nach dem Login kopieren |
在一般的基于 Dojo 的工程项目中,除了 Dojo 自身提供的各式 Widget,开发人员会根据实际项目需要扩展 Dojo 提供的 Widget 或是重新开发新的 Widget。我们在后面的内容里将会在 Dojo Widget 框架的基础上,利用 HTML5 的非凡特性来实现新的 Widget。
如前文所述,HTML5 中包含了很多强大的特性,它们的普及和发展会给前端页面的控件技术带来巨大的变化。本文不去描绘这种改变将会是什么样子,而是举一个具体的例子来为读者掀 开未来的一角并由读者亲身品位。HTML5 中的很多特性都可以用于页面控件功能的实现,如前文提到的 Web 套接字、离线存储、拖拽、矢量绘图等。本文将利用 HTML5 中的 Canvas 矢量绘图来渲染 Dojo Widget 的视图,并在此基础上设计了属性映射和事件绑定。
当前,很多网站的页面都会在适当地方弹出一些对话框,图 2 所示是 Google maps 网站上的对话框,图 3 所示的是腾讯的 Web QQ 网站上的对话框。一般来讲,网页上的对话框都是通过 DIV 或是 Table 来进行布局。有的设计力求简洁,如图 2 中的对话框,只用一层 DIV 表示外框;有的设计则力求美观,如图 3 中的对话框,用了 9 个 DIV 来描述外框。页面上的对话框的外观设计的关键是边框的设计。以往的技术,如图 2 和图 3 都是利用 DIV 加一些式样和背景图片来实现对话框。但 HTML5 中的 canvas 给了我们另外一种实现页面上控件外观的手段,就是用矢量图将对话框的边框“画”出来,而不是通过 DIV“拼”出来。这样可以利用矢量图技术来为对话框增加各种新特性,比如对话框的阴影、圆角、渐变等各种效果,再比如特殊形状的对话框,如椭圆形,菱形 等等。此外,利用矢量绘图技术去“画”对话框的另一个好处就是可以很方便的调整大小和形状。例如要求设计一个椭圆形的对话框并且可以设置尺寸,如果没有矢 量绘图,或许还可以用椭圆背景图片来实现,但设置大小的需求就很难实现。所以 HTML5 中的矢量绘图确实能为页面前端控件的外观设计带来灵活性。我们这里会用 HTML5 的 Canvas 实现对话框控件。
利用 HTML5 我们可以画出图 4 所示的对话框的外观,包括标题栏和主体两部分,在标题栏的右侧还有一个关闭按钮。与上面例子中的对话框类似,我们也会使用两个 DIV 分别作为标题栏内容和主体内容的容器。得到的对话框 Widget 结构上会由三部分组成,分别是:绘制对话框外观的 Canvas、包含标题内容的 DIV 和包含主体内容的 DIV。
设计好对话框 Widget 的外观和结构后,接下来需要考虑如何为它绑定事件。图 2 和图 3 中的对话框中的每一个组成部分都是一个或几个 HTML 元素,换句话说就是可以对应到页面上的一个或几个 DOM 节点。比如 Google Maps 和腾讯 Web QQ 网站上的对话框中的关闭按钮都是 Anchor 元素,其所对应的 DOM 节点上可以直接绑定事件处理函数。但是,对于图 4 中的那个关闭按钮,则不能通过简单的 DOM 节点事件绑定来完成。为 Canvas 矢量图上的某个区域进行事件绑定,如为图 4 中的关闭按钮添加事件响应,需要首先监听 Canvas 节点的相应事件,再在事件处理函数中进行事件分发。同样以图 4 中的关闭按钮为例,要监听它的鼠标点击事件,需要监听 Canvas 的鼠标点击事件,在其回调函数中计算鼠标的坐标是否落入了关闭按钮的区域内,若是则调用关闭按钮的事件点击处理函数。
对于 Widget 外观矢量图上表示出的嵌套关系,如图 4 中的外层对话框包含里面的关闭按钮,更好的实践是将矢量图上的内容分成不同的实体进行封装,如可将外层对话框和里面的关闭按钮封装成不同的组件,这样整个 对话框就变成了一个组合控件。这种组合关就可以用树的结构来进行描述,并以此设计类似浏览器 DOM 树上的事件捕获和冒泡机制,如图 5 所示。因为 HTML5 的 Canvas 的矢量绘图不允许将事件响应绑定到矢量图中的某个具体图形上,所以图 5 中 Widget3 的鼠标单击事件处理需要从 Canvas 的鼠标单件事件处理中逐级分发,在事件分发的过程中加入事件捕获和事件冒泡的响应。
在 Canvas 上设计好 Widget 的外观后,就可以将其包装到 Dojo Widget 中,然后按照清单 3 和清单 4 中给出的方式来使用它。
做为一个提供良好面向对象封装的 JavaScript 类库,Dojo Toolkit 提供了完善的 Widget 封装机制用于创建各种控件,如 Dijit 中的 Form 表单控件、布局控件,Dojox 中的表格控件、颜色选项板控件等。这些控件实现的功能千差万别,却遵循同样的结构,可见 Dojo 所提供的 Widget 机制具有十分良好的适用性。一般来讲,每一个 Dojo Widget 都要继承 Dojo 中两个抽象类 dijit._Widget 和 dijit_Templated 并实现其中的一些方法。dijit_Widget 主要用于实现 Dojo Widget 的生命周期管理,dijit._Templated 用于实现 Widget 的视图渲染和属性映射,对于我们所要实现的 Widget 也会继承这两个接口。我们的 Widget 的视图主要有三部分组成,一个 Canvas 节点用于绘制外观,一个 DIV 节点用于容纳标题内容,一个 DIV 节点用于容纳主体内容,Canvas 节点中的矢量图作为两个 DIV 节点的背景。整个 Widget 的结构如图 6 所示。
在实现 Widget 结构的同时,利用 Dojo 提供的模板的机制,可以轻松的将属性设置反映到视图上。Widget 的结构定义和属性定义如清单 5 所示。
dojo.declare("com.shy.widget.DemoWidget",[dijit._Widget,dijit._Templated], { templateString : " <div style='position:relative;'> <canvas dojoAttachPoint='canvasNode' height='${height}' width='${width}' style='position:absolute' ></canvas> <div dojoAttachPoint='titleNode' style='position:absolute;top:10px;left:12px;'></div> <div dojoAttachPoint='containerNode' style='position:absolute; top:40px;left:12px;overflow:auto'></div> </div>", width : 200, height: 150, dialogTitle : "", }); Nach dem Login kopieren |
清单 5 中的 canvas 节点标记是 HTML5 中的新特性,利用 Canvas 我们可以绘制如图 4 所示的矢量图作为 Widget 的背景。基于 Dojo 所提供的 Widget 生命周期的机制,重载 dijit._Widget 的 postCreate 方法在里面绘制矢量图并调整一些结构式样,矢量图绘制的具体实现会在后面完整的 Widget 程序清单中给出。
dojo.declare("com.shy.widget.DemoWidget",[dijit._Widget,dijit._Templated], { templateString : <div style='position:relative;'> <canvas dojoAttachPoint='canvasNode' height='${height}' width='${width}' style='position:absolute' ></canvas> <div dojoAttachPoint='titleNode' style='position:absolute;top:10px;left:12px;'></div> <div dojoAttachPoint='containerNode' style='position:absolute; top:40px;left:12px;overflow:auto'></div> </div>", width : 200, height: 150, dialogTitle : "", postCreate : function() { this._drawDialog(this.width, this.height, this.canvasNode); this.inherited(arguments); dojo.style(this.titleNode, "height", 20 + "px"); dojo.style(this.titleNode, "width", (this.width - 54) + "px"); dojo.style(this.containerNode, "width", (this.width - 30) + "px"); dojo.style(this.containerNode, "height", (this.height - 60) + "px"); this.titleNode.innerHTML = '<font color=white>' + this.dialogTitle + '</font>'; }, }); Nach dem Login kopieren |
上面实现了 Widget 的视图,接下来我们要为它绑定两个事件响应。首先是对话框 Widget 的鼠标拖动,即为对话框 Widget 标题栏添加拖放功能。为了实现这个功能可在 canvas 的鼠标事件响应中进行处理,判断事件触发点是否落在标题栏的位置上并处理。另外,也可以直接在标题栏 DIV 节点上绑定事件处理。考虑到实现上的简单,我们采用第二种方式,将 Dojo 提供的拖放功能直接绑定到标题栏 DIV 节点上。然后是对话框的关闭按钮响应,即通过对话框上的关闭按钮来关闭对话框。实现这个功能只能通过在 canvas 的鼠标事件响应中判断触发点位置的方式。清单 7 中的完整的 Widget 代码就是在清单 6 的基础上添加了事件响应的内容并实现了矢量绘图的函数功能。
dojo.declare("com.shy.widget.DemoWidget",[dijit._Widget,dijit._Templated], { templateString : <div style='position:relative;'> <canvas dojoAttachPoint='canvasNode' height='${height}' width='${width}' style='position:absolute' ></canvas> <div dojoAttachPoint='titleNode' style='position:absolute;top:10px;left:12px;'></div> <div dojoAttachPoint='containerNode' style='position:absolute; top:40px;left:12px;overflow:auto'></div> </div>", width : 200, height: 150, dialogTitle : "", onClickListeners : [], postCreate : function() { this._drawDialog(this.width, this.height, this.canvasNode); new dojo.dnd.Moveable(this.domNode, {handle:this.titleNode}); this.inherited(arguments); dojo.style(this.titleNode, "height", 20 + "px"); dojo.style(this.titleNode, "width", (this.width - 54) + "px"); dojo.style(this.containerNode, "width", (this.width - 30) + "px"); dojo.style(this.containerNode, "height", (this.height - 60) + "px"); this.titleNode.innerHTML = '<font color=white>' + this.dialogTitle + '</font>'; }, _onCanvasClick : function(e) { for(var i = 0; i < this.onClickListeners.length; i ++) { if(this.onClickListeners[i].isInScope(e.layerX, e.layerY)) { this[this.onClickListeners[i].handler](); } } }, _onClose : function() { this.hide(); }, show : function() { dojo.style(this.domNode, "display", "block"); }, hide : function() { dojo.style(this.domNode, "display", "none"); }, _drawDialog : function(width, height, canvasNode) { var canvas = canvasNode; var ctx = canvas.getContext('2d'); ctx.beginPath(); ctx.shadowOffsetX = 4; ctx.shadowOffsetY = 4; ctx.shadowBlur = 8; ctx.lineWidth = 7; ctx.strokeStyle = '#fff'; ctx.shadowColor = 'rgba(0, 0, 0, 0.25)'; ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; drawBody(ctx,7,7,width - 20,height - 20,15); drawCloseButton(ctx, width - 20 - 10, 20); this.onClickListeners.push({isInScope: function(x, y){ return (x - width + 20 + 10) * (x - width + 20 + 10) + (y - 20) * (y - 20) < 49 }, handler: "_onClose"}); function drawBody(ctx,x,y,width,height,radius) { drawRoundedRect(ctx,x,y,width,height,radius); ctx.fill(); ctx.fillStyle='#fff'; ctx.strokeStyle = '#fff'; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 4; ctx.shadowBlur = 8; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(x,35); ctx.lineTo(x + width,35); ctx.closePath(); ctx.stroke(); } function drawRoundedRect(ctx,x,y,width,height,radius){ ctx.beginPath(); ctx.moveTo(x,y+radius); ctx.lineTo(x,y+height-radius); ctx.quadraticCurveTo(x,y+height,x+radius,y+height); ctx.lineTo(x+width-radius,y+height); ctx.quadraticCurveTo(x+width,y+height,x+width,y+height-radius); ctx.lineTo(x+width,y+radius); ctx.quadraticCurveTo(x+width,y,x+width-radius,y); ctx.lineTo(x+radius,y); ctx.quadraticCurveTo(x,y,x,y+radius); ctx.stroke(); } function drawCloseButton(ctx, x, y) { ctx.fillStyle="#ff0000"; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(x, y, 7, 0, Math.PI*2, true); ctx.closePath(); ctx.stroke(); ctx.strokeStyle = '#ff0000'; ctx.beginPath(); ctx.moveTo(x - 5 * 0.707, y - 5 * 0.707); ctx.lineTo(x + 5 * 0.707, y + 5 * 0.707); ctx.moveTo(x - 5 * 0.707, y + 5 * 0.707); ctx.lineTo(x + 5 * 0.707, y - 5 * 0.707); ctx.closePath(); ctx.stroke(); } } }); Nach dem Login kopieren |
在上面的小节中,我们介绍了对话框 Dojo Widget 的实现。对于这样的 Dojo Widget,前面介绍过可以通过两种方式来使用,一种是通过 HTML 标记的方式,另外一种是通过类型实例化的方式。对话框 Widget 是一个容器控件,可以在包含其它的 Widget 或是 HTML 标记在里面。清单 8 给出了在 HTML 页面上通过标记的方式使用对话框 Widget 的例子。
<div dojoType="com.shy.widget.DemoWidget" width="260" height="220" dialogTitle="I am title" > <input id="text1" dojoType="dijit.form.TextBox" value="shaoyu"></input> <button id="button1" dojoType="dijit.form.Button" >Submit</button> </div> Nach dem Login kopieren |
清单 8 最终会在页面上生成如图 7 所示的对话框。
在实际应用中,本文介绍的对话框 Widget 的设计和实现可以作为原型来进一步增强和扩展,以满足实际项目的需求。而这种结合了 HTML5 新特性的 Widget 设计方法和实现思路则可以运用到很多应用场景中。
本文通过例子介绍了基于 HTML5 的 Dojo Widget 的设计思想和实现方式,利用 HTML5 中的 Canvas 特性和 Dojo 的 Widget 机制创建了一个对话框 Widget。相较于传统的基于 HTML4 和 CSS2 的 Widget 设计和实现,基于 HTML5 技术的 Widget 具有很多天然的优势和良好的特性。虽然现阶段 HTML5 尚未得到广泛的支持,但相信市场对 HTML5 中各种新特性的需求会驱动 HTML5 的迅速普及,届时会有各种基于 HTML5 功能的 Widget 出来,将我们的页面装饰的更加丰富多彩。