Контроллеры и представления¶
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("<html>...</html>")
: ответ строкой “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 и пр. Если используется, вызов должен быть размешен в секции <head>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 намеренно ограничивает возможности шаблонизации до минимума логики. Поэтому многие возможности используемые в 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!"
Результат:
<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 для оптимизации производительности.
Макет (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(
<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 _)
Внутренние представления¶
Обычно, шаблон описывается в отдельном файле, но существует возможность писать шаблоны непосредственно в контроллере:
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>
)
}
}
Фрагменты¶
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()