Контроллеры и представления =========================== Xitrum располагает тремя видами контроллеров или действий (actions): стандартный контроллер ``Action``, ``FutureAction`` и актор контроллер ``ActorAction``. Стандартный контроллер (normal action) -------------------------------------- Реализация данного контроллера синхронная. :: import xitrum.Action import xitrum.annotation.GET @GET("hello") class HelloAction extends Action { def execute() { respondText("Hello") } } В случае наследования от xitrum.Action, ваш код будет выполнятся в потоке Netty's IO. Это допустимо только в случае если ваш контроллер очень легковесный и не блокирующий (возвращает ответ немедленно). Иначе Netty не сможет принимать новые подключения или отправлять запросы клиентам. FutureAction ------------ :: import xitrum.FutureAction import xitrum.annotation.GET @GET("hello") class HelloAction extends FutureAction { def execute() { respondText("hi") } } В случае наследования от xitrum.FutureAction, код контроллера будет выполнятся в отдельном потоке (в том же пуле что и ``ActorAction``) не занимая потоки Netty. Актор контроллер (actor action) -------------------------------- Если вы хотите что бы контроллер был актором Akka наследуйтесь от ``ActorAction``: :: import scala.concurrent.duration._ import xitrum.ActorAction import xitrum.annotation.GET @GET("hello") class HelloAction extends ActorAction with AppAction { 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.") } } } Экземпляр актора будет создан на каждый запрос. Актор будет остановлен в момент закрытия подключения или когда ответ будет отправлен клиенту. Для chunked запросов актор будет остановлен когда будет отправлен последний chunk. Актор будет выполняться в пуле потоков Akka в системе с именем "xitrum". Отправка ответа клиенту ----------------------- Что бы отправить данные клиенту используются функции: * ``respondView``: при ответе использует шаблон ассоциированный с контроллером * ``respondInlineView``: при ответе использует шаблон переданный как аргумент * ``respondText("hello")``: ответ строкой "plain/text" * ``respondHtml("...")``: ответ строкой "text/html" * ``respondJson(List(1, 2, 3))``: преобразовать Scala объект в JSON и ответить * ``respondJs("myFunction([1, 2, 3])")`` * ``respondJsonP(List(1, 2, 3), "myFunction")``: совмещение предыдущих двух * ``respondJsonText("[1, 2, 3]")`` * ``respondJsonPText("[1, 2, 3]", "myFunction")`` * ``respondBinary``: ответ массивом байт * ``respondFile``: переслать файл с использованием техники `zero-copy `_ (aka send-file) * ``respondEventSource("data", "event")`` Шаблонизация ------------ Каждый контроллер может быть связан с шаблоном `Scalate `_. В этом случае при вызове метода `respondView` будет задействован данный шаблон для формирования ответа. 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`` подключает стандартные CSS встроенные в Xitrum. Вы можете убрать их если они не требуются * ``jsDefaults`` подключает jQuery, jQuery Validate и пр. Если используется, вызов должен быть размешен в секции * ``jsForView`` использует функцию контроллера ``jsAddToView`` и включает JS фаргмент в шаблон. Если используется, вызов должен быть в конце шаблона В шаблонах допускается использование любых методов из трейта `xitrum.Action `_. Дополнительно можно использовать утильные методы Scalate, такие как ``unescape`` (см. `Scalate doc `_). Синтаксис `Jade `_ используется по умолчанию для Scalate. Так же вы можете использовать синтаксис `Mustache `_, `Scaml `_ или `Ssp `_. Что бы установить предпочитаемый синтаксис, отредактируйте файл xitrum.conf в директории config. Кроме этого, метод `respondView` позволяет переопределять синтаксис шаблона. :: respondView(Map("type" ->"mustache")) currentAction и приведение типов ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Если известен подкласс контроллера который используется с шаблоном, то можно выполнить приведение ``currentAction`` к этому подклассу. :: p= currentAction.asInstanceOf[MyAction].hello("World") Или так: :: - val myAction = currentAction.asInstanceOf[MyAction]; import myAction._ p= hello("World") p= hello("Scala") p= hello("Xitrum") Mustache ~~~~~~~~ Важно: * `Mustache syntax `_ * `Scalate implementation `_ Mustache намеренно ограничивает возможности шаблонизации до минимума логики. Поэтому многие возможности используемые в Jade не применимы в Mustache. Для передачи моделей из контроллера в шаблон необходимо использовать ``at``: Контролер: :: at("name") = "Jack" at("xitrumCss") = xitrumCss Шаблон Mustache: :: Мое имя {{name}} {{xitrumCss}} Примечание: следующие слова зарезервированы и не могут быть использованы как ключ в ``at``: * "context": Scalate объект предоставляющий методы ``unescape`` и пр. * "helper": текущий контроллер CoffeeScript ~~~~~~~~~~~~ Scalate позволяет включать CoffeeScript в шаблоны `:coffeescript filter `_: :: body :coffeescript alert "Hello, Coffee!" Результат: :: Однако, эта возможность работает достаточно `медленно `_: :: 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 для оптимизации производительности. Макет (Layout) -------------- При использовании ``respondView`` или ``respondInlineView``, Xitrum выполняет шаблонизацию в строку, и присваивает результат в переменную ``renderedView``. Затем, Xitrum вызывает метод ``layout`` текущего контроллера и отправляет результат работы этого метода как ответ сервера. По умолчанию метод ``layout`` просто возвращает переменную ``renderedView``. В случае перекрытия этого метода появляется возможность декорировать шаблон. Таким образом достаточно просто реализовать произвольный макет (layout) для всех контроллеров. Механизм ``layout`` очень простой и понятный. Никакой магии. Для удобства, вы можете думать что Xitrum не поддерживает макеты (layout), есть только метод ``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( {antiCsrfMeta} {xitrumCss} {jsDefaults} Welcome to Xitrum {renderedView} {jsForView} ) } Использование макета непосредственно в respondView ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: val specialLayout = () => DocType.html5( {antiCsrfMeta} {xitrumCss} {jsDefaults} Welcome to Xitrum {renderedView} {jsForView} ) respondView(specialLayout _) Внутренние представления ------------------------ Обычно, шаблон описывается в отдельном файле, но существует возможность писать шаблоны непосредственно в контроллере: :: import xitrum.Action import xitrum.annotation.GET @GET("myAction") class MyAction extends Action { def execute() { val s = "World" // Will be automatically HTML-escaped respondInlineView(

