一、引子
上一篇討論了關于客戶端數據處理的一些問題,以簡單的用例場景的方式描述了出來。很明顯,要想實現一個功能完整的Rich客戶端的話,必須能夠滿足上述用例場景的需求。能否根據這些需求做出合理的設計,是一個挑戰。尤其對于設計而言,不同的人有著不同的風格,而且由于背景不同,也會有不同的見解。本文中,我只是陳述出自己的一些想法和設想,更多的是希望能夠拋磚引玉,通過在這個方面的討論也能增進我的理解。呵呵。
很顯然,blog的形式更適合作為思路的介紹以及探討的平臺,而不是詳細設計的文檔。而且很明顯這一篇文章是承載不了所有的詳細設計的。我爭取把我在各個細化的方面的想法在后續的文章里面發出來。如果時間允許的話,整理出初始的文檔和代碼,建立一個小的開源項目未嘗不可(因為如此,所有的JS都是采用英文來注釋──其實還有一個原因是練習英文 :))。這都是后話了。
二、縮略語
-
RIA:Rich Internet Application的縮寫。RIA是擁有傳統本地應用的功能和效果的Web應用。RIA一般把UI相關的處理交給了Web客戶端,但是大量的數據(包括應用的狀態、數據等)還是交給服務端處理
-
Ajax:又寫為AJAX,是"Asynchronous JavaScript and XML"的縮寫。是一種使用瀏覽器技術進行RIA開發的技術
- Prototype:開源的優雅的Ajax以及JavaScript基礎擴展框架。由于被Rails采用而聞名,使用相當廣泛
- jQuery:功能類似Prototype + script.aculo.us的開源Ajax框架。據我所知,Google用得比較多
- Ext:對YUI的一個擴展的UI構件庫。經過改進后,可以使用jQuery或者Prototype作為基礎。目前好像正在完善自身的基礎Ext base。以優秀的構件聞名。雖然目前仍然屬于商業構件庫,但是License相對寬松,一般的商業應用可以免費使用
-
dojo:由dojo foundation管理的一個開源JavaScript框架。提供了很好的JavaScript擴展,目前被IBM和Sun等大公司支持和使用
-
Json: JavaScript Object Notation的縮寫。它是一種純文本的數據對象傳輸協議,在Ajax的應用中被廣泛采用
-
CRUD:Create(創建)、Retrieve(獲取)、Update(更新)和Delete(刪除)這四種簡單的數據操作的縮寫
-
Form:本文中的Form指的是經過Ajax擴展的簡單的HTML電子表單。表單內部可以擁有很多如ComboBox、TextBox等構件。一般來說,一個表單對應著一個業務的數據對象
-
Tree:樹形顯示結構化數據的構件,由于數據是高度結構化的,往往可以采用懶加載等技術來提高性能
-
Grid:以表格形式顯示和編輯數據的UI構件。一般分為表頭(標題欄)、表身(數據列表)和表尾(合計、狀態顯示等)三個部分。其中,表頭可以是復合的表頭,而表身可以是復合的格式(Tree、Grid、ComboBox、CheckBox等)。一個Grid可以有一個復雜的Grid定義
-
Enhanced Grid:下面簡稱為EGrid,是Grid的擴展。在Grid的功能基礎之上提供了數據獲取和數據持久化的能力,可以大大的減少開發應用的時間(Ext中的Grid可以認為是一個簡化了的EGrid)
-
CodeList:代碼表,一般用在下拉列表數據處,在系統的實現中,由于性能以及標準的要求,下拉列表一般都是采用代碼保存數據,但是用戶在填寫表單的時候需要看到正常的文字而不是代碼,這就需要系統通過CodeList進行文字和代碼的轉換
-
LRU:Least Recent Used的縮寫,是一種簡單的緩存策略,如果緩存已滿,那么就釋放最近最少使用的那個緩存數據
-
REST:是REpresentational State Transfer的縮寫,是一種基于輕量級WebService協議
- JDBC:是Java DataBase Connectivity對縮寫,是基于Java的數據庫連接規范。
三、基礎
既然決定采用Ajax,就最好采用一個基礎,在這個有很多優秀的Ajax框架可供選擇的時代,誰要是還要赤手空拳的來寫Ajax應用,就未免有點兒過于勇敢,而近于魯莽了。這篇文章不是Ajax框架對比文章,我不打算在這里一一列舉各個流行的Ajax框架的優缺點,我只拿下面幾個進行討論,dojo、Prototype、jQuery、Ext。
先提需求,框架應該:
- 以一種形式支持面向對象(畢竟開發人員多數都是Java程序員,更有可能的是,他/她只對Java熟悉)
- 以一種方式來支持命名空間和分包機制(開發企業應用與開發網站不同,復雜的不是技術而是業務。沒有命名空間和分包支持,對于大型應用,基本不可控制。──設想一下如果Java沒有package關鍵字會怎樣)
- 有完善的模塊封裝與管理規范或者最佳實踐,能夠自動處理模塊間的依賴則更好(模塊的定義不在這里解釋了,一個例子說明吧,一個Jar就是一個模塊。模塊化和構件化是實現、維護和管理大型應用的重要手段)
- 提供豐富的調優選項,使得對復雜的應用調優是可能的(這一點對于UI層尤為重要,因為它直接面對使用者)
- 便于調試(這一點對于開發者致關重要)
綜合上面幾點,我選擇dojo作為基礎。Sorry Prototype。
四、整體構架
整體構架如下圖所示(為了便于理解UI構件與其對關系,我把需要數據處理的UI構件也加了上去,圖中藍背景色的部分就是):
一目了然,整個客戶端數據處理由3個部分構成,DataSource、DataSet和DataStore。下面分別進行簡單的介紹。
五、DataSet
DataSet的概念很簡單,就是數據對象的集合。它的構架如下圖所示(注意:DataSet只是一套標準的API,可以有不同的實現——比如XML形式的):
如圖所示,DataSet由四個部分構成,Record Set(數據集合)、Change Tracker(數據變更追蹤)、Meta Data(數據對象源信息)和Cursors(游標API)。
分別介紹如下:
- Record Set:Record Set就是Record的集合,一個Record就是一個數據對象,它由一系列的屬性(Property)構成,屬性是一個簡單的字符串,其對應的值可以是任意類型。Record提供屬性讀寫的方法,可以直接在Record上對屬性進行讀寫,并且,Record會為寫操作提供一個事件。Record Set內部的元素必須是Record,Record Set支持對內部Record的CRUD操作。并且會有相應的事件觸發
- Change Tracker:ChangeTracker為DataSet提供所有寫操作的追蹤,可以通過配置選擇Change Tracker是否記錄修改,如果記錄修改,采用的是針對每一個Record記錄增加、刪除以及針對每一個屬性記錄First和Last修改的策略
- Meta Data:提供DataSet中Record的元數據(屬性名、屬性對應類型、其他自定義數據——校驗、Form Label信息、表頭信息等)以及DataSet的元數據(在全部數據中的位置等)。
- Cursors:Cursor其實就是Record的迭代器,可以通過對dataset查詢(一般都是非常簡單的filter或者range)得到,推薦通過Cursor進行Record訪問,而不是通過Index,因為通過迭代器訪問Record,DataSet擁有更多的能力。雖然通過Cursor完全可以訪問所有的Record及其中的數據,但是由于DataSet擁有MetaData,所以還是采用DataSet作為數據綁定的主體
DataSet是整個客戶端數據處理構架的核心,所有的數據訪問都應該通過DataSet的API。這樣客戶端的數據處理才是統一的一個整體。
解決的用例問題:
- 數據綁定:Form綁定、Grid綁定
- 數據變更追蹤:Grid變更提示、數據集合變更追蹤、Form修改的追蹤
- 數據訪問:數據對象元數據信息訪問、數據寫操作的統一事件定義、統一的數據讀取方式
六、DataSource
DataSource的功能是提供對數據進行查詢以及數據的持久化(持久化到客戶端或者服務器端)。與DataSet一樣,不同的格式數據就會有不同的DataSource,一種DataSource代表了一種客戶端與服務端的數據傳輸協議。但是由于DataSource的邏輯與結構過于復雜(事實上,DataSet的實現也完全依賴DataSource的實現,可以類比JDBC),不應該對每一種格式都重新實現一個DataSource,由此,DataSource不應該是一套標準的API,而是使用了Adapter模式,來滿足不同的數據來源,其構架如下圖所示:
看上去很簡單?實際上這是最復雜的部分,關于這個部分,至少可以再寫3篇文章來詳細探討,在本文中,就不過度討論了。:)
如圖所示,DataSource由Cache、Query、Persist以及Providers構成,分別介紹如下:
- Cache:Cache的概念在前面已經闡述了,不在這里多說了。在這里簡單的介紹一下客戶端Cache的策略以及技術,由于基于瀏覽器的數據緩存技術非常重要,擬在后續的文檔《客戶端的緩存策略與相關技術》中對其進行詳細討論
- 緩存策略(這一方面客戶端數據的緩存與服務端的數據緩存考慮的問題應該是類似的,唯一的區別是,客戶端的數據緩存不必考慮集群支持。:))
- LRU:基礎的Cache算法,如果緩存已滿時,根據最近很少使用算法來確定哪些對象需要被清除
- Priority:按照優先級高低來清理緩存空間的策略,當緩存已滿時,按照優先級高低來確定哪些對象需要被清除,可以與LRU算法共存
- Refreshable:有時效性的數據,或者在運行時有可能會被修改需要同步或者刷新的數據。可以設置數據過期時間,到時間則數據處于stale狀態,再度訪問該數據時,如果不能重新獲得該數據,則報錯
- Expireable:臨時性數據,可以設置失效時間,到時間則數據失效,在緩存需要清理時,緩存會清理掉這些失效的對象
- 緩存持久化(屬于高級的緩存策略,依賴客戶端基于JavaScript的數據存儲技術)
- Save(持久化):把緩存當中的數據持久化到本地或者服務端,其用處如下
- 通過把數據持久化可以增加Cache的容量
- 數據本地緩存提供了離線表單填寫的能力
- Retrieve、Delete:對緩存中數據的基本操作
- Sync:離線緩存的本地數據,可以與遠程服務端進行同步,從而實現離線表單提交以及實現離線CodeList等功能
- 緩存技術
- Client
- Server(服務端協助客戶端做一些緩存,這在傳統的B/S下是匪夷所思的設計,在RIA的情況下卻未嘗不是一種手段)
- WebService或者REST
- Post + Get
- Query:其實Query只是一個方法,由于有可能會向服務端發XmlHttpRequest,所以這個方法需要回調方法。其參數應該是一個Query對象,一個配置對象,有一些事件的關鍵字(onBegin、onError、onSuccess等)。Query對象以及配置對象的格式在本文中就不詳細說明了,留給以后慢慢說(因為這個部分是我最喜歡的部分)。很明顯,如果查詢成功,查詢的結果應該是DataSet,一般來說,DataSource是DataSet來源。不同的DataSource,查詢的語言未必相同。不一定所有的查詢對象都支持SQL語法(比如可以支持HQL或者XQuery啊),而且我最推薦采用的是服務端提供一個服務抽象層,接受Json格式的查詢請求,并返回Json格式的數據
- Persist:數據持久化。只是Save和Update兩個方法。接受DataSet和配置對象作為參數(這兩個方法也應該是異步的)。如果DataSet里面沒有足夠的元數據信息,需要在配置對象里面提供這些信息。這個部分不比Query部分復雜
- Providers:數據的來源,提供了Query和Persist API的實現,是DataSource的底層實現,它使DataSource的實現可以跨不同協議而使用。給DataSource提供不同的Provider,DataSource就可以支持不同的數據傳輸協議。
DataSource是數據查詢和持久化的核心,所有的客戶端的數據查詢和持久化基本都應該采用DataSource,從而獲得DataSource提供的強大而靈活的特性。
解決的用例問題:
- 數據緩存:離線運行的CodeList、離線表單填寫、性能考慮
- 數據查詢:樣本查詢、模糊查詢、Range查詢、邏輯查詢、自定義查詢
- 數據處理:數據處理事件、過濾、排序
七、DataStore
DataStore基于DataSource和DataSet的實現,實現了dojo的data api。這樣既實現了對需要dojo api的dojo構件的支持,又滿足了類似Tree、EGrid這種既需要綁定又需要數據來源的高級構件的要求。總體來說,DataStore只是薄薄的一層接口,實際的實現完全依賴于DataSource。
解決的用例問題:
- 數據綁定、查詢、處理:Tree綁定、ComboBox數據源、EGrid綁定
八、待討論的問題
客戶端的數據處理事實上要比我想象得還要復雜,我還有一些設計決策沒有確定,列舉下來以供討論吧。
8.1 客戶端的事務處理?
由于很多數據處理都放在了客戶端。客戶端的數據處理操作相應增多而且復雜,是否應該在DataSource中添加寫事務的處理?以便對數據操作進行更細粒度的管理,而不是僅僅停留在Query、Save和Track階段?
8.2 權限數據的來源
如果可以連接到服務端,用戶在向服務端登錄后,服務端會返回用戶的權限信息列表。客戶端接收后,可以相應的提供權限控制。但是,如果客戶端離線運行,用戶無法向服務的登錄,權限信息列表無從獲得,怎么提供權限控制?
探討方案1:離線客戶端無須登錄,由于沒有權限控制列表,默認擁有系統最低權限(固定的配置在客戶端),由系統開發人員決定用戶在離線時可以進行何種操作。用戶在線提交數據的時候,服務端也要根據相應的權限進行驗證以防止越權的數據操作
探討方案2:離線客戶端仍須登錄,權限控制列表使用用戶在線登錄時緩存的數據。其余與在線方式相同。如果用戶沒有在線登錄過,那么離線應用無法使用。
8.3 跨域數據的來源
由于瀏覽器的安全性策略,JavaScript無法向除本身域之外的其他服務器發送請求。也就是說,對于JavaScript而言,所有的數據必須來源于同一個域的服務器。對客戶端進行集成非常不利。
探討方案1:服務端提供一個服務接入層,接受如Spring Bean、EJB、JMS、WebService等形式的服務,并且把所有的服務都轉化成為一種固定格式的協議與客戶端交互。所有的服務集成與協議轉化都交給服務端,對客戶端完全透明。
探討方案2:服務端提供一個簡單的Proxy,通過一定的協議把請求和回復轉發給客戶端,處理交給客戶端來做
九、小結
雖然到了這里,但是對于基于JavaScript的RIA客戶端數據處理的討論卻才剛剛開始,還需要很多后續的展開討論。但是,上午已經過去了,需要去吃午飯了。:)