普通Webサイトを作るというときは画面から作ると思いますが、ことWebアプリケーションとなるとそうではないことが多いのではないでしょうか。
それというのも、Railsをはじめとする昨今のフレームワークでは、画面に至るにはControllerが必要ですしModel定義がないと記述できないこともままあります。
・・・まず簡単に画面を作らせてくれ。そこからModelの構成や画面遷移を検討したいんだ。
そんな心の声を実現するフレームワーク、それがLiftなのです。
Lift フレームワークの 1 ・ 2 ・ 3
ここでLiftの紹介をしているとそれだけで終わってしまうので、それはこちらを見ていただくとして、ここでは実際にLiftを使ったアプリケーションの作成方法を紹介します。そのStepは以下の通り。
1 初期設定
2 Viewを作る
3 Modelを作る
チュートリアル用のコードはこちら。ソース中には関連する情報へのリンクなども記載するようにしました。
1.初期設定
まずScalaとそのビルドツールであるsbtがないと話が始まらないので、まずこれらをセットアップします・・・が、これだけでもえらく長いのでここは3分クッキングばりにすでにできているものとして話を始めます。
→Windowsの方は、セットアップ方法をまとめた記事を書きましたのでご参考ください。
そして公式サイトにサンプルが置いてあるので、これをダウンロードしてきます。
http://liftweb.net/download
LiftのGithubにもリポジトリがあるので、こちらからcloneするのでもOKです。
あとはサイトに書いてある通り、試しにダウンロードしたフォルダ内にあるlift_basicでsbtコマンドを実行し、container:start
を実行すればhttp://localhost:8080
にサイトが立ち上がるはずです。
2.Viewを作る
ここからがLiftの本領発揮、まずは画面を作ります。
Viewで出番になるのは、主に以下のフォルダ内のファイルです。
src/main/webapp
src/main/scala/code/snippet
src/main/scala/code/view というもっともらしいフォルダがありますが、基本的にこいつの出番はありません。exampleを見るとページを直接レンダリングする際に使うみたいですが(Response.writeでタグを打っていくイメージ)、そんな機会はほぼないと思います。
基本的にはwebapp配下のhtmlファイルでレイアウトを整え、サーバーサイドでの処理が必要な部分をsnippetで記載する、といったイメージになります。
a.templateを作る
templateなどはwebapp配下のtemplate-hiddenに格納します。
ちなみに、Liftでは-hidden
がついたフォルダ、.
や_
で始まるテンプレートファイルはHTTPアクセスを発生させないでロードされるようです(こちら)。
ページのベースとなるようなhtml、共通で使いたいメニューやフッターといった部品はこの中に格納しておきます。
b.Viewを作る
作成したテンプレートを使いながらViewを作成します。テンプレートの呼び出し方法は、主に以下2つです(その他はこちら)。
Surround
要素を囲う、逆に言えばtemplateの中に埋め込むような感じになります。
指定方法は以下のようにclassへ指定を行います。
<body class="lift:content_id=main">
<div id="main" class="lift:surround?with=default;at=content">
default.html内のid="content"に埋め込まれます
</div>
</body>
Surroundされる側は、上記のようにclass="lift:content_id=main"
と埋め込まれるcontentを指定しておく。
Embed
要素を追加します。こちらは、what
でファイル名のhtmlを除いた部分を指定します。
<span class="lift:embed?what=_copywrite"></span>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lift:content_id="copywrite">
<head><meta content="text/html; charset=UTF-8" http-equiv="content-type" /></head>
<body >
<span id="copywrite">Copyright 20xx - 20yy All Rights Reserved.</span>
</body>
</html>
c.snippetを作る
サーバーサイドで処理をしたい箇所についてはsnippetを作成します。ControllerはないのでPOSTなどを受け取るFORM処理もsnippetで実装しますが、最初は一旦Modelや永続化の方法は脳内から消し去り、画面の動きをhtmlで記述することに注力します。
c1.snippetの呼び出し
<span class="lift:helloWorld.howdy">
Welcome to your Lift app at <span id="time">Time goes here</span>
</span>
HTML側の記載は、CSS classにlift:クラス名.メソッド名
で呼び出すsnippetを記載します。
注目すべきは<span id="time">Time goes here</span>
と書いてある箇所ですが、ここは実際にはsnippetで処理されて置き換えられる部分です。
これが何を意味しているかというと、「snippetの実装がまだ済んでいない箇所はサンプルデータ・タグをいれておくことができるし、実装後もそれを消す必要はない」という点です。
これにより、サーバーサイドの実装はいったん忘れてデザインに集中することができます。
class HelloWorld {
lazy val date: Box[Date] = DependencyFactory.inject[Date] // inject the date
// replace the contents of the element with id "time" with the date
def howdy = "#time *" #> date.map(_.toString)
}
snippet側はこちら。src/main/scala/code/snippet
に突っ込んでいきます(snippetが検索されるフォルダは、Boot.scalaのLiftRules.addToPackages("code")
で定義されています)。
注目すべきは #>
という見慣れない演算子ですが、こいつはjQueryのSelectorのようなものです。
引数として渡されるのはclassが指定されたDOMですが、その中から処理をしたい部分をidやclassを指定して抜き出すことができます。この場合は「id=timeの要素全部」が対象になります。
その種類についてはこちら参照。
c2.snippetでのリクエストの処理
フォームなどによるリクエスト処理は以下のように行います。
<form class="lift:TodoView.add?form=post">
<input type="text" name="title" class="column span-10"/>
<input type="submit" value="Add Todo!" style="margin-top:5px;"/>
</form>
def add(form: NodeSeq): NodeSeq = {
var title = ""
def addTodo() = {
var todo = new Todo(title)
todo.validate match{
case Nil => todo.save(); S.notice("Added " + todo.title)
case x => S.error(x); S.mapSnippet("TodoView.add", doBind)
}
}
def doBind(form: NodeSeq): NodeSeq = {
var sel =
"name=title" #> SHtml.onSubmit(title = _) &
"type=submit" #> SHtml.onSubmitUnit(addTodo) ;
return sel(form)
}
doBind(form)
}
リクエスト処理は、JavaScriptのコールバックのような形で記載を行います。
scala側のdoBind
で、submit時に受け取る値と処理関数(この場合addTodo
)を指定しており、実際の処理はこのaddTodo
内で行っています。
つまり、Liftにおけるフォーム処理snippetは、1.サーバーサイドの変数/関数とDOMのバインド、2.実際処理、の2つを実装するのが基本(のはず)です。
c3.Ajax処理
Ajaxの簡単なサンプルはこちらになります(Lift cookbookより)。
<form data-lift="form.ajax">
<div data-lift="EchoForm">
<input type="text" name="name" placeholder="What's your name?">
<input type="submit">
</div>
</form>
<div id="result">Your name will be echoed here</div>
object EchoForm extends {
def render = {
var name = ""
def process() : JsCmd = SetHtml("result", Text(name))
"@name" #> text(name, s => name = s) &
"type=submit" #> ajaxSubmit("Click Me", process)
}
}
ajaxSubmit
をバインドしているように、ここまでは基本的にフォーム処理と同じです(ajaxText
のように、あらかじめAjaxイベントが発生する要素をレンダリングすることも可能です。また、イベントに合わせて"button [onclick]" #> SHtml.ajaxInvoke(callback)
のように記載することもできます)。
ただ、Ajax処理の場合コールバック処理の帰り値はNodeSeq
でなくJsCmd
である必要があります。上記ではdef process() : JsCmd = SetHtml("result", Text(name))
の部分になりますが、これは該当IDの部分を置き換えるJavaScript処理を返却・・・という意味合いになります。使用可能なJsCmdはAPIの通りなので、ここを見つつ実装する感じになります。
この辺りは実装しているとJavaScriptの方が断然楽、というところがいくつもあるので、棲み分けを考える必要があると思います(サーバーサイド処理を呼び出すだけのTemplateを作って、jQuery Ajaxで処理する、Jsonでやり取りするなど・・・)。
Modelを作る
LiftにはRailsにおけるActiveRecordのような便利野郎はいないので、もうちょい実テーブルに近い形で記述します。これは一見デメリットのようにも見えますが、実際のテーブルがあまり隠蔽されずちゃんと扱ってる感があるというメリットもあります。
DB設定
DBへアクセスを行うModelを作成するためには、いろいろと設定が必要なのでまずそれを行います。
mapperライブラリを追加
Modelを定義するにあたりmapperのライブラリが必要なため、以下のような感じでこいつをbuild.sbt
に追加します。また、DBドライバも当然必要なのでそちらも追加(今回はh2dbのドライバを設定)。
libraryDependencies ++= {
val liftVersion = "2.6-M2"
Seq(
"net.liftweb" %% "lift-webkit" % liftVersion % "compile",
"net.liftweb" %% "lift-mapper" % liftVersion % "compile",
"net.liftmodules" %% "lift-jquery-module_2.6" % "2.5",
・・・
"com.h2database" % "h2" % "1.3.167"
Eclipseを使っている場合は、sbtを実行後eclipseコマンドで依存関係をEclipse側のプロジェクトに反映する。
DBドライバの追加
当然DBへ接続できないと話にならないので、その設定を追加します。こうしたプロパティ関係はsrc/main/scala/bootstrap/liftweb/Boot.scala
へ記載します。
この設定はちょっと長めなので、lift_basicの中のこちらをご参照ください。
Modelを作成する
LiftでのModel定義は以下のように行います。ポイントとしては、Mapperを継承してModel classを作成し、それを扱う(findしたりsaveしたりする)オブジェクトをMetaMapper traitを使用して作成するという点です。
MapperはLongKeyedをはじめ複数の種類があり、キーの形態に合わせて使います。
Mapper内の項目はMappedStringなどの項目用のクラスを使って定義していきます。これはちょうど「オブジェクト上の項目(メンバ)とDB上のカラムをMapしている」感じになります。
→バリデーションや型定義などもここへ記載していきます。
種類と使い方はちょっと書ききれないので、詳細はこちら、またAPIドキュメントをご参照ください。
object Todo extends Todo with LongKeyedMetaMapper[Todo]{
override def dbTableName = "todos" // define the DB table name
// add static method (like findById etc ...)
}
class Todo extends LongKeyedMapper[Todo] with IdPK {
def getSingleton = Todo
object todo extends MappedString(this, 100) {
override def validations = valMaxLen(256, "message must be under 256 characters long ") _ :: super.validations
}
object done extends MappedBoolean(this)
object minutes extends MappedDecimal(this,0) {
override def defaultValue = 5
}
}
Modelの作成が終わったら、Boot.scalaにDBテーブルとの連動設定を追記します。
Schemifier.schemify(true, Schemifier.infoF _, Todo)
※scalaにはJavaで言うところのstaticがありません。クラス間で共通の変数はobjectで宣言してそこにまとめることになってます(objectはシングルトンとして扱われる)。クラスと同盟のオブジェクトはコンパニオンオブジェクトと呼ばれます。