Freewind @ Thoughtworks scala java javascript dart 工具 编程实践 月结 math python english [comments admin] [feed]

(2015-01-06) 选用一个Scala模板引擎

广告: 云梯:翻墙vpn (省10元) 土行孙:科研用户翻墙http proxy (有优惠)

我的博客生成器是用Scala做的,既然是“生成器”,那就少不了模板,少不了模板引擎。

我在最开始的时候考虑了两个Scala的模板引擎:

  1. Scalate: http://scalate.github.io/scalate/
  2. Twirl: https://github.com/playframework/twirl

最开始对Scalate非常看好,因为印象中它是一个比较早也比较出名的Scala模板引擎,提供了多种语法,也有貌似很详细的文档。然而当我想项目中使用它的时候,惊讶地发现,它不知道什么时候变成了一个web framework。虽然提供了详细的语法文档,却找不到任何在普通Scala项目中使用它的办法!

Twirl是将Playframework2.x内置的scala模板引擎,抽取出来单独发布了。twirl@的一种发音,而该符号在模板中大量使用。它在文档中介绍了如何通过sbt插件来使用它,但是那时候我对SBT还不太熟悉,不知道如何下手,所以也没有采用。

最后选择的是一个Java版的mustache框架:mustachejava

但是在后来的使用中发现了一些问题:

  1. 无类型。如果需要的数据没有提供,也不会报错,对于一个Scala项目来说,这种无静态类型的模板让人感觉很不适应
  2. 类型需要转换。有些Scala的类型(如集合),需要转成Java的
  3. 不能表达复杂的视图逻辑。比如显示一棵目录树,不太好实现
  4. 如果操作系统的字符集不是utf8,则读取中文文件会变问号。原因是里面有一些读取文件的代码,没有指定字符集。见我提的Issue

最近实在无法忍受,决定把它换成Twirl。现在已经成功了,因为你看到的这稿博客就是它生成的。

使用方法比较简单,直接照着它的readme来就行了,这里记录一些需要注意的东西。

文件后缀

注意文件名形如{name}.scala.{ext},中间一定要有一个scala,后缀是html,js, xml,或者txt之一。

我开始的时候忘了写scala,结果怎么都出不来,反复检查之后才发现,一定要注意。

如何编译成Scala代码

Twirl的模板需要编译成scala源代码,以一个object的方式供其它Scala代码使用。在Play中,这是自动的,但是对于普通Scala项目,我们必须手动操作。

在Sbt命令中行执行:

compile

它就会在target/scala-version/twirl/下生成相应的Scala源文件,并且以后缀分组。

比如index.scala.html会变成html/index.scalauser.scala.js会变成js/user.scala

如何在Scala代码中调用

Twirl生成的scala文件形如:

package html

import play.twirl.api._
import play.twirl.api.TemplateMagic._


object category extends BaseScalaTemplate[HtmlFormat.Appendable,Format[HtmlFormat.Appendable]](HtmlFormat) with Template1[Category, HtmlFormat.Appendable] {

  def apply(category:Category):HtmlFormat.Appendable = {
    _display_ {
        ...
    }
  }

  def render(category:Category,):HtmlFormat.Appendable = apply(category)

  def f:((Category) => HtmlFormat.Appendable) = (category) => apply(category)

  def ref: this.type = this

}

如果我们想在普通的Scala代码中调用它,如下:

val someCategory = new Category("Scala")
val content = html.category.render(comeCategory).toString()
...

需要注意的是html.category.render(comeCategory)的返回类型是HtmlFormat.Appendable,需要调用它的.toString()方法才能得到一个String

与IDEA配合使用

如果我们使用Sbt的命令行来编译,不会遇到任何问题,所有的twirl模板会被正确的编译为scala代码,而所有其它的Scala代码也能正确地找到它们。但是在IDEA中,模板并不会自动的转为Scala代码,所以如果我们直接在其它Scala代码中调用它们,将会报编译错误。

我们需要做两件事:

  1. target/scala-version/twirl设置为IDEA项目的“源目录”
  2. 同时在命令行上运行sbt ~compile,当模板文件发生变化时,自动在后台编译

这样IDEA才能及时获取编译后的scala模板文件。需要经常去看看有没有报一些编译错误,及时修正,否则IDEA看到的还是旧代码。稍有不便,但还可以接受。

运行时依赖

我们还需要在build.sbt中添加额外的依赖,否则会报某些类找不到:

libraryDependencies ++= Seq(
  "com.typesafe.play" %% "twirl-api" % "1.0.4"
)

使用感受

这是一套极为强大的Scala模板引擎,静态类型,与Scala相似的语法,功能很多,可以轻松实现出一些复杂的页面(比如通过在模板内定义一个嵌套的函数,生成树):

@showCategory(category:Category) = {
    <ol class="posts">
        <div class="category_title">@category.name</div>
        @for(post <- category.posts) {
            <li>
                @partials.html.post_title(post)
            </li>
        }
    </ol>
    @for(sub <- category.subCategories) {
        @showCategory(sub) // 递归在这里!!!
    }
}

@showCategory(category)

目前唯一不方便的是,必须手动把它编译成scala文件才能使用,这样的话,我就没办法只提供一些单独的twirl模板直接使用了(而之前用mustachejava)就可以,用户可以随便改,马上用。

我想是有办法让它即时编译的,因为Play2自己就实现了。但是我现在对这个需求不强烈,因为只是自动使用,改完编译一下也不嫌麻烦,以后有需求时再考虑。

comments powered by Disqus