Статьи

Больше Scalate Goodness для игры

Эта статья является шестой в серии о моих приключениях по разработке веб-приложения на HTML5, Play Scala, CoffeeScript и Jade. Предыдущие статьи можно найти по адресу:

  1. Интеграция Scalate и Jade с Play 1.2.3
  2. Попытка заставить CoffeeScript работать с Scalate and Play
  3. Интеграция HTML5 Boilerplate с Scalate и Play
  4. Разработка с использованием HTML5, CoffeeScript и Twitter Bootstrap
  5. Слушать проблемы 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

Я опубликовал свой модуль 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