jQuery により、CSS (Cascading Style Sheets) 構文の中でセレクターを使用して DOM (Document Object Model) 要素を取得したり、それらの要素を操作したりする方法がよく使われるようになりました。jQuery を使うことでもたらされる数あるメリットの 1 つは、複雑な DOM ツリーを JavaScript でトラバースする苦労がなくなることです。
ZK はイベント・ドリブン方式のコンポーネント・ベースの Ajax フレームワークであり、Java でプログラミングされ、Java EE 技術と統合されていることで知られています。ZK により、デスクトップ・プログラミングの経験を Ajax に生かすことができます。HTML を好む人の場合には、マークアップや式言語 (EL) を使って ZK の UI を組み立てることができます。
この記事では、サーバー・サイドの Java コードと jQuery とを組み合わせることで、いかにしてエンタープライズ Ajax アプリケーション開発の難題を解決できるかについて説明します。また、jQuery と ZK が持つ技術を組み合わせることで、リッチで応答性の高い UI エクスペリエンスを容易かつ効率的に実現することができます。
この記事で使用するサンプル・コードは、ダウンロードすることができます。
エンタープライズ・クラスの Ajax アプリケーションを作成する場合、通常は以下のような難題に直面します。
- 複雑さ
- Ajax は複雑です。Ajax の通信パターンでは、DOM の要素が個別に更新される際、Web ページ全体を更新する必要がありません。
エンタープライズ・アプリケーションの場合、高度なビジネス・ロジックをサーバー・サイドからクライアント・サイドへと集め、それぞれの Ajax 呼び出しを手動で作成するのは非常に大変な作業です。
- サーバー・サイド・プログラミング
- サーバー・サイドの Ajax ソリューションでは、クライアントのリソースを活用して応答性の高いユーザー・エクスペリエンスやオフライン機能を実現することができず、コンピューティングやデータ・ストレージをクライアントに分散することもできません。
また、サーバー・サイドの Ajax ソリューションでは、サーバー上のメモリー・フットプリントを減らすことはできないため、アプリケーションのスケーラビリティーを向上させることができません。
- クライアント・サイド・プログラミング
- クライアント・サイド・プログラミングでは、セキュリティーの懸念事項や開発コストが一層増加します。ビジネス・ロジックや機密データがブラウザー上でオープンにされ、アプリケーションが攻撃を受けやすくなります。
クライアント・サイド・プログラミングでは、広範にわたる JavaScript 実装が必要であり、そうした実装は Java EE 技術によるアプリケーション開発に慣れた人にとっては楽ではありません。
- 巨大なデータ
- 大量のデータを UI 階層によってビューに表示する必要がある場合、もう 1 つ一般的な問題が起こります。例えば、通常の HTML の表を考えてみてください。データ・ソースが大規模になると、下記のいずれかを選択しなければなりません。
- 大規模なデータ・セットをプリロードすることにより、ブラウザーのキャッシュが過負荷になり、トラフィックの速度が低下するリスクをとる。
- 何らかのメカニズムを手動でサーバーに実装し、ユーザーが要求した場合には対象データのサブセットをいつでも表示できるようにする。
これらの難題を克服するための有効なソリューションは、クライアントの状態とサーバーの状態を自動的に同期させるフレームワークを使用することです。サーバー・サイドでは、あらゆるセキュアな Java EE 技術を活用する一方、クライアント・サイドのコンピューティング能力を利用することで、応答性や UI 制御を改善することができます。
ZK の画期的なアーキテクチャーにより、サーバー・サイドで利用可能な技術とクライアント・サイドで利用可能な技術のすべてを最大限に活用することができます。必要な場合には、クライアント・サイドのウィジェットに直接アクセスすることができます。このセクションでは、サーバーとクライアントを融合した ZK のアーキテクチャーのメリットについて、そのいくつかを概説します。
ZK ローダーは URL リクエストに対して以下のように動作します。
- リクエストされたページ上に UI コンポーネントを作成します。
- 必要なデータをそれらの UI コンポーネントにロードします。
- それらの UI コンポーネントを HTML ページ上に描画します。
- その HTML ページを ZK のクライアント・エンジンと共にクライアントに返送します。
ZK の AU (Asynchronous Update) エンジンとクライアント・エンジンによって Ajax 通信が処理されるため、開発者は Ajax 呼び出しの処理にかかわる複雑さを気にする必要がありません。図 1 は、ZK ローダー (ZK Loader)、ZK AU エンジン (ZK AU Engine)、ZK クライアント・エンジン (ZK client engine) を示しています。
図 1. ZK の融合アーキテクチャーの概要
ZK のクライアント・エンジンと AU エンジンは、野球に例えると投手と捕手の役割を果たし、ZK のイベント・キューは処理対象のイベントに対するパイプラインとして動作します。この結果、非同期イベントの処理がデスクトップ・プログラミング並みに単純化されます。
サーバーは一連の ZK コンポーネントを JVM の中に保持します。これらの ZK コンポーネントはブラウザーの中に JavaScript オブジェクトとして存在する一連の ZK ウィジェットに対応しているため、ページの状態はサーバー・サイドとクライアント・サイドで同期された状態に保たれます。ビジネス・ロジックや機密データを扱うイベント処理は、すべてサーバー・サイドで行われます。イベント処理の結果としてサーバー・サイドで ZK コンポーネントの状態が変化した場合、その変化がクライアント・サイドの ZK ウィジェットに反映されるのは、そのイベント処理が完了した時点のみです。ビジネス・ロジックがクライアント・サイドに公開されることは決してありません。
ZK ウィジェットはクライアント・サイドの JavaScript オブジェクトであり、jQuery で作成されています。そのため ZK ウィジェットには jQuery に付随する動作がすべて継承されています。コンポーネントの完全な表現とコンポーネントの状態はクライアントとサーバーの両方にあるため、アプリケーションのプログラミングをサーバー・サイドで行うこともクライアント・サイドで行うこともできます。図 2 はその例を示しています。
図 2. ZK のクライアント・サイド技術
場合によると、クライアント・サイドで大量のデータを処理するという問題を解決しなければならない場合があります。ZK の grid コンポーネント、listbox コンポーネント、tree コンポーネントはデータのレコードを表示するために使用されますが、これらのコンポーネントにはオンデマンドでデータをロードする機能があります。そのため、ビューの中で必要にならない限り、データはブラウザー上に描画されません。ZK にはまた、大量のデータを複数のページに分散するページング機能もあります。これらの機能を利用することで、大量のデータを表示する上での複雑さを大幅に軽減することができます。
サーバーとクライアントを融合した ZK のアーキテクチャーを説明するために、株価表示アプリケーションを概念検証の例に上げ、このアプリケーションを 2 つの異なる方法で実現します。純粋にサーバー主導のソリューションをリファレンスの方法として使用し、もう一方の方法ではサーバーとクライアントの両方にイベント処理を分散します。
この株価表示アプリケーションは XML の株価レコードの履歴を読み取り、そのデータを表形式と折れ線グラフで表示します。またこのアプリケーションには、Google Suggest のように検索用語を自動補完する検索ボックスも含まれています。図 3 はこのアプリケーションを示しています。
図 3. 株価表示アプリケーション
ZK の MVC を使用したサーバー・サイド・ソリューション
サーバー主導の手法として、このサンプルではZK の MVC (Model-View-Controller) の手法を採用しています。
- モデル
-
このモデルは、3 つの Java クラス、2 つのデータ・オブジェクト、1 つの DAO オブジェクトによって構成されています。1 つの
Stockオブジェクトが多くのPriceオブジェクトに対応する場合があります。public class Stock { private int _id; private String _name; private List<Price> _priceItems = new ArrayList<Price>(); .... //getter and setter methods } public class Price { private String _date; private double _open; private double _high; private double _low; private double _close; private int _volumn; .... //getter and setter methods }
StockDAOクラスは data.xml の中にある株価の履歴データを解析し、メモリー内にStockオブジェクトとPriceオブジェクトを作成します。各Stockオブジェクトは LinkedList のオブジェクト stocks に追加されます。public class StockDAO { private List<Stock> stocks = new LinkedList<Stock>(); public List findAll() {...} // returns all stock items in data.xml public Stock getStock(int id) {...} }
- ビュー
-
ビューは ZK のマークアップ言語を使って実装されます。メインの表示ページ (index.zul) はボーダー・レイアウトを使って分割されます。west ブロックには検索ボックスと株式銘柄の一覧が配置され、center ブロックには、price.zul のプレースホルダーとして機能する
include要素が配置されます。各株式のデータ・レコードは grid コンポーネントの中に表示され、また price.zul の chart コンポーネントにプロットされます。リスト 1 に示す index.zul の内容は以下のとおりです。
- 1 行目の
initタグは ZK ローダーに対し、ZK のデータ・バインディング・マネージャーを初期化するように指示します。この指示によって、データ・ソースは自動的に UI コンポーネントにバインドされ、また逆に UI コンポーネントがデータ・ソースにバインドされます。 - 2 行目の
apply="StockController"は、デフォルト・パッケージのStockControllerクラスがborderlayoutコンポーネントのコントローラーとして動作することを表しています。 - 12 行目の
model="@{main$composer.stocks}"は、StockControllerクラスのgetStocks()メソッドから取得したデータ・モデルによってlistboxを設定することを意味しています。 - 14 行目から 16 行目の
listitemコンポーネントは各 stock オブジェクトに対して作成され、その stock オブジェクトの名前がlistcellのラベルに割り当てられます。
リスト 1. index.zul1. <?init class="org.zkoss.zkplus.databind.AnnotateDataBinderInit"?> 2. <borderlayout id="main" apply="StockController"> 3. <west title="ZK Finance" size="250px" flex="true" 4. splittable="true" minsize="210" maxsize="500" collapsible="true"> 5. <panel> 6. <toolbar> 7. <label value="Search:" /> 8. <textbox id="searchBox" ctrlKeys="#down#up" 9. focus="true" sclass="demo-search-inp" /> 10. </toolbar> 11. <panelchildren> 12. <listbox id="itemList" model="@{main$composer.stocks}" 13. fixedLayout="true" vflex="true"> 14. <listitem self="@{each='stock'}" value="@{stock}"> 15. <listcell label="@{stock.name}" /> 16. </listitem> 17. </listbox> 18. </panelchildren> 19. </panel> 20. </west> 21. <center> 22. <include id="detail"/> 23. </center> 24. </borderlayout>
リスト 2 に示す price.zul の内容は以下のとおりです。
- 1 行目では、先ほどと同じように ZK のデータ・バインディング・マネージャーが初期化されています。
- 2 行目の
apply="PriceController"は、デフォルト・パッケージのPriceControllerクラスが window コンポーネントのコントローラーとして動作することを表しています。 - 3 行目の
model="@{main2$PriceController.prices}"は、PriceControllerクラスのgetPrices()メソッドから取得したデータ・モデルによってグリッドを設定することを意味しています。 - 13 行目から 20 行目の row コンポーネントは各
priceオブジェクトに対して作成され、このpriceオブジェクトの値がラベルに割り当てられます。
リスト 2. price.zul1. <?init class="org.zkoss.zkplus.databind.AnnotateDataBinderInit"?> 2. <window id="main2" apply="PriceController"> 3. <grid id="history" model="@{main2$PriceController.prices}" > 4. <columns menupopup="auto"> 5. <column label="Date" /> 6. <column label="Open" /> 7. <column label="High" /> 8. <column label="Low" /> 9. <column label="Close" /> 10. <column label="Volumn" /> 11. </columns> 12. <rows> 13. <row self="@{each='price'}"> 14. <label value="@{price.date}"/> 15. <label value="@{price.open}"/> 16. <label value="@{price.high}"/> 17. <label value="@{price.low}"/> 18. <label value="@{price.close}"/> 19. <label value="@{price.volumn}"/> 20. </row> 21. </rows> 22. </grid> 23. <chart id="line" width="500" height="250" type="line" 24. fgAlpha="128" model="@{main2$PriceController.cateModel}"/> 25. </window>
- 1 行目の
- コントローラー
-
コントローラーは ZK の
rg.zkoss.zk.ui.util.GenericForwardComposerを継承するクラスです。rg.zkoss.zk.ui.util.GenericForwardComposerにより、データ・オブジェクトが UI コンポーネントと自動的に接続されます。StockControllerクラスは index.zul のborderlayoutコンポーネントの中でビューを制御しています。リスト 3 に示すStockControllerクラスの内容は以下のとおりです。- 8 行目で、id=
mainのborderlayoutがonCreateイベント・リスナーに登録されています。この onCreate イベント・リスナーにより、デフォルトでビューに表示される株式が設定されます。 - 13 行目で、id=
itemListのlistboxがonSelectイベント・リスナーに登録されています。このonSelectイベント・リスナーはlistboxの中で選択されたlistitemを取得し、id=detailの include コンポーネント (株価データと折れ線グラフを表示するためのページ・ソース) を設定します。 - リスト 18 で、id=
searchBoxの textbox が onChanging イベント・リスナーに登録されています。このonChangingイベント・リスナーは株式に対して繰り返し処理を行い、テキストボックスの値が変更されるたびに、ユーザーが入力した内容に一致する株式のリストを返します。引数のイベントはorg.zkoss.zk.ui.event.InputEventにキャストされ、ユーザーが入力した内容はkeyというストリングに割り当てられます。 - 34 行目で、
getStocks()メソッドは data.xml に記載されているすべての株式を返します。
リスト 3. StockController.java1. public class StockController extends GenericForwardComposer { 2. Textbox searchBox; 3. Listbox itemList; 4. Include detail; 5. StockDAO dao = new StockDAO(); 6. private static String DETAIL_URL = "price.zul"; 7. 8. public void onCreate$main(){ 9. itemList.setSelectedIndex(0); 10. Events.postEvent(new Event(Events.ON_SELECT, itemList)); 11. } 12. 13. public void onSelect$itemList(){ 14. int id = ((Stock)itemList.getSelectedItem().getValue()).getId(); 15. detail.setSrc(DETAIL_URL + "?id=" + id); 16. } 17. 18. public void onChanging$searchBox(InputEvent event) { 19. String key = event.getValue(); 20. LinkedList item = new LinkedList(); 21. List items = dao.findAll(); 22. 23. if (key.trim().length() != 0) { 24. for (Iterator iterator = items.iterator(); iterator.hasNext();) { 25. Stock st = (Stock) iterator.next(); 26. if (st.getName().toLowerCase() 27. .indexOf(key.toLowerCase()) != -1) 28. item.add(st); 29. } 30. itemList.setModel(new ListModelList(item)); 31. } else itemList.setModel(new ListModelList(items)); 32. } 33. 34. public List getStocks(){ 35. return dao.findAll(); 36. } 37. }
PriceControllerクラスはグリッドの中のビューと、price.zul の chart コンポーネントを制御します。リスト 14 に示すPriceControllerクラスの内容は以下のとおりです。- 14 行目で、price.zul に対するリクエストに追加された
idパラメーターを取得しています。org.zkoss.zk.ui.Executionsクラスは Ajax 以外のリクエストに対するラッパーです。 - 21 行目で、折れ線グラフ・モデルの値を設定しています。
- 25 行目で、
getPrices()メソッドを宣言しています。この宣言により、price.zul の grid コンポーネントの式言語でgetPrices()メソッドを呼び出すことができます。なぜなら、PriceControllerクラスは (UI コンポーネントをデータ・オブジェクトに接続する)org.zkoss.zk.ui.util.GenericAutowireComposerを継承しているからです
リスト 4. PriceController1. public class PriceController extends GenericAutowireComposer { 2. 3. private StockDAO dao = new StockDAO(); 4. private CategoryModel cateModel; 5. private List items; 6. 7. public PriceController() { 8. init(); 9. } 10. 11. public void init() { 12. //get stock id 13. int id = Integer.parseInt((String) 14. Executions.getCurrent().getParameter("id")); 15. Stock stock = dao.getStock(id); 16. items = stock.getPriceItems(); 17. //create category model for chart 18. cateModel = new SimpleCategoryModel(); 19. for (Iterator iterator = items.iterator(); iterator.hasNext();) { 20. Price price = (Price) iterator.next(); 21. cateModel.setValue(stock.getName(), price.getDate(), 22. Price.getClose()); 23. } 24. } 25. public List getPrices(){ 26. return items; 27. } 28. public CategoryModel getCateModel() { 29. return cateModel; 30. } 31. }
- 8 行目で、id=
この例全体を通して、検索ボックス機能を実現するために必要な非同期呼び出しと、表データおよびグラフ・データの動的な更新は、すべて ZK のクライアント・エンジンと AU エンジンによって処理されています。命令はサーバーで実行され、クライアントは単純に UI コンポーネントとデータ・モデルの中で更新情報を受信しているにすぎません。
同じアプリケーションを、サーバーとクライアントを融合した手法で実現してみましょう。明らかにクライアント・サイドで実装した方が適切な候補として、検索ボックスがあります。検索ボックスでは応答性が重要であり、株式の名前のみを解析して表示すればよく、株価データの解析や表示は必要ありません。この場合には StockController クラスは必要ありません。検索ボックスのイベント処理はすべてクライアントで実装されるからです。この例では index.zul を変更し、hybrid.zul とリネームします。
リスト 5 に示す hybrid.zul のリストでは、以下の ZK 構成体に注目してください。
- ZK のクライアント名前空間を宣言する必要があります。
<zk xmlns:w="http://www.zkoss.org/2005/zk/client">
(ウィジェットの名前空間を w で表すのは ZK の規約です。) - 2 行目で、
borderlayoutからapply="StockController"属性が削除されています。これは株式の名前を表示する検索ボックスとリストボックスがクライアントによって直接更新されるためです。 - 8 行目で、
textboxのonChangingイベントが、w:というアノテーションによってクライアントで処理されるように宣言されています。 - 9 行目で、listbox を取得し、
listboxのupdate()メソッドを呼び出しています。this.$f(‘list')という表現は Java のinp.getFellow("list")と等価であり、この表現によってid="list"のlistboxコンポーネントを取得しています。 - 13 行目で、zk.Widget クラスの
bind_というcallbackメソッドはオーバーライドされてupdate()メソッドを実行し、またlistboxが DOM ツリーにバインドされた後、最初のリスト項目が選択された状態になるように設定されています。 - 28 行目の
zUtl.parseXML()は XML 文書を構文解析するためのユーティリティー・メソッドです。ZK での jQuery セレクターの表記としてjq()が指定されているため、必要な場所ではどこでも jQuery の表記$()を使用することができます。 - 38 行目で新しい
listitemウィジェットが作成され、このウィジェットのラベルの値と ID は各株式に対応して設定されています。またlistitemウィジェットはlistboxの子ウィジェットとしてlistboxに追加されています。 - 56 行目から 70 行目で、
listboxのonSelectイベントは、非同期リクエストに対するサービス・メソッドをサーバーで実装することによって直接処理されています。この例では、選択されたlistitemのuuidを取得し、price.zul ページに対するリクエストに追加しています。そして price.zul ページによって株価データと折れ線グラフが表示されます。
リスト 5. hybrid.zul
1. <zk xmlns:w="http://www.zkoss.org/2005/zk/client">
2. <borderlayout id="main">
3. <west title="ZK Finance" size="250px" flex="true"
4. splittable="true" minsize="210" maxsize="500" collapsible="true">
5. <panel>
6. <panelchildren>
7. <textbox id="inp">
8. <attribute w:name="onChanging">
9. this.$f('list').update(event.data.value);
10. </attribute>
11. </textbox>
12. <listbox id="list" onSelect="" rows="10" width="300px">
13. <attribute w:name="bind_">
14. function (desktop, skipper, after) {
15. this.$bind_.apply(this, arguments);
16. var self = this;
17. after.push(function () {
18. self.update();
19. self.firstChild.setSelected(true);
20. });
21. }
22. </attribute>
23. <attribute w:name="update"><![CDATA[
24.
25. (function () {
26. var data;
27. function loadData(w) {
28. var xmlDoc = zk.xml.Utl.parseXML(jq(w).html().replace(/<!--|-->/g,'').trim()),
29. ids = xmlDoc.getElementsByTagName("id"),
30. labels = xmlDoc.getElementsByTagName("name");
31. data = [];
32. jq(ids).each(function (i) {
33. data.push({id: this.firstChild.nodeValue, label:
34. labels[i].firstChild.nodeValue});
35. });
36. }
37.
38. function createItems (listbox, data) {
39. if (!data.length) return;
40. jq(data).each(function () {
41. listbox.appendChild(new zul.sel.Listitem({label: this.label, uuid: this.id}));
42. });
43. }
44. return function (txt) {
45. txt = txt || '';
46. if (!data) loadData(this.$f('data'));
47. this.clear();
48. createItems(this, jq.grep(data, function (item) {
49. return item.label.toLowerCase().indexOf(txt.toLowerCase()) != -1;
50. }));
51. this.stripe();
52. };
53. })()
54. ]]></attribute>
55. </listbox>
56. <zscript>
57. public class MyService implements org.zkoss.zk.au.AuService {
58. public boolean service(org.zkoss.zk.au.AuRequest request, boolean everError) {
59. final String cmd = request.getCommand();
60. if (cmd.equals(Events.ON_SELECT)) {
61. String uuid = ((List)request.getData().get("items")).get(0);
62. System.out.println("selected:" + uuid);
63. content.setSrc("price.zul?id=" + uuid);
64. return true;
65. }
66. return false;
67. }
68. }
69. list.setAuService(new MyService());
70. </zscript>
71. <include id="data" src="data.xml" comment="true"/>
72. </panelchildren>
73. </panel>
74. </west>
75. <center>
76. <include id="content" src="price.zul?id=1"/>
77. </center>
78. </borderlayout>
79. </zk>
|
このサンプル・コードはダウンロードすることができます。検索ボックスの応答性が改善されていることに注目してください。
この記事では、ZK による Ajax 開発を改善する方法として、サーバー・サイドのプログラミングとクライアント・サイドのプログラミングの両方のメリットを活用する方法について説明しました。ZK により、サーバー・サイドとクライアント・サイドのアプリケーションの状態を自動的に同期させることができます。Java EE 技術の生産性の高さを生かせる一方、クライアント・サイドの機能を実装する選択肢が相変わらず残されています。実装の手法は作成対象のエンタープライズ・アプリケーションの要件に依存するようになり、開発フレームワークのメリットや制約に依存することがなくなります。
| 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|---|---|---|
| Sample code for this article | zk5Client_ServerFusion.zip | 9018KB | HTTP |
学ぶために
- 「ZK を使ったリッチ・インターネット・アプリケーション」(developerWorks、2010年1月) は ZK を紹介し、ZK を使って作成した実際のアプリケーションを Apache Tomcat 上で実行して MySQL データベースに接続する例について説明しています。
- ZK の資料として、「ZK 5.0 and jQuery」と「ZK 5.0 and jQuery part 2」を読んでください。ZK の紹介、デモ、ZK の制御の実装方法、サンプル・アプリケーションの作成方法など、豊富な内容が含まれています。
- クライアント名前空間とクライアント・サイド・プログラミングについて学んでください。
- あらゆるウィジェット・クラスのスーパークラス、ZK.Widget の資料を読んでください。
- 「ZK MVC Made Easy」は、ZK に用意された 3 つのユーティリティー・クラスと 3 つのヘルパー・メソッドを詳細に説明しています。これらのクラスやメソッドにより、Model-View-Controller のパターンに従って容易にアプリケーションを作成することができます。
- ZK のデータ・バインディング (UI コンポーネントとデータ・ソースの間でデータをコピーするための接続コードを自動化するメカニズム) とデータ・バインディング・マネージャーについて学んでください。
製品や技術を入手するために
- ZK (無料) をダウンロードしてください。
- IBM 製品の評価版をダウンロードするか、あるいは IBM SOA Sandbox のオンライン試用版で、DB2®、Lotus®、Rational®、Tivoli®、WebSphere® などが提供するアプリケーション開発ツールやミドルウェア製品を試してみてください。
議論するために
- developerWorks blogs から developerWorks のコミュニティーに加わってください。
- 今すぐ My developerWorks プロフィールを作成し、Ajax に関するウォッチ・リストを設定してください。My developerWorks とずっとつながっていられます。
- Web 開発に関心を持つ他の developerWorks メンバーを見つけてください。
- Web の話題に焦点を絞った developerWorks のグループの 1 つに参加し、皆さんの知識を共有してください。
- Roland Barcia が彼のブログの中で Web 2.0 とミドルウェアについて語っています。
- developerWorks のメンバーが Web のトピックに関して共有するブックマークを調べてみてください。
- 即座に答えを得るために、Web 2.0 Apps フォーラムを訪れてください。
- 即座に答えを得るために、Ajax フォーラムを訪れてください。

