2013年1月9日水曜日

Backbone.jsとYiiフレームワークの連携


高瀬です。

Backbone.jsとYiiフレームワークを使用してアプリケーションを開発する場合の、ソースコードの配置や書き方について検討してみる。

サンプルアプリケーションのソースコードはこちらを参照。


サンプルアプリケーションの概要

Webブラウザ上でデータベースのER図が書けないものかと思い立ったので、それをテーマにアプリケーションを作成する。

アプリケーションはプロジェクトを1つの単位として、その中にスキーマやテーブルの定義を書いていく、という形にする。ここで、プロジェクトにリビジョンを付けて、編集履歴を残せるようにしたい。リビジョンはプロジェクトの作成直後を1とし、DB管理者のレビューを経て確定。その後さらに編集を開始する時に2に更新する、というサイクルを想定する。

画面構成や機能はまだまだ未検討で、今回はひとまずプロジェクトの作成、編集、削除ができるところまでを実装する。

画面は以下のとおり。まず、作成済みプロジェクトの一覧が画面の左上に表示される。


プロジェクト名にカーソルを重ねると、そのプロジェクト名の右側にメニューが表示される。メニュー項目は「編集」「プロパティ」「削除」とあり、それぞれER図の編集画面への遷移、プロジェクト名などのプロパティ編集、プロジェクトの削除、を行う。「レビュー」というメニュー項目を追加することを考えているが、今回は未実装。


プロジェクト一覧にカーソルを重ねた時、メニューと同時に、一覧の下側に「新しいプロジェクト」というリンクが表示される。「新しいプロジェクト」をクリックすると、画面の中央に「プロジェクトの作成」というダイアログを表示する。ダイアログにプロジェクト名を入力し、「作成」をクリックするとプロジェクトが作成される。作成されたプロジェクトは自動的にプロジェクト一覧に表示される。



プロジェクトのメニュー項目「編集」は現在未実装。「プロパティ」は「プロジェクトの作成」とほぼ同様のダイアログが表示され、プロジェクト名を変更できる。プロジェクト名が変更されると自動的にプロジェクト一覧に反映される。

メニュー項目「削除」をクリックすると、画面中央に確認メッセージが表示される。「OK」をクリックするとプロジェクトの削除が行われる。削除されたプロジェクトは自動的にプロジェクト一覧から消える。


画面には表示されないが、プロジェクトを作成すると、自動的にリビジョン1を表すデータが作成される。プロジェクトを削除するとリビジョンのデータがすべて削除される。現時点ではリビジョンは1固定。

開発するアプリケーションの名前はtprefix(ティー・プレフィックス)とする。以下、この名前で記述する。

tprefixは以下のデータベース、開発言語、フレームワークで開発する。

構成 DB/言語 フレームワーク
データベース PostgreSQL 9.x -
サーバ側 PHP 5.4 Yii
ブラウザ側 JavaScript Backbone.js

なお、JavaScriptのHTMLテンプレートエンジンとして、Handlebars.jsを使用する。



データベース

tprefixが記憶、管理するデータにはプロジェクトやリビジョン、スキーマ、テーブル、カラムなどが考えられる。その他にもユーザやカラム定義などを予定しているが、今回は「プロジェクト」「プロジェクトリビジョン」のテーブルのみを用意する。その他のテーブルの内容は検討中。




プロジェクトテーブルにはプロジェクトの名前を登録する。
プロジェクトリビジョンテーブルは、プロジェクトのリビジョンを登録する。今回はリビジョンIDは前述のとおり1固定とする。説明内容、レビュー日時、レビュー結果内容は未使用。

どちらのテーブルにもある「削除日時」は、レコードの削除が行われた日時を登録する。すなわち、「レコードの削除」操作はテーブルからレコードを消してしまうのではなく、「削除日時」を登録することにより、削除済みであるとみなすようにする。



ソースコードの配置