Hello {s}!

) } } Фрагменты --------- MyAction.jade: ``scr/main/scalate/mypackage/MyAction.jade`` Шаблонизация с помощью фрагмента ``scr/main/scalate/mypackage/_MyFragment.jade``: :: renderFragment[MyAction]("MyFragment") Можно записать короче, если ``MyAction`` - текущий контроллер: :: renderFragment("MyFragment") Использование шаблона смежного контроллера ------------------------------------------ Использование метода ``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]() } } Один контроллер - много представлений ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Использование нескольких шаблонов для одного контроллера: :: package mypackage import xitrum.Action import xitrum.annotation.GET // Шаблоны автоматически не маршрутизируются // 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]() } } } Использование дополнительных не автоматических маршрутов выглядит утомительно, однако это более безопасно относительно типов (typesafe). Вы также можете использовать `` String`` указать местоположение шаблона: :: respondView("mypackage/HomeAction_NormalUser") respondView("mypackage/HomeAction_Moderator") respondView("mypackage/HomeAction_Admin") Компонент --------- Компоненты позволяют создавать переиспользуемое поведение и могут быть включены во множество представлений. Концептуально компонент очень близок к контроллеру, но: * Не имеет маршрутов, поэтому отсутствует метод ``execute``. * Компонент не отправляет ответ сервера, он просто выполняет шаблонизацию фрагмента. Поэтому внутри компонента, вместо вызовов ``respondXXX``, необходимо использовать ``renderXXX``. * Как и контроллеры, компонент может иметь ни одного, одно или множество связанных представлений. :: 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()