若干年前,很少有人會想到一家生產電腦的公司會憑借一款功能設計上存在著不少缺陷的手機在市場上取得巨大的成功。也很少有人會想到一家曾經 占據著智能手機大部分市場份額的廠商會落入今天這樣舉步維艱的局面。人們不禁驚訝的發現,精美的界面、方便的操作對于消費者的吸引力要大于完善的功能及穩 定的系統。用戶體驗的優劣在一定的程度上決定了一個面向終端用戶的產品能否在市場上生存。移動設備的設計如此,互聯網應用的設計亦如此。現在,互聯網上充 斥著各種精美的 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 就可以繪制各種矢量圖,如下所示。
清單 1. 利用 HTML5 Canvas 繪制的矩形和三角形
<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> |
圖 1. HTML5 的 Canvas 繪制的矩形和三角形
在上面的例子中,我們在一個 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 的實現。
清單 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');} }); |
清單 2 定義的 Dojo Widget 會在頁面上生成一個 DIV 標簽并將屬性 text 的值作為 DIV 中的內容。同時,一個 onclick 事件響應被綁定到這個 DIV 上。
Dojo Widget 的使用有兩種方法:一種是通過 HTML 標記的方式將 Dojo Widget 添加到頁面上;另一種是通過類型實例化的方式來初始化一個實例。清單 3 和清單 4 分別給出了這兩種方法各自的例子。
清單 3. 通過 HTML 標記的方式使用 Dojo Widget
<div dojoType="com.shy.widget.MyWidget" text="Hello World" /> |
清單 4. 通過類型實例化的方式使用 Dojo Widget
<div id="testNode" /> <script type="text/JavaScript" > var myWidget = new com.shy.widget.MyWidget({text:'Hello World'}, document.getElementById("testNode")); </script> |
在一般的基于 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 實現對話框控件。
圖 2. Google Maps 網站上彈出的對話框
利用 HTML5 我們可以畫出圖 4 所示的對話框的外觀,包括標題欄和主體兩部分,在標題欄的右側還有一個關閉按鈕。與上面例子中的對話框類似,我們也會使用兩個 DIV 分別作為標題欄內容和主體內容的容器。得到的對話框 Widget 結構上會由三部分組成,分別是:繪制對話框外觀的 Canvas、包含標題內容的 DIV 和包含主體內容的 DIV。
圖 4. HTML5 Canvas 上畫出的對話框外觀
設計好對話框 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 的鼠標單件事件處理中逐級分發,在事件分發的過程中加入事件捕獲和事件冒泡的響應。
圖 5. 組合模式的 Widget 的事件捕獲和冒泡
在 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 所示。
圖 6. 所要實現的 Widget 的結構
在實現 Widget 結構的同時,利用 Dojo 提供的模板的機制,可以輕松的將屬性設置反映到視圖上。Widget 的結構定義和屬性定義如清單 5 所示。
清單 5. 所實現的 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 : "", }); |
清單 5 中的 canvas 節點標記是 HTML5 中的新特性,利用 Canvas 我們可以繪制如圖 4 所示的矢量圖作為 Widget 的背景。基于 Dojo 所提供的 Widget 生命周期的機制,重載 dijit._Widget 的 postCreate 方法在里面繪制矢量圖并調整一些結構式樣,矢量圖繪制的具體實現會在后面完整的 Widget 程序清單中給出。
清單 6. 實現 Widget 的 postCreate 方法
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>'; }, }); |
上面實現了 Widget 的視圖,接下來我們要為它綁定兩個事件響應。首先是對話框 Widget 的鼠標拖動,即為對話框 Widget 標題欄添加拖放功能。為了實現這個功能可在 canvas 的鼠標事件響應中進行處理,判斷事件觸發點是否落在標題欄的位置上并處理。另外,也可以直接在標題欄 DIV 節點上綁定事件處理。考慮到實現上的簡單,我們采用第二種方式,將 Dojo 提供的拖放功能直接綁定到標題欄 DIV 節點上。然后是對話框的關閉按鈕響應,即通過對話框上的關閉按鈕來關閉對話框。實現這個功能只能通過在 canvas 的鼠標事件響應中判斷觸發點位置的方式。清單 7 中的完整的 Widget 代碼就是在清單 6 的基礎上添加了事件響應的內容并實現了矢量繪圖的函數功能。
清單 7. 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 : "", 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(); } } }); |
在上面的小節中,我們介紹了對話框 Dojo Widget 的實現。對于這樣的 Dojo Widget,前面介紹過可以通過兩種方式來使用,一種是通過 HTML 標記的方式,另外一種是通過類型實例化的方式。對話框 Widget 是一個容器控件,可以在包含其它的 Widget 或是 HTML 標記在里面。清單 8 給出了在 HTML 頁面上通過標記的方式使用對話框 Widget 的例子。
清單 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> |
清單 8 最終會在頁面上生成如圖 7 所示的對話框。
圖 7. 由 Widget 生成的對話框
在實際應用中,本文介紹的對話框 Widget 的設計和實現可以作為原型來進一步增強和擴展,以滿足實際項目的需求。而這種結合了 HTML5 新特性的 Widget 設計方法和實現思路則可以運用到很多應用場景中。
本文通過例子介紹了基于 HTML5 的 Dojo Widget 的設計思想和實現方式,利用 HTML5 中的 Canvas 特性和 Dojo 的 Widget 機制創建了一個對話框 Widget。相較于傳統的基于 HTML4 和 CSS2 的 Widget 設計和實現,基于 HTML5 技術的 Widget 具有很多天然的優勢和良好的特性。雖然現階段 HTML5 尚未得到廣泛的支持,但相信市場對 HTML5 中各種新特性的需求會驅動 HTML5 的迅速普及,屆時會有各種基于 HTML5 功能的 Widget 出來,將我們的頁面裝飾的更加豐富多彩。