Эта статья является шестой в серии о моих приключениях по разработке веб-приложения на HTML5, Play Scala, CoffeeScript и Jade. Предыдущие статьи можно найти по адресу:
- Интеграция Scalate и Jade с Play 1.2.3
- Попытка заставить CoffeeScript работать с Scalate and Play
- Интеграция HTML5 Boilerplate с Scalate и Play
- Разработка с использованием HTML5, CoffeeScript и Twitter Bootstrap
- Слушать проблемы Anala, Heroku и PostgreSQL в Scala
На прошлой неделе я написал о своих приключениях с Anorm и упомянул, что внес некоторые улучшения в совместимость Scalate Play. Прежде всего, я использовал черту Scalate и класс ScalateTemplate для визуализации шаблонов Jade в моем приложении. Я описал эту настройку в моей первой статье о Scalate and Play .
Добавление функций SiteMesh и переменных по умолчанию
Когда я начал делать свое приложение хорошо выглядящим с помощью CSS, я начал мечтать о функции, которую я использовал в SiteMesh. То есть иметь идентификатор тела или класс, который может идентифицировать страницу и разрешать постраничные правила CSS. Чтобы сделать это с помощью SiteMesh, на вашей странице должно быть что-то вроде следующего:
<body id="signup"/>
А затем прочитайте это в вашем декораторе:
<body<decorator:getproperty property="body.id" writeentireproperty="true"><decorator:getProperty property="body.class" writeEntireProperty="true"/>> </decorator:getproperty>
Когда я начал изучать, как это сделать, я наткнулся на ScalaController в Play Scala и на то, как он заполняет переменные Play по умолчанию (запрос, ответ, flash, параметры и т. Д.). Основываясь на этих новых знаниях, я добавил метод populateRenderArgs (), чтобы установить все переменные по умолчанию и желаемую переменную bodyClass .
def populateRenderArgs(args: (Symbol, Any)*): Map[String, Any] = {
val renderArgs = Scope.RenderArgs.current();
args.foreach {
o =>
renderArgs.put(o._1.name, o._2)
}
renderArgs.put("session", Scope.Session.current())
renderArgs.put("request", Http.Request.current())
renderArgs.put("flash", Scope.Flash.current())
renderArgs.put("params", Scope.Params.current())
renderArgs.put("errors", validationErrors)
renderArgs.put("config", Play.configuration)
// CSS class to add to body
renderArgs.put("bodyClass", Http.Request.current().action.replace(".", " ").toLowerCase)
renderArgs.data.toMap
}
implicit def validationErrors:Map[String,play.data.validation.Error] = {
import scala.collection.JavaConverters._
Map.empty[String,play.data.validation.Error] ++
Validation.errors.asScala.map( e => (e.getKey, e) )
}
После добавления этого метода я смог получить доступ к этим значениям в моих шаблонах, определив их в верхней части:
-@ val bodyClass: String -@ val params: play.mvc.Scope.Params -@ val flash: play.mvc.Scope.Flash
А затем читая их значения в моем шаблоне:
body(class=bodyClass)
...
- if (flash.get("success") != null) {
div(class="alert-message success" data-alert="alert")
a(class="close" href="#") &×
| #{flash.get("success")}
- }
...
fieldset
legend Leave a comment →
div.clearfix
label(for="author") Your name:
input(type="text" name="author" class="xlarge" value={params.get("author")})
div.clearfix
label(for="content") Your message:
textarea(name="content" class="xlarge") #{params.get("content")}
div.actions
button(type="submit" class="btn primary") Submit your comment
button(type="reset" class="btn") Cancel
Для запроса, такого как Home / index, тег body теперь отображается как:
<body class="home index">
Это позволяет группировать стили CSS по именам контроллеров, а также по именам методов.
Затем я начал разрабатывать формы и логику валидации. Я быстро обнаружил, что мне нужен метод action (), подобный методу, определенному в классе TemplateMagic ScalaTemplate .
def action(action: => Any) = {
new play.mvc.results.ScalaAction(action).actionDefinition.url
}
Поскольку TemplateMagic является внутренним классом, я решил, что копирование метода в мой класс ScalateTemplate было самым простым обходным путем. После этого я смог импортировать метод и использовать его в своих шаблонах.
-import controllers.ScalateTemplate._
...
form(method="post" class="form-stacked" id="commentForm"
action={action(controllers.Profile.postComment(workout._1.id()))})
После получения правильного URL-адреса, записанного в атрибуте действия моей формы, я столкнулся с новой проблемой. Play Scala Учебник объясняет поток проверки следующим образом :
if (Validation.hasErrors) {
show(postId)
} else {
Comment.create(Comment(postId, author, content))
Action(show(postId))
}
Однако, когда у меня были ошибки проверки, я получаю следующую ошибку:
Could not load resource: [Timeline/postComment.jade]
Чтобы исправить это, я добавил логику в свою черту Scalate, которая ищет переменную «template» перед использованием Http.Request.current (). Action.replace («.», «/») Для имени. После внесения этого изменения я смог использовать следующий код для отображения ошибок проверки.
if (Validation.hasErrors) {
renderArgs.put("template", "Timeline/show")
show(postId)
} else {
Comment.create(Comment(postId, author, content))
Action(show(postId))
}
Затем я хотел дать дочерним страницам возможность устанавливать контент на родительских страницах. С SiteMesh я мог бы использовать тег <content> следующим образом:
<content tag="underground"> HTML goes here </content>
Этот HTML-код можно затем извлечь в декораторе с помощью тега <decorator: getProperty>:
<decorator:getProperty property="page.underground"/>
С Scalate я нашел, что одинаково легко использовать метод captureAttribute () . Например, вот как я записал список тренировок атлета для отображения на боковой панели.
- captureAttribute("sidebar")
- Option(older).filterNot(_.isEmpty).map { workouts =>
.older-workouts
h3
| Older workouts
span.from from this app
- workouts.map { workout =>
- render("workout.jade", Map('workout -> workout, 'mode -> "teaser"))
- }
- }
- }
Затем в моем макете я смог получить это и отобразить. Ниже приведен фрагмент макета, который я использую (скопированный из примера Bootstrap в Twitter ). Вы можете увидеть, как боковая панель включена в .span4 в конце.
-@ val sidebar: String = ""
...
.container
.content
.page-header
h1
= pageHeader
small
= pageTagline
.row
.span10
!~~ body
.span4
= unescape(sidebar)
footer
Представление и рендеринг в Scalate
В приведенном выше коде боковой панели вы можете заметить вызов render () . Это Scalate-версия серверных включений . Это работает хорошо, но есть также ярлык view (), который вы можете использовать, если хотите иметь шаблоны для рендеринга объектов модели. Я быстро обнаружил, что это может быть трудно использовать эту функцию в моем приложении, потому что мой объект был Option [(models.Workout, models.Athlete, Seq [models.Comment])] вместо простого объекта. Вы можете прочитать тему просмотра и визуализации в Scalate Google Group, если вам интересно узнать больше.
Scalate as the Module
Последнее усовершенствование, которое я попытался сделать, — добавить поддержку Scalate в модуль Play . Сначала я попробовал переопределить класс Template Play, но столкнулся с проблемами компиляции . Затем Гийом Борт (ведущий разработчик Play) порекомендовал мне придерживаться подхода, основанного на особенностях, и я смог заставить все работать. Я посмотрел на устаревший модуль play-scalate, чтобы выяснить, как добавить поддержку Scala в build.xml, и скопировал его страницу 500.scaml для сообщения об ошибках.
Для того, чтобы получить отчет об ошибке с точностью до строки, мне пришлось обернуться попыткой вызова метода Scaleate TemplateEngine.layout () . Опять же, большая часть этого кода была скопирована из устаревшего модуля play-scalate.
case class Template(name: String) {
def render(args: (Symbol, Any)*) = {
val argsMap = populateRenderArgs(args: _*)
val buffer = new StringWriter()
var context = new DefaultRenderContext(name, scalateEngine, new PrintWriter(buffer))
try {
val templatePath = new File(Play.applicationPath+"/app/views","/"+name).toString
.replace(new File(Play.applicationPath+"/app/views").toString,"")
scalateEngine.layout(templatePath + scalateType, argsMap)
} catch {
case ex:TemplateNotFoundException => {
if(ex.isSourceAvailable) {
throw ex
}
val element = PlayException.getInterestingStrackTraceElement(ex)
if (element != null) {
throw new TemplateNotFoundException(name,
Play.classes.getApplicationClass(element.getClassName()), element.getLineNumber());
} else {
throw ex
}
}
case ex:InvalidSyntaxException => handleSpecialError(context,ex)
case ex:CompilerException => handleSpecialError(context,ex)
case ex:Exception => handleSpecialError(context,ex)
} finally {
if (buffer.toString.length > 0)
throw new ScalateResult(buffer.toString,name)
}
}
}
...
private def handleSpecialError(context:DefaultRenderContext,ex:Exception) {
context.attributes("javax.servlet.error.exception") = ex
context.attributes("javax.servlet.error.message") = ex.getMessage
try {
scalateEngine.layout(scalateEngine.load(errorTemplate), context)
} catch {
case ex:Exception =>
// TODO use logging API from Play here...
println("Caught: " + ex)
ex.printStackTrace
}
}
private def errorTemplate:String = {
val fullPath = new File(Play.applicationPath,"/app/views/errors/500.scaml").toString
fullPath.replace(new File(Play.applicationPath+"/app/views").toString,"")
}
Как только я это сделал, сообщения об ошибках от Scalate стали намного лучше. Я не только вижу ошибку в своем браузере, но я также могу щелкнуть по строке с ошибкой, чтобы открыть ее прямо в TextMate.
Я опубликовал свой модуль play-scalate на GitHub, чтобы другие могли его опробовать. Чтобы сделать это, добавьте следующее в файл зависимости:.
- upgrades -> play-scalate 0.1
repositories:
- upgrades:
type: http
artifact: "http://static.raibledesigns.com/[module]-[revision].zip"
contains:
- upgrades -> *
Затем добавьте с помощью play.modules.scalate.Scalate к своим контроллерам и вызовите метод render () .
Резюме
После использования Scalate и Play в течение почти 3 месяцев, я действительно наслаждаюсь комбинацией. Когда я впервые интегрировал Scalate с простой чертой, сообщения об ошибках всегда были в консоли. Теперь, когда я позаимствовал некоторые смарты у Play ScontController и отчетов об ошибках play-scalate, я чувствую, что это практически встроенное решение. Я легко смог интегрировать нужные функции SiteMesh, и он даже позволяет многократно использовать шаблоны блоков . В конце концов, это всего лишь Scala, и Scalate хорошо справляется с этой задачей.
Другие мысли:
- Если вы много пишете на Jade и знакомы с HTML, html2jade от Don Park — отличный инструмент с поддержкой Scalate.
- Мне действительно нравится писать CSS с LESS , особенно возможность вкладывать правила и иметь функции программирования. Единственная проблема, с которой я столкнулся, это то, что плагин IntelliJ LESS выполняет кодовое завершение только для переменных, а не для значений CSS.
- Play Framework Поваренная книга является хорошим справочником для обучения , как писать модули. Он не только объясняет, как создавать модули, но и содержит некоторые примеры из реальной жизни, которые позволяют усовершенствовать байт-код, реализовать очереди сообщений, использовать Solr и как осуществлять производственный мониторинг.
Если эта серия статей заинтриговала вас и вы будете в Devoxx на следующей неделе, вам следует остановиться на моем выступлении в четверг днем . Кроме того, есть несколько других выступлений в Play на Devoxx и возможная встреча в среду. Проверьте Devoxx, кто-нибудь? нить для получения дополнительной информации.
Обновление : есть одна вещь, которую я забыл упомянуть о модуле Play Scalate. Когда у меня была Scalate, интегрированная в мое приложение с особенностью, я включал только JAR-файлы scalate-core и scalate-util в dependencies.yml:
- org.fusesource.scalate -> scalate-core 1.5.2-scala_2.8.1:
transitive: false
- org.fusesource.scalate -> scalate-util 1.5.2-scala_2.8.1:
transitive: false
Однако когда я создал модуль play-scalate, я допустил больше зависимостей.
- org.fusesource.scalate -> scalate-core 1.5.2-scala_2.8.1:
exclude:
- javax.servlet -> *
- com.sun.jersey -> *
- org.osgi -> *
- org.fusesource.scalate -> scalate-util 1.5.2-scala_2.8.1
Поскольку Scalate зависит от
Logback , отладочные сообщения начали появляться в моей консоли. Чтобы это исправить, я создал
conf / logback.xml в своем проекте и заполнил его следующим XML.
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
Это сокращает ведение журнала и позволяет мне увеличить ведение журнала Scalate, если мне когда-либо понадобится.
От http://raibledesigns.com/rd/entry/more_scalate_goodness_for_play
