第2回ではsprayのテンプレート・プロジェクトをデバッグ実行してみました。
今回は、かんたんなTODO登録を作成してみます。
掲載したコードはGithubにて公開しています。
https://github.com/x1-/spray-sandbox
1. パッケージ名の変更
まずは・・・sprayのテンプレートは、ソース・パッケージがcom.exampleのままなので適切なパッケージ名に変更しておきます。
ソース・パッケージ上でShift+F6押すとrenameダイアログが立ち上がります。
私はcom.inkenkun.x1に変更しました。
build.sbtの定義も変更します。
organization := "com.inkenkun.x1"
version := "0.1"
scalaVersion := "2.11.2"
scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8")
resolvers += "repo.typesafe.com" at "http://repo.typesafe.com/typesafe/releases/"
libraryDependencies ++= {
val akkaV = "2.3.6"
val sprayV = "1.3.2"
Seq(
"io.spray" %% "spray-can" % sprayV,
"io.spray" %% "spray-routing" % sprayV,
"io.spray" %% "spray-testkit" % sprayV % "test",
"com.typesafe.akka" %% "akka-actor" % akkaV,
"com.typesafe.akka" %% "akka-testkit" % akkaV % "test",
"org.specs2" %% "specs2-core" % "2.3.11" % "test"
)
}
Revolver.settings
2. TODO登録ページをつくる
MyService.scalaを開きます。
package com.inkenkun.x1
import akka.actor.Actor
import spray.routing._
import spray.http._
import MediaTypes._
// we don't implement our route structure directly in the service actor because
// we want to be able to test it independently, without having to spin up an actor
class MyServiceActor extends Actor with MyService {
// the HttpService trait defines only one abstract member, which
// connects the services environment to the enclosing actor or test
def actorRefFactory = context
// this actor only runs our route, but you could add
// other things here, like request stream processing
// or timeout handling
def receive = runRoute( myRoute )
}
// this trait defines our service behavior independently from the service actor
trait MyService extends HttpService {
val myRoute =
path( "" ) {
get {
respondWithMediaType( `text/html` ) { // XML is marshalled to `text/xml` by default, so we simply override here
complete {
<html>
<body>
<h1>Say hello to <i>spray-routing</i> on <i>spray-can</i>!</h1>
</body>
</html>
}
}
}
}
}
ルーティングにTODO登録ページを追加します。
val myRoute =…に“add”というパスを追加します。
val myRoute =
path( "" ) {
get {
respondWithMediaType( `text/html` ) { // XML is marshalled to `text/xml` by default, so we simply override here
complete {
<html>
<body>
<h1>Say hello to <i>spray-routing</i> on <i>spray-can</i>!</h1>
</body>
</html>
}
}
}
} ~
path( "add" ) {
get {
respondWithMediaType( `text/html` ) {
complete {
<html>
<body>
</body>
</html>
}
}
}
}
“add”ページはパラメータを取らないので、pathを使います。
HTMLを返却するのでrespondWithMediaType( `text/html` )を追加しました。
ここで・・・
completeの中に直接HTML文字列を記述しているのに、エラーになりません。
これはsprayもakkaも関係ありません。scalaの機能です。
scalaではXMLをプログラム中に直接書くことができるのです。
※詳しくは、hishidamaさんのScala XMLをご参照ください。
ここまでで実行します。
無事起動したらブラウザでhttp://localhost:8080/addを開きます。
TODO入力ボックスが表示されます。
さて、せっかくなのでトップページのHTMLを編集して”add”へのリンクを作成しておきます。
val myRoute =
path( "" ) {
get {
respondWithMediaType( `text/html` ) { // XML is marshalled to `text/xml` by default, so we simply override here
complete {
<html>
<body>
<h1>TODO管理 |д゚)チラッ</h1>
<ul>
<li><a href="/add" title="TODO登録">TODO登録</a></li>
</ul>
</body>
</html>
}
}
}
} ~
:
またルーティングにHTMLを記述してしまうと、変更しづらいので外に出します。
trait MyService extends HttpService {
val myRoute =
path( "" ) {
get {
respondWithMediaType( `text/html` ) { // XML is marshalled to `text/xml` by default, so we simply override here
complete ( index )
}
}
} ~
path( "add" ) {
get {
respondWithMediaType( `text/html` ) {
complete ( add )
}
}
}
lazy val index =
<html>
<body>
<h1>TODO管理 |д゚)チラッ</h1>
<ul>
<li><a href="/add" title="TODO登録">TODO登録</a></li>
</ul>
</body>
</html>
lazy val add =
<html>
<body>
<h2>TODO登録ヾ(*´∀`*)ノ</h2>
<form name="form1" method="POST" action="add">
<div>
<span><input type="text" name="todo" value="" style="width:60%;" /></span>
<span><input type="submit" name="submit" value="登録" /></span>
</div>
</form>
</body>
</html>
}
3. 入力されたTODOを保存する
入力されたTODOを保存します。
とりあえず、現時点ではキャッシュに保存することにします。
※永続化は後ほど・・・
まずはMyService.scalaにキャッシュオブジェクトを作成しておきます。
object Todos {
private var items: Seq[String] = Seq()
def add( item: String ): Unit = synchronized {
items = items :+ item
}
def remove( item: String ): Unit = {
val ( head, tail ) = items.span( _ != item )
synchronized {
if ( tail.size == 0 )
head
else
head ++ tail.tail
}
}
def all: Seq[String] = items
}
今度はルーティングにPOSTを追加します。
val myRoute =
path( "" ) {
get {
respondWithMediaType( `text/html` ) { // XML is marshalled to `text/xml` by default, so we simply override here
complete ( index )
}
}
} ~
path( "add" ) {
get {
respondWithMediaType( `text/html` ) {
complete ( add )
}
} ~
post {
respondWithMediaType( `text/html` ) {
complete ( add )
}
}
}
これだけだと、addページと同じものを返すだけなので、POSTされた値を取り出します。
post {
formFields( 'todo ) { todo =>
respondWithMediaType( `text/html` ) {
complete ( add )
}
}
}
これで、formの中のtodoフィールドを取り出せました。
todoに入力された値をキャッシュオブジェクトに追加します。
post {
formFields( 'todo ) { todo =>
Todos.add( todo )
respondWithMediaType( `text/html` ) {
complete ( add )
}
}
}
lazy val add = <html>…も少し改造して、入力された値を表示するようにしましょう。
lazy valをdefに変更して引数を取るようにします。
def add( items: Seq[String] = Seq.empty[String] ) =
<html>
<body>
<h2>TODO登録ヾ(*´∀`*)ノ</h2>
<form name="form1" method="POST" action="add">
<div>
<span><input type="text" name="todo" value="" style="width:60%;" /></span>
<span><input type="submit" name="submit" value="登録" /></span>
</div>
</form>
<div>
<ul>
{ for { item <- items } yield <li>{item}</li> }
</ul>
</div>
</body>
</html>
ルーティングの方も変更します。
path( "add" ) {
get {
respondWithMediaType( `text/html` ) {
complete ( add( Todos.all ) )
}
} ~
post {
formFields( 'todo ) { todo =>
Todos.add( todo )
respondWithMediaType( `text/html` ) {
complete ( add( Todos.all ) )
}
}
}
}
Response返却の部分がGETとPOSTで被っているのでリファクタしておきます。
val myRoute =
path( "" ) {
get {
respondWithMediaType( `text/html` ) { // XML is marshalled to `text/xml` by default, so we simply override here
complete ( index )
}
}
} ~
path( "add" ) {
get {
addPage
} ~
post {
formFields( 'todo ) { todo =>
Todos.add( todo )
addPage
}
}
}
def addPage: Route = {
respondWithMediaType( `text/html` ) {
complete( add( Todos.all ) )
}
}
ここまで出来たら実行してみます。
TODOを登録すると・・・
下に追加されますv(´∀`*v)ピース
今回はHTMLを返すTODO登録ページを作成しましたが、次回はJSONを送受信するREST APIを作成したいと思います。





