[2006 年 12 月号] |
[技術講座]
(株)オージス総研 Yoji Kanno
2006年現在、JavaでWebアプリケーションを開発する場合は、MVCアーキテクチャを採用するケースがほとんどです。Web開発におけるMVCは、本来のMVCとは若干異なる部分もあり、Web版MVCというべき概念です。しかし、このWeb版MVCアーキテクチャはStrutsの流行ととも本来のMVCよりメジャーになりました。
Web版MVCアーキテクチャの大雑把なコンセプトは以下のとおりです。
Web版MVCフレームワークは、上記のようなMVCアーテクチャに基づいた設計と実装を支援もしくは強制します。そして上記の施策により画面開発者とロジック開発者が分離でき、より効率的で再利用性が高い開発が行えると宣伝されています。
しかしWeb版MVCフレームワーク上で開発を始めると、その煩雑さに圧倒されます。ちょっとしたプログラムをつくるために、複数の設定ファイルやJavaクラスを実装しなければならないフレームワークで、個々の開発者は幸せになっているでしょうか? 例えば、コマンドラインベースのツールに簡単なGUIをつけたいときにJavaやStrutsを使いたいと思うでしょうか? ある程度JavaやStrutsに習熟した開発者でも辛い作業でしょう。
このような状況を改善する方法としては、なにかツールを使って必要なファイルを自動生成する事があります。しかし本末転倒の気配があります。そもそも複雑な作業を開発者に強いるフレームワークがいけないんじゃない? という訳です。この現象はバッドノウハウと呼ばれ世界中で報告例があります。
上記のような反省を踏まえて、このようなWeb版MVCアーキテクチャを採用しないフレームワークが多数出現しています。Clickはそのようなフレームワークのひとつです。
Clickの公式サイトではClickの特徴として以下のことを述べています。
Clickは日本ではまだまだマイナーな存在かもしれませんが、少しずつ注目され始めています。またClickの主要コミッタとして日本人の方(Naoki Takezoeさん)も参加されていて、日本語化の作業やClickIDEといったプロダクトの開発を行っています。
WebアプリケーションフレームワークとしてClickが持つ機能は以下の通りです。
こちらのサンプルを見れば、Clickの能力がイメージできるでしょう。
https://www.avoka.com:8080/click-examples/home.htm
Clickにはページ間遷移情報の管理機構は存在しません。Clickでページ遷移をする場合は、Javaのコードで遷移先のパスかページクラス(後述します)を直接指定します。ページ間遷移情報をXML等で集中管理したい場合には、開発者が専用の仕組みを作成することになります。もちろん筆者自身はあまり必要性を感じません(ここはきっぱりと)。
同じようなフレームワークがある中で筆者がClickに特に注目した理由は、コードのシンプルさです。ClickのコアAPIに含まれるクラスはわずか50個以下で、個々のクラスの実装を確認しても、あまり複雑なクラスは存在しません。しかし、それだけでWebフレームワークとしての基本的な機能をすべて含んでいます。このシンプルな割りに多機能な所に魅力を感じます。(さらに付け加えると利用も簡単です)
そもそもフレームワーク自体のシンプルさは必要でしょうか? 開発者はフレームワークの仕組みに乗っかるだけだから詳細を知らなくてもいいじゃんという考え方もあります。もちろんある意味正しい意見ですが、世の中はそんなに簡単には出来ていません。
なぜならば、実際の開発現場では、どんなに開発者に親切で多機能で抽象化が進んだフレームワークでも、内部動作に注意する必要が依然としてあるからです。
例えば開発時には何らかのトラブルがおきた場合、そのトラブルが何処に起因するか切り分けが必要です。結局、開発者はフレームワークの内部に分け入ってソースコードを読む羽目になります。この現象は抽象化の漏れと呼ばれる現象で世界中で報告例があります。皆さんも見覚えがありますよね。
もちろん将来、完璧な抽象化が進めば、現状我々がJVMの実装を気にする必要が(あまり)無いのと同様に、Webフレームワークの実装を完全に隠蔽できるかもしれません。しかし、そこまではまだ遠いというのが実情です。
とりあえずClickはシンプルで強力なフレームワークです。では採用しましょう!!
という訳に行かない以下の弱点があります。
ただし、これらは単純な欠点ではなくClick Frameworkの設計指針に照らし合わせて意図的に切り捨てられたものだといえます*2。一見便利な機能でもシンプルさを損なうようなものは省かれています。このような潔い設計指針がClick Frameworkのシンプルさとパワフルさを保つ要因でしょう。ここは設計をする人間としては、とても参考になる部分です。また後に説明するように、開発者が必要な機能を実装することによって、上記弱点をカバーすることができます。
Clickフレームワークの場合、フレームワーク本体はシンプルのまま保ち、足りない機能は開発者が拡張して使うことを前提にしています。ほとんどの開発者はプロジェクト用の共通基底Pageクラスや拡張コントロールを記述することになるでしょう。Click本体を拡張して利用したり、他のフレームワークに組み込む事は、シンプルな設計と実装のおかげで大変容易になっています。
例えばSeasar2やSpringのようなDIコンテナとの組み合わせなどは興味深いと思いますが、Springに関してはサンプルに実装があり、Seasar2に関しても何人か取り組んでいる方がいる模様です。
ClickはWebフレームワークというより、Webフレームワークの素かもしれません。つまり何か欲しければ自分で作るDIYなフレームワークなんです。
それでは、Clickフレームワークの構成と振る舞いについて簡単に説明します。
Clickでフレームワークを作成する際に、開発者が意識する要素は以下の3種類です。
開発者はnet.sf.click.Pageクラスを継承して表示するページに対応するPageクラスを定義します。このPageクラスは後述するControlクラスのコンテナとして機能すると同時にユーザからのイベントのハンドリングも行います。つまりPageクラスは完全にView層とModel層の双方に依存し、両者を繋ぐものとして存在しています。
net.sf.click.control以下のクラスです。これがHTMLの画面部品を表現します。これらのControlをPageクラスにaddすることによって、表示画面を組み立てます。組み立てた画面(Page)をフレームワークに引き渡せば、フレームワークがVeloccityテンプレートエンジンと連携しつつ画面をHTMLとしてレンダリングしてくれます。
Controlの中で重要なものは、Formクラスです。ユーザからのリクエストを受け取るためには、FormクラスのTextFieldやSubmitといったコントロールを追加しPageクラスに追加する必要があります。これによってサブミット時にTextFieldの内容などがリクエストの内容として送付されます。この値は自動的にコントロールの値にマッピングされます。
実はこれらのコントロールのtoString()メソッド呼び出すと、HTMLの断片を生成します。これはClickの自動レイアウト機能と呼ばれるもので、後述するVelocityと組み合わせて短期間でプロトタイプを作成する際に役立ちます。
Velocityテンプレート形式のファイルです。以下の要素を参照できます。
コントロール
モデル情報
Velocity上で$変数名とだけ記述すると、その場でtoString()した結果が展開されます。例えばPageクラスにformというインスタタンス変数を定義しておき、テンプレートで$formと書いておくだけで、Fromとそれに追加されたControlのHTML断片がそこに埋め込まれて描画されることになります。Controlの見え方をカスタマイズする場合は、Javaプログラム側でコントロールの属性を設定します。
この機能を使い、Clickでは画面の大枠はVelocityテンプレートで記述し、FormやTableなどの細かい画面部品はControlの自動レイアウトに任せるという発想で画面記述を行うことになります。
もちろんテンプレート側でHTMLを完全に制御して描画することも可能です。以下のリンク先に記述方法が載っています。
https://click.sourceforge.net/docs/click-api/net/sf/click/control/Form.html#form-layout
ここでは、ブラウザからリクエストが飛ばされてから、ページが表示されるまでのフレームワークの動きを説明します。
一見複雑だと思われるかも知れませんが、これはフレームワークがやっていることであって,開発者が意識するポイントはほとんどありません。
開発者はPageクラスに関しては以下の作業を行えばよいでしょう。
それではClickを使ったサンプルを実装しながら、Clickフレームワークの概観をつかんでいきましょう。今回のサンプルでは一行コメント掲示板を作成してみることにしました。Blogのエントリの下についているような小型掲示板ですね。
コマンドラインでantを利用可能にしてください (https://ant.apache.org/) インストール方法や実行方法については省略します。
筆者は以下の環境で試しました。
Microsoft Windows XP [Version 5.1.2600] >java -version java version "1.5.0_06" Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_06-b05) Java HotSpot(TM) Client VM (build 1.5.0_06-b05, mixed mode, sharing)
次のURL https://www.ogis-ri.co.jp/otc/hiroba/technical/ClickFramework/sample/click_sample_20061208.zip からサンプル一式をダウンロードして展開します。
コマンドラインで展開によって作成されたディレクトリに移動してant setup_libを実行してください。
このコマンドではHibernate関連のライブラリなどを自動ダウンロードしてインストールします。
以下のような画面が表示されるはずです
(サンプルのディレクトリ)>ant setup_lib Buildfile: build.xml pre_setup_lib: [get] Getting: https://www.ibiblio.org/maven2/org/apache/geronimo/specs/geronimo-jta_1.0.1B_spec/1.0/geronimo-jta_1.0.1B_spec-1.0.jar [get] To: (サンプルのディレクトリ)\local-repo\javax\transaction\jta\1.0.1B\jta-1.0.1B.jar setup_lib: [artifact:dependencies] Downloading: antlr/antlr/2.7.6/antlr-2.7.6.jar [artifact:dependencies] Downloading: javax/persistence/persistence-api/1.0/persistence-api-1.0.jar [artifact:dependencies] Transferring 50K [artifact:dependencies] Downloading: commons-collections/commons-collections/2.1.1/commons-collections-2.1.1.jar (中略) [artifact:dependencies] Transferring 115K [copy] Copying 15 files to (サンプルのディレクトリ)\lib BUILD SUCCESSFUL
会社内でプロキシの設定が必要な環境にいる方は、展開して作成されたbuild.xmlの先頭4行目のコメントアウトを外し定義してください。
<!-- プロキシーを利用したい方は以下の行を書き換えてください --> <!-- <setproxy proxyhost="***" proxyport="***" proxyuser="***" proxypassword="***" /> -->
ant start と実行するとwinstone(Webコンテナ)が起動します。
https://localhost:8080/sampleCommentPage.htm にアクセスしてみましょう。
適当にコメントを入力して動作を確かめてみましょう。作成されたデータはインストールディレクトリ以下ののdbディレクトリに保存されます。
サーバをストップさせたい場合は、プロンプト上でCrtl+Cを押下してください。
展開されたディレクトリ以下のwebapp/WEB-INF/web.xmlを確認してください。
<web-app> <display-name>Click Examples</display-name> <servlet> <servlet-name>click-servlet</servlet-name> <servlet-class>net.sf.click.ClickServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>click-servlet</servlet-name> <url-pattern>*.htm</url-pattern> </servlet-mapping>
<?xml version="1.0" encoding="Windows-31J"?> <click-app charset="Windows-31J" locale="ja"> <headers> <header name="Pragma" value="no-cache"/> <header name="Cache-Control" value="no-store, no-cache, must-revalidate, post-check=0, pre-check=0"/> <header name="Expires" value="1" type="Date"/> </headers> <pages package="sample.view"> <excludes pattern="/tiny_mce/"/> </pages> <mode value="trace"/> </click-app>
注目すべき点はsample.viewの設定です。これにより、webapp以下にsamplePage.htmというファイルがあった場合 sample.viewというパッケージ以下にあるSamplePageクラスが自動的に検索されます。
また、このような自動マッピングが基本ですが、個別に設定することもできます。
例:
<pages package="sample.view"> <page path="home.htm" classname="example.Sample"/> </pages>
それではhtmファイルを配置しましょう。
webapp以下に以下のような内容のファイルcommentPage.htmを配置します
<html> <head> $imports </head> <body> $commentTable $commentBox <hr/> </body> </html>
$importsはClickが利用するcssやjavascriptを取り込むために必要な設定です。
body内にcommentTableとcommentBoxを配置することにしました。
commentTableやcommentBoxの中身をどのように実装するかは、Pageクラスで定義します。
それでは画面部品をページに貼り付けていきましょう。まずは、Velocityテンプレートから参照されている、commentTableとcommentBoxを用意します。
コントロールはpublicで宣言することによってVelocityテンプレートから参照できるように設定されます。コンストラクタに渡す文字列がVelocityテンプレートで参照する変数名に対応します。
public class CommentPage extends Page { public Table commentTable = new Table("commentTable"); public Form commentBox = new Form("commentBox");
さらに後でイベントハンドラから参照できるように、TextFieldも宣言しておきます。これらはVelocityテンプレートで直接参照する必要は無いので、protectedで宣言しておきます。(commentBoxから取り出すこともできますが、こちらの方がソースが簡潔になります)
(中略) protected Field emf = new TextField("email"); protected Field cmf = new TextField("commentText");
組み立てはコンストラクタの中で行います。
public CommentPage() { commentTable.addColumn(new Column("id", "ID")); commentTable.addColumn(new Column("email", "E-MAIL")); commentTable.addColumn(new Column("commentText", "comment")); commentBox.add(emf); commentBox.add(cmf); }
HTMLテーブルのcommentTableにカラムを追加し、またFormのcommentBoxにフィールドを追加しています。
完成版はこのような形になります。
package sample.view; import net.sf.click.*; import net.sf.click.control.*; import net.sf.click.extras.control.*; import java.util.*; public class CommentPage extends Page { public Table commentTable = new Table("commentTable"); public Form commentBox = new Form("commentBox"); protected Field emf = new TextField("email"); protected Field cmf = new TextField("commentText"); public CommentPage() { commentTable.addColumn(new Column("id", "ID")); commentTable.addColumn(new Column("email", "E-MAIL")); commentTable.addColumn(new Column("commentText", "comment")); commentBox.add(emf); commentBox.add(cmf); } @Override public void onInit() {} @Override public void onRender() {} @Override public void onDestroy() {} }
サンプルのディレクトリ上で ant compile を実行してください。
(サンプルのディレクトリ)>ant Buildfile: build.xml init: [mkdir] Created dir: (サンプルのディレクトリ)\build\classes compile: [copy] Copying 1 file to (サンプルのディレクトリ)\build\classes [javac] Compiling 5 source files to (サンプルのディレクトリ)\build\classes [javac] 注: (サンプルのディレクトリ)\src\sam ple\repoimpl\RepoImpl.java の操作は、未チェックまたは安全ではありません。 [javac] 注: 詳細については、-Xlint:unchecked オプションを指定して再コンパイ ルしてください。 BUILD SUCCESSFUL Total time: 8 seconds (サンプルのディレクトリ)\sample>
それでは今の段階で動作確認をしてみましょう。サンプルのディレクトリ上で ant start を実行してください。
コンテナが起動した後に以下のページを参照してください。
https://localhost:8080/commentPage.htm
これで、とりあえず見た目だけは実装できたことになります。
掲示板の機能を実現するためにはコメントリストを保存する必要がありますが、この情報はPageクラスのインスタンスに持たせるわけにはいきません。Pageクラスは毎回生成されるためです。
サンプルなのでHTTPセッションにでも保存しておけばいいのですが、どうせならばDBに永続化しましょう。
以下のようなリポジトリクラスを用意します。
package sample.model; import java.util.List; public interface Repository { public void begin(); public void setRollbackOnly(boolean isRollback); public void commitOrRollback(); public void commit() throws RuntimeException; public void rollback() throws RuntimeException; public void close(); public <T> T get(Class<T> clazz , java.io.Serializable key) ; public <T> List<T> select(Class<T> clazz , String query); public <T> T merge(T entity); public void save(Object entiry); }
このリポジトリクラスはオブジェクトのCRUD機構以外にトランザクションのスタートとコミット/ロールバック、永続化セッションのクローズ機能を持っています。オールドスタイルな気もしますが、そこはサンプルということでご容赦を。
このインタフェースとHibernate(HibernateAnnotationsを利用)版の実装、Hibernateの設定に関しては、すでにサンプルに組み込んであります。3分間クッキングの要領ですね。
一行コメントのモデルとして、電子メール情報を格納するインスタンス変数emailと一行コメントを格納するcomment、ついでに一意キーをあらわすidを持つクラスを定義します。
package sample.model; import javax.persistence.*; @Entity public class Comment { @Id @GeneratedValue Long id; String commentText; String email; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getCommentText() { return commentText; } public void setCommentText(String commentText) { this.commentText = commentText; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } }
ここでインスタンス変数としてRepositoryクラスの実装を組み込みます。実装クラスのsample.repoimpl.RepoImplは既にサンプルの中に用意されています。
PageクラスのonInitメソッドとonDestroyメソッドをオーバーライドして、トランザクションの開始と終了のコードを記述します。実際の開発では、共通の基盤となるPageクラスを用意してこれらのコードを記述し、開発者はサブクラス化して利用することになります。
package sample.view; import net.sf.click.*; import net.sf.click.control.*; import net.sf.click.extras.control.*; import java.util.*; import sample.model.*; import sample.repoimpl.*; public class CommentPage extends Page { /** リポジトリ */ Repository repo = new RepoImpl(); ・・・(省略)・・・ public CommentPage() { ・・・(省略) ・・・ } @Override public void onInit() { repo.begin(); } @Override public void onDestroy() { repo.commitOrRollback(); repo.close(); } }
onDestroyは、(画面のレンダリングを含む)ページに対する一連の処理の最後に必ず呼び出されます。他のページのフォワード処理後でも、例外が搬送された場合でも必ず呼び出されます。
この動作を利用すると、例えばHibernateを使っている場合に、onInitでHibernateセッションを開きonDestoryでHibernateセッション閉じることにより、手軽にOpenSessionInViewパターンが実装できます。このような実装をおこなうことによって、Velocityテンプレートの中でDBアクセスが発生しても問題無く処理することができます。レンダリングはonDestroyより前に実行されるからです。
ただしPage遷移する場合は、以下のような順序で実行されます。
[PageAのonInit]->・・略・・->[フォワード]->[PageBのonInit]->・・省略・・->[PageBのレンダリング]->[PageBのonDestory]->・・省略・・->[PageAのonDestry]
この時にトランザクションを複数回コミットしないといった工夫をする必要はあります*4。詳しくはsample.repoimpl.RepoImplを参考ください。
ここでページ表示時に今まで投稿されたコメントを表示する機能を作成しましょう。ページのレンダリング時にロジックを実行したい場合は、onRenderメソッドをオーバーライドします。
onRenderメソッドではリポジトリ内にあるCommentオブジェクトを全検索した結果をsetRowListでcommentTableに設定しています。乱暴な実装ですがご容赦を。
@Override public void onRender() { List<Comment> commentList =repo.select(Comment.class , "from Comment c order by c.id"); commentTable.setRowList(commentList); }
commentTableはHTMLテーブルとしてレンダリングされます。またClickのTableはオブジェクトとのマッピングをある程度自動的に行います。このマッピングのルールは実はcommetTableを組み立てる時に暗黙的に指定されています。
commentTableの組み立て時にはColmunを追加していました。この時のIDの属性がsetRowListで設定された属性とマッピングされます。そしてレンダリング時に中身の属性を取り出してColmunの値として表示します。この場合は、Commentオブジェクトのidとemailとcommentが自動的にマッピングされます。
commentTable.addColumn(new Column("id", "ID")); commentTable.addColumn(new Column("email", "E-MAIL")); commentTable.addColumn(new Column("commentText", "comment"));
この時のColmunのIDとしては、OGNL式も定義できます。例えばCommentに userという属性が定義されていて、このオブジェクトがnameという属性を持っていれば、user.nameといった形でオブジェクトを辿って値を取り出すように定義できます。
これで表示部分は完成したことになります。先程のsampleCommentPage.htmでコメント情報を入力し、こちらのcommentPageで確認すれば、DBアクセス部分のコードが動いていることを確認することができます。
それでは、コメントの追加機能を実装してみましょう。
CommentPageクラスにサブミットボタンの宣言を追加します。第2引数がイベントハンドラのレシーバとなるオブジェクト*5、第3引数がメソッドの名前になります。
protected Submit submit = new Submit("submit", this, "onComment");
コンストラクタでcommentBoxにsubmitを追加します。
public CommentPage() { ・・・中略・・・ commentBox.add(submit);
onCoommentメソッドの実装を行います。このメソッドではCommentオブジェクトを生成し、Formの内容をそこにコピーしリポジトリにセーブしています。その後setRedirectによってページ遷移を決定しています。ただしこの場合は同じページに遷移しています。このように処理が成功した場合は、リダイレクトしてもう一度このページを表示し直すことによって2重サブミット等の危険を減らすことができます。
public boolean onComment() throws Exception { try { Comment c = new Comment(); commentBox.copyTo(c); repo.save(c); setRedirect(this.getClass()); return false; } catch (Exception e) { e.printStackTrace(); repo.setRollbackOnly(true); throw e; } }
また、ここで戻り値をfalseとしているのは直前でリダイレクトを設定しているからです。このページのそれ以降の処理はほとんど意味が無いためです。
先ほどのcommentTableの事例と同様に自動的なオブジェクトのマッピングが行われています。copyToメソッドではFormに追加されたField系コントロール(emfとcmf)のIDと、copyTo()で指定されているオブジェクトのインスタンス属性等に一致するものがあれば、値をコピーします。FormにはcopyFromというメソッドもあり、オブジェクトからFormへのマッピングを行うこともできます。
コントロールの定義をもう一度見てみましょう。このときコンストラクタに渡していたIDが重要です。実は、このIDはOGNL形式で記述できます。Personがdepartmentという属性をもっていて、departmentがnameという属性をもっていれば、new TextField("department.name")というIDを渡すことができます。
//各コントロールの定義 protected Field emf = new TextField("email"); protected Field cmf = new TextField("commentText"); protected Submit submit = new Submit("submit");
その後、コンストラクタ内でControlをCommentFormに追加しています。このときにFormがemailとcommentBoxというテキストフィールドを持つことが設定されると同時に、オブジェクトとFormのマッピング情報が設定されます。
//formへの追加 commentBox.add(emf); commentBox.add(cmf); commentBox.add(submit);
イベントハンドラ(onCommentメソッド)では、オブジェクトをコピーしていました。
//マッピング Comment c = new Comment(); commentBox.copyTo(c);
今までは、ユーザの入力を無制限に受け入れていました。とりあえずEmailフィールドの検証ロジックを追加してみましょう。
Clickの場合、検証機能を持ったコントロールを追加することによって、検証機能を組み込むことができます。emailのフォーマットを検証するロジックを組み込みたかったら、EmailFieldを使います。
protected Field emf = new TextField("email"); ↓ protected Field emf = new EmailField("email")
検証に失敗したらどのような画面遷移をすべきかを記述します。
public boolean onComment() { if(emf.getError()!=null) return true; //エラーがあったらそのままリターン try { Comment c = new Comment(); commentBox.copyTo(c); repo.save(c); } catch (Exception e) { e.printStackTrace(); repo.setRollbackOnly(true); } finally { setRedirect(this.getClass()); return false; } }
今回のようなフィールド単位で検証するようなエラーの場合は、そのフィールドがエラーを持っているか否かでエラーの有無を確認します。onProcessの段階で検証が行われています。
onCommentのロジックでは、もしエラーがあった場合は、そのままリターンしています。emfが持っているエラーは適切に描画されます。
Pageクラスなどで独自チェックをおこないう業務的なエラーがあった場合は、setError(error)もしくはsetErrorMessage(messageのkey)でErrorを設定することもできます。
最終的なソースコードは、(インストールディレクトリ)/src/sample/view/SampleCommentPage.java とほぼ同等のものになります。そちらをご参考ください。
IDEによる自動生成など一切無しで、数分で実装できる程の作業量だと思います。旧来のWeb系フレームワークをはるかに超えるClickの生産性がうかがえます。ビルド環境を完備した上に、永続化の部分を先に実装してしまっているので他のフレームワークと比べるのは少しフェアではありませんが。
10年近くJavaを使っていて、個人的に思うのは「標準」や「スタンダード」の影響力です。標準から外れたり、有名じゃないフレームワークは採用されないことが多いのです。そして標準やスタンダードに則ったフレームワークを使いながらフレームワークを使い(使わされ)、あまりの面倒さにJavaごと嫌いになってしまう人が多いこと。
しかしEJB標準に反旗を翻すかたちで、DIコンテナが広まり、そこから逆流してEJB3が作られたという例があるとおり、現場のエンジニアがアクションを起こさなければ、現状は絶対変わりません。Clickに限らず、良いものを見つけたら、それがスタンダードじゃなくても、積極的に評価、研究して導入していきたいものですね。もちろんリスクを考えずに飛びつくのは危険行為なので、ご利用は計画的にお願いします。
本ドキュメントでは、多言語化、ページの共通テンプレートなどまだまだ紹介していない情報も沢山あります。本家のサイトにいけば、十分過ぎるほどのドキュメントが存在しますが、日本語の紹介ドキュメントがあってもよいでしょう。著者もより経験を積んで各種ノウハウを貯めてから第2弾の記事で紹介したいと思います。
本ドキュメントは、以下のサイトの情報を参考にしています。もしくはClickを学習する上で以下のサイトが参考になります。
© 2006 OGIS-RI Co., Ltd. |
|