前回、インストールしてチュートリアル画面を出すまでの記事を書いた。今回はチュートリアルに挑戦する。チュートリアル読めばわかることなんだけど、「英語読むのめんどくせーよ、ボケ」という人のために書いてみる。
これがチュートリアル画面。
起動後、左側にあるRunを押してlocalhost:9000を開くと、こういう画面が立ち上がった。
うむ。
HTTPリクエストはJavaのコントローラによって処理されてレスポンスが作られる。JSONも吐けるよ。ボタンを押したらAjaxでJSONデータを取ってきて表示するよ。と書いている。押す。Hello from scalaと言われた。うん。どうも。
ファイル構成
次はファイル構成の説明だ。Playプロジェクトのディレクトリにはapp、conf、logs、project、public、target、testというディレクトリが含まれている。conf/application.confは全般的な設定のようだ。言語やロガーの設定などが書いてある。conf/routesはルーティングルールだ。
projectディレクトリはビルドインフォメーション(SBT)が入っているらしい。SBTってなんじゃ。と思って調べてみると、「最小限で押し付けがましくないビルドツール」だ、そうだ。押しつけがましいビルドツールってなんだ。antか?makeか?
静的なCSS、JavaScriptファイルなどはpublicフォルダにおかれる。デフォルトでは/images/favicon.pngのみが入っていた。
testディレクトリはその名の通り、ユニットテストのテストケースなどが入っているディレクトリだ。気になるのは、「Unit, functional, and integration tests go in the test directory」と書いていることだ。ingegration testをどうやってやるのか気になる。
HTTPルーティング
以下が、/conf/routesファイルだ。わかり易い書き方だ。
# Routes # This file defines all application routes (Higher priority routes first) # ~~~~ GET / controllers.MainController.index() GET /message controllers.MessageController.getMessage() GET /assets/javascripts/routes controllers.MessageController.javascriptRoutes() # Map static resources from the /public folder to the /assets URL path GET /assets/*file controllers.Assets.at(path="/public", file) GET /webjars/*file controllers.WebJarAssets.at(file)
ここに、http://localhost:9000/fooでindexを表示させるような設定を書いてみる。
GET /foo controllers.MainController.index
として保存して、http://localhost:9000/fooを表示させると、
表示できた。ファイルを保存するとすぐに反映された。楽だ。じゃあ次はあえて間違った記述をしてみる。
GET /baa controllers.MainController.unkoooooooo
controllers.MainControllerクラスにunkoooooooというメソッドは無いので、これはエラーになる。実際に表示させると以下のような画面がでる。
実行時エラーだけでなく、コンパイルエラーまで表示してくれんのはうれしいね。
コントローラの記述
次はコントローラを見てみる。/app/controllers/MainController.javaを見てみると以下のようになってる。
public class MainController extends Controller { public static Result index() { return ok(views.html.index.render("Hello from Java")); } }
さっきはルーティングルールからこのindexメソッドが呼ばれていたね。localhost:900/にアクセスするとここが実行されるわけだ。
ここで、ok()はHTTP 200のレスポンスを返すことを表すらしい。ためしに、文字列のtestを返してみた。
public class MainController extends Controller { public static Result index() { return ok("test"); //return ok(views.html.index.render("Hello from Java")); } }
すると、勝手にcontent-typeがtext/plainになり、bodyが「test」だけのレスポンスが生成された。なるほど。
次に、http://localhost:9000/messageにアクセスすると、「{"value":"Hello from Scala"}」というシンプルなJSONメッセージが返ってきた。これを生成しているのが、/app/controllers/MessageController.scalaだ。
package controllers import play.api.mvc.{Action, Controller} import play.api.libs.json.Json import play.api.Routes case class Message(value: String) object MessageController extends Controller { implicit val fooWrites = Json.writes[Message] def getMessage = Action { Ok(Json.toJson(Message("Hello from Scala"))) } def javascriptRoutes = Action { implicit request => Ok(Routes.javascriptRouter("jsRoutes")(routes.javascript.MessageController.getMessage)).as(JAVASCRIPT) } }
こんな感じ。知っている限りで説明する。まず、
case class Message(value: String)
ってのは、これでMessageというクラスを定義になる。これで、valueというStringのフィールドを持ったMessageクラスができる。case class(ケースクラス)と宣言すると、自動的にtoStringとかhashCodeとかがいい感じに実装されているそうだ。私は、あのhashCodeとかをいちいち実装するのが面倒でどうにかならんのかとずっと思っていた。こういう風に楽に宣言できるのはありがたい。
で、
object MessageController extends Controller { // ... }
というのがオブジェクトの宣言だ。objectというのはシングルトンインスタンスである。わざわざClassを書いてSingletonパターンを実装しなくてもobjectで宣言すればいいという感じ。続いて、
implicit val fooWrites = Json.writes[Message]
というのは、JSONインセプションというやつらしい。説明読んだけどよくわからん。多分、Messageというクラスの中で暗黙的にJSONに変換するための何かを宣言させてるんだろう。知らんけど。ということは、おそらく、Scalaはclassを宣言した後にclassの定義をある程度書き換えることができるということなんだろう。知らんけど。多分。
def getMessage = Action { Ok(Json.toJson(Message("Hello from Scala"))) }
ここがJSONメッセージを作っている所だ。case classにするとnewを省略できるらしい。これは型名によるパターンマッチの機能がいい感じに作用してどうちゃらこうちゃらという原理だそうだ。まあとにかくcase classだとnewと書かなくてもいいってこった。これは楽。
で、Messageで"Hello from Scala"を与えると、自動的にvalueフィールドに"Hello from Scala"が設定されたインスタンスが出来上がる。それをJSONに変換しているということだな。すごい!静的型付け言語なのにJavaScript並みに簡単だ。ためしに、Messageにtimeフィールドも設定してみよう。
Messageクラスを以下のように宣言して、
case class Message(value: String, time: Date)
レスポンスを返すところは以下のように。
Ok(Json.toJson(Message("Hello from Scala", new Date())))
実行すると、「{"value":"Hello from Scala","time":1399963796987}」と帰ってきた。なるほど。
先に進める。
def javascriptRoutes = Action { implicit request => Ok(Routes.javascriptRouter("jsRoutes")(routes.javascript.MessageController.getMessage)).as(JAVASCRIPT) }
という部分は、JavaScriptでタイプセーフに何かを扱うためのメカニズムらしい。保留。
テンプレートエンジン
app/views/index.scala.htmlは以下のような感じになっている。
@(message: String) @main("Welcome to Play Framework") { <script type='text/javascript' src='@routes.Assets.at("javascripts/index.min.js")'></script> <div class="well"> <h1>@message</h1> </div> <button id="getMessageButton">Get JSON Message</button> }
明らかにHTMLの一部分だ。ヘッダなどの情報が含まれていない。じゃあそれはどこにあるのかというと、main.scala.htmlにある。
@(title: String)(content: Html) <!DOCTYPE html> <html> <head> <title>@title</title> <link rel='shortcut icon' type='image/png' href='@routes.Assets.at("images/favicon.png")'> <link rel='stylesheet' href='@routes.WebJarAssets.at(WebJarAssets.locate("bootstrap.min.css"))'> <link rel='stylesheet' href='@routes.Assets.at("stylesheets/index.css")'> <script type='text/javascript' src='@routes.WebJarAssets.at(WebJarAssets.locate("jquery.min.js"))'></script> <script type="text/javascript" src="@routes.MessageController.javascriptRoutes"></script> </head> <body> <div class="navbar navbar-fixed-top"> <div class="navbar-inner"> <div class="container-fluid"> <a id="titleLink" class="brand" href="/">@title</a> </div> </div> </div> <div class="container"> @content </div> </body> </html>
app/views/index.scala.htmlの大部分のソースは@mainという宣言で囲まれていたが、つまり、これがmain.scala.htmlを表しているのだね。main.scala.html内部の@contentの部分にindex.scala.htmlが入る、と。なるほど。
で、
<script type="text/javascript" src="@routes.MessageController.javascriptRoutes"></script>
の部分が先ほどのMessageControllerのjavascriptRoutesと対応している。仮に、これを
<script type="text/javascript" src="@routes.MessageController.unkoooooo"></script>
と変えると、コンパイルエラーとなる。タイプセーフなルーティングをするというのはこういうことだね。じゃあ、これを書いておくと何が嬉しいのか。それは、同じく読み込まれるapp/assets/javascripts/index.jsのソースを見てみよう。
$(function() { // add a click handler to the button $("#getMessageButton").click(function(event) { // make an ajax get request to get the message jsRoutes.controllers.MessageController.getMessage().ajax({ success: function(data) { console.log(data) $(".well").append($("<h1>").text(data.value)) } }) }) })
なんと、jsRoutes.controllers.MessageController.getMessage()を呼び出して.ajaxなんて素敵なことをしている。あたかもコントローラのメソッドをjavascriptから直接呼んでいるかのようだ。こういうことが簡単にできて、かつ、名前を変更したらコンパイルエラーを出してくれる。というところが嬉しくてこういうことをやってるんだね。
また、viewでさりげなく
<script type='text/javascript' src='@routes.Assets.at("javascripts/index.min.js")'></script>
と書いている。こうやって".min"を付けると、自動的にminifyしてくれる(改行などを削除してサイズを小さくしてくれる)らしい。すげー便利だ。同様にして、coffee scriptなどをjsに自動変換してくれる機能もあるそうだ。すごいな。
以降、チュートリアルはテストやSBTファイルについて触れられているのだけど、あまり面白くないので省略。
まとめ
Play Frameworkいい感じ。