ソースコードはYiiフレームワークのディレクトリ構造を元に配置した。

サーバ側(PHP) ブラウザ側(JavaScript)

上図において、tprefixがルートを表す。protected以下にサーバ側のPHP、js以下にブラウザ側のJavaScriptが配置されている。

protected以下はMVCに従ってmodels、controllers、viewsにそれぞれソースコードを作成。これに対して、js以下はBackbone.jsのクラスに従って、models、collections、viewsを作成。

tprefixでプロジェクト一覧が表示されるページとなるのは、projectコントローラのindexアクション。URLでは http://(サーバ名)/index.php?r=project/index となる。



サーバ側

サーバ側はMVCフレームワークに従って開発する。

■モデル

モデルでは、今回はプロジェクトとプロジェクトのリビジョンを扱うので、Project.phpProjectRevision.phpを作成。これらは特にBackbone.jsとの関連は少ない。データベースへのアクセスにはCActiveRecordを使用。

Project.phpのsave()メソッドでトランザクションを使用しているが、トランザクション内の処理はtry/catchで例外を捕らえ、例外発生時にrollbackされるようにするべき。

■コントローラ

コントローラは、2種類のアクションを用意する。1つはブラウザ側で必要になるJavaScript、CSS、HTMLテンプレートを用意するアクション。もう一つは、ブラウザ側からのAjaxでの要求に対応する応答を返すアクション。

ProjectControllerでは、JavaScriptなどを用意するアクションはIndexAction.phpが担当。JavaScript、CSSのファイルを配列で定義する。このソースコード内にあるregisterPackage()メソッドはController.phpに定義しているメソッドで、こちらの記事を参照。

HTMLテンプレートも配列で定義する。これらはYiiのパーシャルビューとして、Controller::renderPartial()で描画させる。

よって、このproject/indexアクションを呼び出すと、表示する内容が何もない、YiiのレイアウトのみのHTMLが生成される。生成されたHTMLのheadタグ内には配列で指定したJavaScriptとCSSのファイルが含まれ、bodyタグ内にはHTMLテンプレートが含まれる。レイアウト部分以外の画面の描画はJavaScriptで行う。

※レイアウト部分と言うのは、前述「サンプルアプリケーションの概要」の図にある、「My Web Application」や「Home About ...」などが表示されている部分。それ以外の部分というのは、プロジェクトの一覧が表示されている部分。

もう一つのAjax要求に対応する応答を返すアクションでは、ListAction.phpのようにモデルを使ってデータを抽出・登録し、結果をJSON形式で出力する。

JavaScriptのファイルをYii::app()->clientScript->registerPackage()でHTMLに反映させるようにしており、この場合headタグ内にscriptタグが列挙される。しかし、headタグ内でJavaScriptを読み込むと、ページ全体の描画が始まるまでに時間がかかるため、ページの表示が遅い。bodyの終了タグの直前でJavaScriptが読み込まれるようにするとページの表示が速くなる。表示を速くするためにはもう少し工夫が必要。

■ビュー

ビューではHTMLテンプレートのみを定義した。このテンプレートはHandlebars.jsによって実際のDOMに変換される。Handlebars.jsのテンプレートはscriptタグ内に記述するようになっているので、それぞれのパーシャルビューは_project_delete_dialog.phpのようにscriptタグで囲まれた内容となっている。

■例外の出力

Yiiではtry/catchによってハンドリングされなかった例外はsiteコントローラのerrorアクションで処理される。ここで、SiteController.phpのとおり、もともとこのアクションは要求がAjaxであればエラー内容をechoで出力し、そうでなければrender()で描画するようになっている。この仕組みをそのまま使ってみようと思う。

Project.phpのprepare()メソッドで、更新または削除対象のレコードが見つからなかった場合は例外とした。この例外はsite/errorアクションに到達するが、NotFoundException.php内で表示するべきエラーメッセージを定めており、要求がAjaxだった場合はJSON形式、そうでない場合は文字列のみとしている。

