Action と view

Xitrumは3種類のActionを提供しています。 通常の ActionFutureAction 、そして 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インスタンスはコネクションが切断された時、 または respondTextrespondView 等を使用してレスポンスが返された時に停止されます。 チャンクレスポンスの場合すぐには停止されず、最後のチャンクが送信された時点で停止されます。

リクエストは「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"): respondJsrespondJsonText の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 を使用しています。 ほかには MustacheScamlSsp を選択することもできます。 テンプレートのデフォルトタイプを指定は、アプリケーションの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()