Action と view¶
Xitrumは3種類のActionを提供しています。
通常の Action
、FutureAction
、そして ActorAction
です。
Action¶
import xitrum.Action
import xitrum.annotation.GET
@GET("hello")
class HelloAction extends Action {
def execute() {
respondText("Hello")
}
}
リクエストはNettyのIOスレッド上で直ちに処理されますので、時間かかる処理(ブロック処理)を含めて はいけません。NettyのIOスレッドを長い時間使ってしまうとNettyは新しいコネクションを受信できなく なったりリスポンスを返信できなくなったりします。
FutureAction¶
import xitrum.FutureAction
import xitrum.annotation.GET
@GET("hello")
class HelloAction extends FutureAction {
def execute() {
respondText("hi")
}
}
リクエストは下記の ActorAction
と同じスレッドプールが使用されます。これはNettyのスレッドプールとは異なります。
ActorAction¶
ActionをAkka actorとして定義したい場合、ActorAction
を継承します。
import scala.concurrent.duration._
import xitrum.ActorAction
import xitrum.annotation.GET
@GET("hello")
class HelloAction extends ActorAction {
def execute() {
// See Akka doc about scheduler
import context.dispatcher
context.system.scheduler.scheduleOnce(3 seconds, self, System.currentTimeMillis())
// See Akka doc about "become"
context.become {
case pastTime =>
respondInlineView(s"It's $pastTime Unix ms 3s ago.")
}
}
}
Actorインスタンスはリクエストが発生時に生成されます。このactorインスタンスはコネクションが切断された時、
または respondText
、 respondView
等を使用してレスポンスが返された時に停止されます。
チャンクレスポンスの場合すぐには停止されず、最後のチャンクが送信された時点で停止されます。
リクエストは「xitrum」(システム名)というAkka actorシステムのスレッドプール上で処理されます。
クライアントへのレスポンス送信¶
Actionからクライアントへレスポンスを返すには以下のメソッドを使用します
respondView
: レイアウトファイルを使用または使用せずに、Viewテンプレートファイルを送信しますrespondInlineView
: レイアウトファイルを使用または使用せずに、インライン記述されたテンプレートを送信しますrespondText("hello")
: レイアウトファイルを使用せずに文字列を送信しますrespondHtml("<html>...</html>")
: contentTypeを"text/html"として文字列を送信しますrespondJson(List(1, 2, 3))
: ScalaオブジェクトをJSONに変換し、contentTypeを"application/json"として送信しますrespondJs("myFunction([1, 2, 3])")
contentTypeを"application/javascript"として文字列を送信しますrespondJsonP(List(1, 2, 3), "myFunction")
: 上記2つの組み合わせをJSONPとして送信しますrespondJsonText("[1, 2, 3]")
: contentTypeを"application/javascript"として文字列として送信しますrespondJsonPText("[1, 2, 3]", "myFunction")
: respondJs 、 respondJsonText の2つの組み合わせをJSONPとして送信しますrespondBinary
: バイト配列を送信しますrespondFile
: ディスクからファイルを直接送信します。 zero-copy を使用するため非常に高速です。respondEventSource("data", "event")
: チャンクレスポンスを送信します
テンプレートViewファイルのレスポンス¶
全てのActionは Scalate のテンプレートViewファイルと関連付ける事ができます。 上記のレスポンスメソッドを使用して直接レスポンスを送信する代わりに独立したViewファイルを使用することができます。
scr/main/scala/mypackage/MyAction.scala:
package mypackage
import xitrum.Action
import xitrum.annotation.GET
@GET("myAction")
class MyAction extends Action {
def execute() {
respondView()
}
def hello(what: String) = "Hello %s".format(what)
}
scr/main/scalate/mypackage/MyAction.jade:
- import mypackage.MyAction
!!! 5
html
head
!= antiCsrfMeta
!= xitrumCss
!= jsDefaults
title Welcome to Xitrum
body
a(href={url}) Path to the current action
p= currentAction.asInstanceOf[MyAction].hello("World")
!= jsForView
xitrumCss
XitrumのデフォルトCSSファイルです。削除しても問題ありません。jsDefaults
jQuery, jQuery Validate plugin等を含みます。<head>内に記載する必要があります。jsForView
jsAddToView
によって追加されたjavascriptが出力されます。レイアウトの末尾に記載する必要があります。
テンプレートファイル内では xitrum.Action クラスの全てのメソッドを使用することができます。 また、unescape のようなScalateのユーティリティも使用することができます。Scalateのユーティリティについては Scalate doc を参照してください。
Scalateテンプレートのデフォルトタイプは Jade を使用しています。 ほかには Mustache 、 Scaml 、 Ssp を選択することもできます。 テンプレートのデフォルトタイプを指定は、アプリケーションのconfigディレクトリ内の`xitrum.conf`で設定することができます。
respondView メソッドにtypeパラメータとして"jade"、 "mustache"、"scaml"、"ssp"のいずれか指定することでデフォルトテンプレートタイプをオーバーライドすることも可能です。
val options = Map("type" ->"mustache")
respondView(options)
currentActionのキャスト¶
現在のActionのインスタンスを正確に指定したい場合、currentAction
を指定したActionにキャストします。
p= currentAction.asInstanceOf[MyAction].hello("World")
複数行で使用する場合、キャスト処理は1度だけ呼び出します。
- val myAction = currentAction.asInstanceOf[MyAction]; import myAction._
p= hello("World")
p= hello("Scala")
p= hello("Xitrum")
Mustache¶
Mustacheについての参考資料:
Mustachのシンタックスは堅牢なため、Jadeで可能な処理の一部は使用できません。
Actionから何か値を渡す場合、at
メソッドを使用します。
Action:
at("name") = "Jack"
at("xitrumCss") = xitrumCss
Mustache template:
My name is {{name}}
{{xitrumCss}}
注意:以下のキーは予約語のため、 at
メソッドでScalateテンプレートに渡すことはできません。
"context":
unescape
等のメソッドを含むScalateのユーティリティオブジェクト"helper": 現在のActionオブジェクト
CoffeeScript¶
:coffeescript filter を使用して CoffeeScriptをテンプレート内に展開することができます。
body
:coffeescript
alert "Hello, Coffee!"
出力結果:
<body>
<script type='text/javascript'>
//<![CDATA[
(function() {
alert("Hello, Coffee!");
}).call(this);
//]]>
</script>
</body>
注意: ただしこの処理は 低速 です。
jade+javascript+1thread: 1-2ms for page
jade+coffesscript+1thread: 40-70ms for page
jade+javascript+100threads: ~40ms for page
jade+coffesscript+100threads: 400-700ms for page
高速で動作させるにはあらかじめCoffeeScriptからJavaScriptを生成しておく必要があります。
レイアウト¶
respondView
または respondInlineView
を使用してViewを送信した場合、
Xitrumはその結果の文字列を、renderedView
の変数としてセットします。
そして現在のActionの layout
メソッドが実行されます。
ブラウザーに送信されるデータは最終的にこのメソッドの結果となります。
デフォルトでは、layout
メソッドは単に renderedView
を呼び出します。
もし、この処理に追加で何かを加えたい場合、オーバーライドします。もし、 renderedView
をメソッド内にインクルードした場合、
そのViewはレイアウトの一部としてインクルードされます。
ポイントは layout
は現在のActionのViewが実行された後に呼ばれるということです。
そしてそこで返却される値がブラウザーに送信される値となります。
このメカニズムはとてもシンプルで魔法ではありません。便宜上Xitrumにはレイアウトが存在しないと考えることができます。
そこにはただ layout
メソッドがあるだけで、全てをこのメソッドで賄うことができます。
典型的な例として、共通レイアウトを親クラスとして使用するパターンを示します。
src/main/scala/mypackage/AppAction.scala
package mypackage
import xitrum.Action
trait AppAction extends Action {
override def layout = renderViewNoLayout[AppAction]()
}
src/main/scalate/mypackage/AppAction.jade
!!! 5
html
head
!= antiCsrfMeta
!= xitrumCss
!= jsDefaults
title Welcome to Xitrum
body
!= renderedView
!= jsForView
src/main/scala/mypackage/MyAction.scala
package mypackage
import xitrum.annotation.GET
@GET("myAction")
class MyAction extends AppAction {
def execute() {
respondView()
}
def hello(what: String) = "Hello %s".format(what)
}
scr/main/scalate/mypackage/MyAction.jade:
- import mypackage.MyAction
a(href={url}) Path to the current action
p= currentAction.asInstanceOf[MyAction].hello("World")
独立したレイアウトファイルを使用しないパターン¶
AppAction.scala
import xitrum.Action
import xitrum.view.DocType
trait AppAction extends Action {
override def layout = DocType.html5(
<html>
<head>
{antiCsrfMeta}
{xitrumCss}
{jsDefaults}
<title>Welcome to Xitrum</title>
</head>
<body>
{renderedView}
{jsForView}
</body>
</html>
)
}
respondViewにレイアウトを直接指定するパターン¶
val specialLayout = () =>
DocType.html5(
<html>
<head>
{antiCsrfMeta}
{xitrumCss}
{jsDefaults}
<title>Welcome to Xitrum</title>
</head>
<body>
{renderedView}
{jsForView}
</body>
</html>
)
respondView(specialLayout _)
respondInlineView¶
通常ViewはScalateファイルに記載しますが、直接Actionに記載することもできます。
import xitrum.Action
import xitrum.annotation.GET
@GET("myAction")
class MyAction extends Action {
def execute() {
val s = "World" // Will be automatically HTML-escaped
respondInlineView(
<p>Hello <em>{s}</em>!</p>
)
}
}
renderFragment¶
MyAction.jadeが
scr/main/scalate/mypackage/MyAction.jade
にある場合、同じディレクトリにあるフラグメント
scr/main/scalate/mypackage/_MyFragment.jade
を返す場合:
renderFragment[MyAction]("MyFragment")
現在のActionが``MyAction``の場合、以下のように省略できます。
renderFragment("MyFragment")
別のアクションに紐付けられたViewをレスポンスする場合¶
次のシンタックスを使用します respondView[ClassName]()
:
package mypackage
import xitrum.Action
import xitrum.annotation.{GET, POST}
@GET("login")
class LoginFormAction extends Action {
def execute() {
// Respond scr/main/scalate/mypackage/LoginFormAction.jade
respondView()
}
}
@POST("login")
class DoLoginAction extends Action {
def execute() {
val authenticated = ...
if (authenticated)
redirectTo[HomeAction]()
else
// Reuse the view of LoginFormAction
respondView[LoginFormAction]()
}
}
ひとつのアクションに複数のViewを紐付ける方法¶
package mypackage
import xitrum.Action
import xitrum.annotation.GET
// These are non-routed actions, for mapping to view template files:
// scr/main/scalate/mypackage/HomeAction_NormalUser.jade
// scr/main/scalate/mypackage/HomeAction_Moderator.jade
// scr/main/scalate/mypackage/HomeAction_Admin.jade
trait HomeAction_NormalUser extends Action
trait HomeAction_Moderator extends Action
trait HomeAction_Admin extends Action
@GET("")
class HomeAction extends Action {
def execute() {
val userType = ...
userType match {
case NormalUser => respondView[HomeAction_NormalUser]()
case Moderator => respondView[HomeAction_Moderator]()
case Admin => respondView[HomeAction_Admin]()
}
}
}
上記のようにルーティングとは関係ないアクションを記述することは一見して面倒ですが、 この方法はプログラムをタイプセーフに保つことができます。
またはテンプレートのパスを文字列で指定します:
respondView("mypackage/HomeAction_NormalUser")
respondView("mypackage/HomeAction_Moderator")
respondView("mypackage/HomeAction_Admin")
renderView
, renderViewNoLayout
, respondView
, respondViewNoLayout
では src/main/scalate
からのテンプレートファイルへのパス、
renderFragment
にはフラグメントを配置したディレクトリーへのパスをクラスの代わりに指定することができます。
Component¶
複数のViewに対して組み込むことができる再利用可能なコンポーネントを作成することもできます。 コンポーネントのコンセプトはアクションに非常に似ています。 以下のような特徴があります。
コンポーネントはルートを持ちません。すなわち
execute
メソッドは不要となります。コンポーネントは全レスポンスを返すわけではありません。 断片的なviewを "render" するのみとなります。 そのため、コンポーネント内部では
respondXXX
の代わりにrenderXXX
を呼び出す必要があります。アクションのように、コンポーネントは単一のまたは複数のViewと紐付けるたり、あるいは紐付けないで使用することも可能です。
package mypackage
import xitrum.{FutureAction, Component}
import xitrum.annotation.GET
class CompoWithView extends Component {
def render() = {
// Render associated view template, e.g. CompoWithView.jade
// Note that this is renderView, not respondView!
renderView()
}
}
class CompoWithoutView extends Component {
def render() = {
"Hello World"
}
}
@GET("foo/bar")
class MyAction extends FutureAction {
def execute() {
respondView()
}
}
MyAction.jade:
- import mypackage._
!= newComponent[CompoWithView]().render()
!= newComponent[CompoWithoutView]().render()