在一般的Web
GUI
中,每個應用都分散在一個頁面中,會随着頁面的跳轉而反映在浏覽器的位址欄上;稍微複雜的基于Web
系統中,都采用劃分Frame
元素或打開浏覽器新視窗的方式來組織頁面,從浏覽器的位址看起來,雖然隻有一個位址,但是子Frame
的頁面還是會整張頁面地重新整理。AJAX
改變了以往一張頁面一次請求的模式,可以允許在同一張頁面發起各種的請求,這樣我們對于頁面的組織形式有了新的途徑。在單頁面GUI
模型中,首頁面是可以獨立加載、更新和替換的一些可視元素的組合。通過這種方式,可以不必在每次使用者操作後重新加載整個頁面。在任何時候,都隻顯示與應用程式目前階段相關的可視元素和内容。其他所有内容均被隐藏;但隻要應用程式流程中需要用到它,它就會顯示出來。
單頁面GUI
與傳統頁面組織的本質差別在于,單頁面隻允許一個document
對象存在,一切UI
元件的根節點就是這個document
對象,程式所有
UI
的渲染任務均在這個頁面内完成,即使是表單送出的任務也不需要作頁面轉向(Redirect
)。圖一是單頁面GUI
的對象組織示意圖:

Ajax
技術的出現給頁面帶來了一些變化,其中最直覺的莫過于站點的頁面上出現越來越多的“loading
…”
,“正在加載中……”等提示資訊,忽如一夜春風來,loading
加載處處開的意思。“loading
或者“正在加載中……”表示浏覽器正在與伺服器之間進行互動,互動完成之後,将對頁面進行局部重新整理,這種互動模式雖然簡單卻極大的提高了Web
應用的使用者體驗,使單頁面GUI
的設計成為可能。
為我們帶來了什麼?
的定義是相對于多頁面概念(Multi-page
)的,兩者的對比略舉如下:
多頁面相對更容易。多頁面的設計是你熟悉的開發理念,而且浏覽器前進、後退或收藏的問題是從來不用考慮的
多頁面載入的時間更快。把程式的每一個子產品分散的多個頁面中,浏覽器負荷小,加載時間短
、Roll-your-own
。
單頁面提供更快速的渲染
單頁面使得共享UI
元件更輕松。元件定義在同一頁面中,調用更友善
的應用情況
曾經有一個實際案例是,把74
張JSP
頁面轉化為單個頁面,利用Java
下常用的Ajax
架構DWR
向負責背景的通訊,結果是450
行的HTML
和200
行的CSS
。有許多的Ajax
程式采用單頁面的方案,典型的有Google
下的一系列線上應用,比如GMail
、Reader
、Maps
等,
雅虎的Oddpost
和微軟的Kahuna
、Start
等。以上的應用都是大規模的Web
應用,如果在實際的項目開發中,單頁面GUI
很難說一定比多頁面的設計好,因為在傳統開發模式大背景下,多數開發者希望利用IDE
或類似WinForms/Swing
的GUI
“
畫出”控件這樣強大的支援來解決表示層的方案,以提供工作效率;另一方面,大量JavaScript
投入項目産生,開發者會因JavaScript
的困惑而對項目總體把握度而大打折扣,是以開發人員對于哪些邏輯可以在用戶端執行應該有一個清醒的認識。單頁面實施起來可以說難度更高,但從後期的維護工作量和使用者體驗來講,效果會更好。
動态資源下載下傳
意味着頁面内所有功能會重新地被規劃安排。通常,這個規劃過程會被開發人員了解為所必需的子產品化,因為通過其可以友善地進行調試和編碼。當網頁内的各種資源,包括HTML
、圖檔、腳本程式等的資源數量或體積特别大的時候,我們必須采取一定政策來優化資源的加載,例如HTML
内容過長我們可以分頁,圖檔體積過大可以采用更高壓縮比的格式或縮小尺寸等的手段優化,進而在同一時刻内,使得浏覽器保持在一個合理的資源調控,和帶來較好的使用者體驗。另一個方法減少初始下載下傳是,對頁面的内容部分進行延遲加載。
腳本程式屬于網頁資源中特殊的一種。傳統多頁面的設計下,每個頁面需要的腳本檔案會按照功能上劃分而有所不同。這就需要使用者在頁面上手動加入标簽裝載指定的腳本資源。随着腳本數量的增多,如果都要為這些檔案去管理腳本的引用标簽,組織頁面,将會是一件痛苦的事情,尤其使用者在不清楚類庫之間的依賴關系、加載先後順序的情況下,更容易出現錯誤。最直截了當的解決辦法是将常用的類庫資源打包到單一的架構檔案中,作為一個完整的加載資源出現。Ext
也是采用這種政策(ext-all.js
)。實際上,如果采用單頁面GUI
的方案(One
Page One
Application
,簡稱OPOA
),——即所有的任務均在同一頁面内完成的方案。相對而言,這種方案即使不過多關心如何按需加載的,也是情有可原的。當然從頁面運作效率而言,完整加載的方式是有害的,因為同一個頁面上基本不會用到所有的元件,簡單說,使用者有80%
的功能不會用到“那部分”的函數,浏覽器卻加載了,造成了不必要的資源、帶寬(Bandwidth
)占用。
當今JavaScript
發展在加快,體積也随着加大,在功能與體積相沖突的情況下,按需加載是行之有效的政策方式。按需加載又稱動态加載、On-Demand
加載,意思都是相近的。目前按需加載常采用的主要有三種方式:
1. 即時同步加載式:
此加載方式是利用XHR
(XMLHttpRequest
)對象,設定Open()
方法的第三個參數為true
,設定通過同步方式下載下傳腳本。若采用了同步(synchronous
)通訊的設定,浏覽器在内容未下載下傳完畢之前,此時的readyState
狀态屬性是2
、3
之間,是一直處于等待的狀态,渲染其他網頁元素的任務亦伴随停止,包括渲染DOM
元素、加載圖檔、停止響應使用者事件等的任務。是以,在加載所需JavaScript
檔案剛好是比較慢的網絡環境,時間一長浏覽器就會變得好像僵硬(Freeze
)的狀态,甚至最大化、最小化浏覽器的操作使用者都難以控制。雖然即時同步加載方式的算法實作起來不太困難,但主要的弊端是在非内網下擷取資源時極容易導緻浏覽器的阻塞,尤其在網絡速度較慢的情況下。使用此方式的庫有早期的Dojo
、JSVM
等。
2.
異步加載式:
異步加載式同樣是使用XHR
對象進行資源的加載,但通訊方式改為異步。其特征是使用了eval
函數執行腳本。使用此方式加載需要指定函數所處于的作用域鍊(Scope
Chain
),因為我們知道eval
的做法。另外,由于采用了異步加載的方式,處理庫内部之間的依賴關系(dependency
)會變得比較複雜,典型的庫有新版Dojo
1.x
中的包加載機制。
3.
異步加載式之動态标簽:
有時候,僅在使用者呼叫出某個功能的這個時候才加載應用程式的相應内容。這樣就把若幹的功能組合成為一個大的子產品。我們不是把單個功能分散都做異步下載下傳,因為這樣的劃分顆粒度過于細小了,而是把若幹的功能組合在一起,使用者一觸發UI
的事件就動态下載下傳所屬子產品,已經下載下傳過的就不再重複。利用DOM
動态載入外部JavaScript
檔案也是一種解決之道,這樣的做法會更适合單頁面GUI
的設計。
具體地說,我們首先把每一個子產品都劃分一個單獨的腳本、CSS
資源,比如部落格子產品、論壇子產品、商城子產品等等……規劃好之後,用EXT
制成一個全局的導航布局,把對應的子產品功能都放在布局上。這裡的安排并不是把所有的子產品都給放上,而隻是列出對應的菜單、對應的按鈕……好了,有了這些菜單、按鈕,我們的設計目的是,隻要有使用者按下這些控件的時候,浏覽器就會下載下傳那塊功能對應的資源;如果是重複的就不用二次下載下傳(腳本應能識别使用者操作哪些是重複的)。
按需下載下傳封包件
前面
談到的動态資源下載下傳方式有三種,與前兩點比較,第三點是重點,也是本應用執行個體所使用原理。之前我們從最基礎的内容說起,現在我們就在loadContent()
的原理基礎上再進一步擴充,形成moduleLoader
類:
/**
* 前端子產品異步加載器。
* 依賴ext的createCallback、createDelegate函數
* @class moduleLoader
* @extends Object
* @constructor
* @param {Object} config 配置屬性對象
*/
moduleLoader = function(config){
// 複制屬性到目前執行個體
Ext.apply(this, config);
/**
* @property {Object} action 該子產品的處理函數,類型為hash
*/
* @property {Array} script 要異步下載下傳的腳本清單
* @property {Array} style 要異步下載下傳的樣式清單
*/
/**
* @propety {Boolean} loaded True表示目前資源已加載到浏覽器渲染。此項是為了不會重複下載下傳資源時的判讀根據。隻讀的。Read-Only
*/
this.loaded = false;
// 鑒于下面使用的createCallback()方法沒有指定scope的地方,是以在這裡先綁定
var moduleDetect = this.moduleDetect.createDelegate(this);
for (var i in this.action) {
var oldFn = this.action[i]; //原有的函數
// 函數作為值送入createCallback方法,傳回的類型是Function。作用是建立回調函數。
this[i] = moduleDetect.createCallback(oldFn);
}
}
// 執行個體方法
moduleLoader.prototype = {
* @private
* @param {Fucntion} onSuccessHandler “按需加載”完成後的回調函數
* @param {Object} Scope 作用域
moduleDetect: function(onSuccessHandler, scope){
if(this.loaded === false) {
moduleLoader.load({
script : this.script
,style : this.style
,onSuccess : onSuccessHandler
});
this.loaded = true;
}
else {
// 如果已經下載下傳過直接執行。
onSuccessHandler.call(scope);
moduleLoader
還需要依賴一個靜态方法load()
,負責加載腳本、樣式。此方法是靜态的是以也可以獨立的使用。
* 局部加載JS或CSS檔案
* @static method
* 靜态用法:
* <code>
ModuleLoader.prototype.load({
script : ['/ajaxee/test.js','/ajaxee/test2.js'],
style : ['/ajaxee/test.css']
})</code>
* @cfg {String} path The URL to request
* @cfg {Function} onSuccess
* @cfg {Object} scope
moduleLoader.load= function(path){
var dom;
if(path.script){
if(!path.script.pop)path.script = [path.script];
for (var i = 0, j = path.script.length; i < j; i++) {
dom = document.createElement("script");
dom.src = path.script[i];
document.getElementsByTagName("head")[0].appendChild(dom);
}
// 相容IE、非IE浏覽器的判斷
dom[Ext.isIE ? "onreadystatechange" : "onload"] = function(){
if (this.readyState && this.readyState == "loading")
return;
dom = null;
if(path.onSuccess)path.onSuccess.call(path.scope, path.script);
}
if(path.style){
if(!path.style.pop)path.style = [path.style];
for (var i = 0, j = path.style.length; i < j; i++) {
dom = document.createElement("link");
dom.type = "text/css";
dom.rel = "stylesheet"
dom.href = path.style[i];
}
熟悉了上面相關的類之後,下面我們以某個OA
項目中的位址簿為例子,建立該子產品的功能管理者AppMgr
。我們隻要執行個體化一次(頭一次加載成功後就不再加載了),儲存到全局變量中,也就是“單例”的形式建立對象。實際上這也是對該子產品下各個功能先作一個配置設定。有了這種前期的思路後,我們寫好的這個AppMgr
執行個體便是封裝每個子產品的公共屬性和方法(如例子OA.Client.AddressBook.AppMgr
中的action
),而且還要定義子產品所需的資源檔案,說明按需加載的JS/CSS
檔案是哪些。
OA.Client.AddressBook.AppMgr = new moduleLoader({
script: [
'Client/AddressBook/panel.js',
'Client/AddressBook/windows.js'
],
style: [
'Style/AddressBook/default/AddressBook.css'
// 各種UI行為,開發者自己定義……
action : {
openFrontPage: function(){
App.mainTabPanel.addTab(new OA.Client.AddressBook.frontPage(), true);
},
openMainGrid : function(){
App.mainTabPanel.addTab(new OA.client.Portal.masterGrid());
openCommentWindow: function(){
(new Ext.Window({
title: "測試對話框",
iconCls: 'AppIcon_Comment_16x16',
resizable: false,
autoDestroy: true,
closeAction: 'close',
width: 610,
height: 400,
items: new OA.Client.Comment.Browser()
})).show();
},
openConfigWindow: function(){
var Index = new OA.Client.Admin.frontPage();
App.mainTabPanel.addTab(Index, true);
Index.show();
}
}
});
有了這個單例後,我們就可以在UI
上面配置設定該子產品的各種方法。大多數Web
系統都會包含功能菜單和顯示頁面,功能菜單可以是UI
左面的一棵樹,也可以是一個可以切換的踏闆标簽頁,而顯示頁面無非就是一塊顯示得區域,點選相應的功能菜單,切換不同的内容。現在我們可以結合到一個按鈕上、結合到菜單上、結合到樹上……總之可以制定事件的地方就可以配置設定觸發該功能的函數。下面我們就以一個樹的根節點為示範例子,說明如何配置設定UI
子產品:
// 建立樹的根節點
var rootNode = new Ext.tree.TreeNode();
rootNode.appendChild(new Ext.tree.TreeNode({
iconCls :'oa-tree-myDesktop-Admin', //圖示樣式
// 登記點選該節點時的事件
listeners: {
'click': OA.Client.AddressBook.AppMgr['openFrontPage'] // 該值的類型是Function函數。是以可以将功能配置設定給目前click事件的處理函數
},
text: '考勤事務'
}));
'click': OA.Client.Admin.AppMgr.openFrontPage
// …………更多的樹節點
小結
我們還本文中嘗試在Ext實作“單一頁面”的程式設計。通過内嵌頁面iframe和傳統的頁面跳轉方法,雖然可以實作資料的定位或功能的切換,但是遺憾的是在全面引入AJAX方案後這樣的方式不夠強大和靈活。本文同時也向大家介紹如何在單頁面的基礎上提供非跳轉或iframe的GUI設計,以提供更合理的使用者體驗及徹底的按需加載方案。
一書的全面介紹。