これにより、JavaScriptからの要求は通常Ajaxで行うようにするので、エラーが発生した場合でもJSON形式の応答がある、という前提でブラウザ側の開発ができるはず。

JavaScriptからの要求がすべてAjaxであるとして開発をするのであれば、NotFoundExceptionクラス内でJSON形式か文字列かを決めるのではなく、site/errorアクション内でやってしまった方がよいだろう。



ブラウザ側

■名前空間とプログラムの開始

Backbone.jsやUnderscore.jsのファイルはmain.phpに記述しており、Ajax用ではないアクションで共通に読み込まれるようにしている。app.jsというファイルも共通としており、これは名前空間の定義用。

プログラムの開始は、プロジェクト一覧を表示する画面の場合、startupディレクトリ内にあるs_project.jsとなる。Webアプリケーション全体をシングルページとして開発する場合はわざわざstartupというサブディレクトリにこのファイルを配置する必要はないが、実験として、プロジェクト一覧の画面と、各プロジェクト内のER図を編集する画面を別のページとしてみようと考えており、起点となるソースコードを配置するディレクトリを用意した。

■コレクション

c_Project.jsでプロジェクトの一覧を扱う。urlに、サーバ側のproject/listアクションを指定。これにより、fetch()メソッドを実行するとプロジェクト一覧が取得される。一覧取得が完了すると自動的にresetイベントが発生するので、v_ProjectList.jsでハンドリングし、画面に描画する。

■モデル

今回扱うモデルはプロジェクトのみ。m_Project.jsで定義している。ここで、困ったことが2点あった。

1点目は、プロジェクトを新規作成する時、サーバ側のproject/saveアクションへプロジェクト名をPOSTしたいのだが、Backbone.Modelクラスのデフォルトのsave()メソッドを呼び出すと、サーバ側(PHP)の$_POST変数に送信しているはずの値が入らない。仕方がないので、jQueryのpost()で送信している。

$_POSTで受け取れないのは、Backbone.sync()により送信されるデータのエンコードがデフォルトではapplication/jsonになっており、これをPHPが受け取れていないため。Backbone.emulateJSON = true を指定することにより、$_POSTで受け取れるようになるかも、とのこと。

2点目は、Yiiとの連携とは関係ないが、プロジェクトの新規作成・編集・削除を行った際に自動的にプロジェクト一覧を再描画したいが、モデルが更新されたというイベントをどのようにしてコレクションに伝えればいいのかが分からなかった。無理やりな感じだが、プロジェクト一覧を描画しているビューのelが参照している要素に対してchangeイベントを発火して伝えている。

と、これを書きながら思ったが、もしかしてコレクションがモデルのaddイベントをハンドリングしていればいいだけの話だろうか。うむむ、気づくのが遅かった。

オブジェクト同士に関連がなく、ハンドリングの指定ができないような場合のために、前述のapp.jsで定義しているappオブジェクトを、Backbone.Eventの派生クラスにしておき、appオブジェクトにtrigger()させる、という方法もある。


■ビュー

ビューはv_ProjectItem.jsのようにモデルやコレクションに対応するものと、v_ProjectEditDialog.jsのようにダイアログなどの表示する部品に対応するものを作成した。ダイアログなどはサーバ側のビューで定義されているHTMLテンプレートから生成する。

ビュー同士や、別のビューが持っているモデルへのイベントの伝達方法が分からず、至らない点が多くあるので、追々直していくことにする。



おわり

ER図の編集画面くらいまでは作りたかったのに、勉強不足でいろいろ苦戦してようやくここまで、といったところ。

バリデーションをしていなかったり、エラー処理が不十分だったりと、実用レベルではない部分もあるので、そのあたりも身に付けていきたい。

tprefixの開発はまだまだ続く。はず。

0 件のコメント:

コメントを投稿