Эта статья является шестой в серии о моих приключениях по разработке веб-приложения на 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