莎氏模板

莎氏模板是Yesod创建HTML、CSS和Javascript的标准模板语言。这组模板语言有一些共同 的语法及总体原则:

  • 最低限度的干涉下层语言,同时在不唐突的地方提供便利。

  • 编译时保证内容合乎规范。

  • 静态类型安全,极大程度的防止 跨站脚本(XSS: cross-site scripting)攻击。

  • 尽可能通过类型安全URL来自动检查URL是否有效

Yesod并没有和这些模板语言捆绑在一起,反之亦然:它们都可以相互独立的使用。本章 会各自讲解这些模板语言,本书其余部分将使用这些模板语言来增强Yesod应用开发。

概要

这里共有四种模板语言:Hamlet是HTML模板语言,Julius是Javascript模板语言, Cassius和Lucius都是CSS模板语言。Hamlet和Cassius在格式上都对空格符敏感,用缩进 表示嵌套。相比之下,Lucius是CSS的超集,保留了CSS用括弧表示嵌套的方式。Julius几 乎就是Javascript;唯一加入的特性是变量插值。

注意 Cassius实际上只是Lucius的另一种写法。它们使用了相同的处理引擎,但Cassius文 件会在处理前将缩进转换成括弧。在它们俩中做选择完全是语法偏好问题。

Hamlet (HTML)

  1. $doctype 5
  2. <html>
  3. <head>
  4. <title>#{pageTitle} - My Site
  5. <link rel=stylesheet href=@{Stylesheet}>
  6. <body>
  7. <h1 .page-title>#{pageTitle}
  8. <p>Here is a list of your friends:
  9. $if null friends
  10. <p>Sorry, I lied, you don't have any friends.
  11. $else
  12. <ul>
  13. $forall Friend name age <- friends
  14. <li>#{name} (#{age} years old)
  15. <footer>^{copyright}

Cassius (CSS)

  1. #myid
  2. color: #{red}
  3. font-size: #{bodyFontSize}
  4. foo bar baz
  5. background-image: url(@{MyBackgroundR})

Lucius (CSS)

  1. section.blog {
  2. padding: 1em;
  3. border: 1px solid #000;
  4. h1 {
  5. color: #{headingColor};
  6. }
  7. }

Julius (Javascript)

  1. $(function(){
  2. $("section.#{sectionClass}").hide();
  3. $("#mybutton").click(function(){document.location = "@{SomeRouteR}";});
  4. ^{addBling}
  5. });

类型

在讲解语法前,我们先看看涉及到的类型。我们在“引言”一章中讲到类型会帮我们阻止 XSS攻击。比如说,我们有一个HTML模板用来显示人名。它可能是这样的:

  1. <p>Hello, my name is #{name}
注意 #{…}是莎氏模板里进行变量插值(variable interpolation)的语法。

name会被替换成什么,它的数据类型是什么?一个幼稚的方法是用Text类型的值,然 后逐字插入。但如果name的值是下面这样的,会给我们带来很大的问题:

  1. <script src='http://nefarious.com/evil.js'></script>

我们希望能够对名字进行实体编码(entity-encode),因此<会变成&lt;。

同样幼稚的一种办法是对每个词都做实体编码。如果你有HTML是从另一个函数生成的呢? 比如,在Yesod官网上,所有Haskell代码都被一个代码高亮函数处理过,这个函数将词嵌 套在适当的span标签内。如果我们编码所有的词,代码块将不具有可读性!

与此相反,我们有一个Html数据类型。为了生成Html值,我们有两个API可选: ToMarkup型类提供了toHtml函数,它能将String和Text值转为Html,并自动转 义实体。这是我们对上面人名例子的解决方法。对于代码高亮的例子,我们要用 preEscapedToMarkup函数。

当你在Hamlet中使用变量插值时,它会对所要插入的值自动应用toHtml函数。所以如果 你插入的是一个String值,它会被转义。但如果你提供的是一个Html值,它就会保持 原样。在代码高亮例子中,我们会这样写#{preEscapedToMarkup myHaskellHtml}。

注意 Html数据类型以及我们提到的几个函数,都来自于blaze-html包。这使得Hamlet 可以与所有其它blaze-html包交互,并且Hamlet能提供一种通用的生成blaze-html值的方 法。另外,我们还能充分利用blaze-html的惊人性能。

类似的,我们也有Css/ToCss和Javascript/ToJavascript函数。这些会在编译时 进行一些合法性校验,以保证我们不会不小心在CSS里嵌入HTML代码。

注意 CSS模板的另一个优势是有一些颜色和单位的辅助数据类型。比如:
  1. .red { color: #{colorRed} }
更多详情请参阅Haddock文档。

类型安全URL (Type-safe URLs)

Yesod最独特的特性可能要数类型安全URL了,正是莎氏模板使我们能够方便的使用类型安 全URL。其用法和变量插值几乎一样;我们只需要用@符号而不是#符号就可以。我们会在 稍后讲解语法;首先,让我们讲讲起因。

假设我们的应用有两条路由:http://example.com/profile/home是首页, http://example.com/display/time显示当前时间。假设我们想从首页链接到时间页 面。我能想到三种不同的构造URL的方法:

这三种方法都有各自的问题:第一种方法只要其中有一个URL改变了,就会失效。同样的 ,它不适用于所有的应用场合;比如说RSS和Atom源需要的是绝对路径。第二种方法比第 一种方法更能抵抗变化,但还是不能用于RSS和Atom源。虽然第三种方法能适用于所有应 用场合,但如果你的域名发生改变,你就需要去更新应用里的每一个URL。你觉得这不会 经常发生?等你从开发环境迁移到升级(staging)环境再迁移到生产环境就知道了。

但更为重要的是,这些方法都有个大问题,如果你修改了你的路由,编译器不会对失效链 接发出警告。更不用说拼写错误都能造成严重破坏。

类型安全URL的目的是尽量让编译器为我们检查。为了做到这一点,我们的第一步是必须 放弃使用普通文本,而是用明确定义的数据类型,因为普通文本是编译器所不能理解的。 对于这个简单的应用,我们用一个汇总类型(sum type)来建模我们的路由。

  1. data MyRoute = Home | Time

我们在模板中不使用/display/time这样的链接,而是使用Time这个构造函数。但最终 ,HTML还是由文本构成的,而不是数据类型,所以我们需要能将这些值转为文本的方法。 我们称之为URL呈现函数(rendering function),下面是一个简单的样例:

  1. renderMyRoute :: MyRoute -> Text
  2. renderMyRoute Home = "http://example.com/profile/home"
  3. renderMyRoute Time = "http://example.com/display/time"
注意 URL呈现函数实际上比这个要复杂一些。它们需要表示请求参数(query string parameters)、处理构造函数中的记录(record),并且智能的处理域名。但实际上 ,你不需要担心这些,因为Yesod会自动为你生成URL呈现函数。需要指出的一点是,为了 处理请求参数,类型标识(type signature)会略微复杂:
  1. type Query = [(Text, Text)]
  2. type Render url = url -> Query -> Text
  3. renderMyRoute :: Render MyRoute
  4. renderMyRoute Home =
  5. renderMyRoute Time =

好了,我们在有了呈现函数,在模板里有了类型安全的URL。这些究竟是怎么结合在一起 的呢?我们不是直接生成Html(或Css、Javascript),莎氏模板实际上生成的是一 个函数,它的输入是呈现函数,输出是HTML。为了看清楚这点,我们看一个(假的) Hamlet底层细节。假设我们有这样一个模板:

  1. <a href=@{Time}>The time

它大概可以翻译成如下Haskell代码:

  1. \render -> mconcat ["<a href='", render Time, "'>The time</a>"]

语法

所有莎氏语言都共用一套插值语法,并且都能够使用类型安全的URL。它们的差异体现在 与其目标语言(HTML、CSS、Javascript)相对应的语法上。

Hamlet语法

Hamlet是莎氏模板语言里最复杂的。不仅因为它提供了生成HTML的语法,它也允许简单的 控制结构:条件、循环和或许(maybe)。

标签(Tags)

显然,标签在任何HTML模板语言中都起重要作用。在Hamlet中,我们试图与现有HTML语法 保持高度一致,这样使用起来更自然。然而,我们使用缩进来表示嵌套,而不用关闭标签 。因此这样的HTML代码:

  1. <body>
  2. <p>Some paragraph.</p>
  3. <ul>
  4. <li>Item 1</li>
  5. <li>Item 2</li>
  6. </ul>
  7. </body>

对应的Hamlet代码是

  1. <body>
  2. <p>Some paragraph.
  3. <ul>
  4. <li>Item 1
  5. <li>Item 2

通常来说,我们发现你习惯Hamlet以后会写起来会比HTML更容易。唯一有点技巧的地方是 处理标签前后的空格字符。比如,如果我们要创建这样的HTML

  1. <p>Paragraph <i>italic</i> end.</p>

我们想确保“Paragraph“后和“end”前的空格符被保留。为了做到这一点,我们使用两个简 单的转义字符:

  1. <p>
  2. Paragraph #
  3. <i>italic
  4. \ end.

空格转义字符实际上非常简单:

  • 如果一行的第一个非空字符是反斜线(),则反斜线被忽略。

  • 如果一行的最后一个字符是#号,则#号被忽略。

另外,Hamlet对内容中的实体(entity)进行转义。这么做是有意的,是为了让拷贝已 有HTML代码到Hamlet更加容易。因此上面的例子也可以写成这样:

  1. <p>Paragraph <i>italic</i> end.

注意,这样写的话第一个标签(<p>)会由Hamlet自动关闭,而嵌套的<i>不会。用哪种写法 你可以自由选择,它们都没有什么负面影响。然而当心,你在Hamlet中能对这些内联 标签使用关闭标签;正常的标签不能手动关闭。

插值

到目前为止,我们有一个不错、简化的HTML实现,但它还完全不能和Haskell代码交互。 我们怎么传入变量呢?很简单:用变量插值:

  1. <head>
  2. <title>#{title}

#号后紧跟一对花括号表示这是一个变量插值。在这面这个例子中,被插入的值是调用 模板时作用域内的title变量值。让我再说一次:Hamlet在被调用时,自动拥有了访问 作用域内变量的权限。不需要特意将变量传入。

你可以在变量插值中调用函数。也可以使用字符串和数字。可以使用限定模块(qualified modules)。可以用括号和美元符号来组合表达式(statements)。最后,toHtml函数会作 用在结果上,意味着任何ToHtml的实例都可以做插值。比如,下面的代码。

  1. -- 暂时忽略准引用(quasiquote)和shamlet。后面后讲解。
  2. {-# LANGUAGE QuasiQuotes #-}
  3. import Text.Hamlet (shamlet)
  4. import Text.Blaze.Html.Renderer.String (renderHtml)
  5. import Data.Char (toLower)
  6. import Data.List (sort)
  7. data Person = Person
  8. { name :: String
  9. , age :: Int
  10. }
  11. main :: IO ()
  12. main = putStrLn $ renderHtml [shamlet|
  13. <p>Hello, my name is #{name person} and I am #{show $ age person}.
  14. <p>
  15. Let's do some funny stuff with my name: #
  16. <b>#{sort $ map toLower (name person)}
  17. <p>Oh, and in 5 years I'll be #{show ((++) 5 (age person))} years old.
  18. |]
  19. where
  20. person = Person "Michael" 26

我们大肆吹捧的类型安全URL是怎样(做插值)的呢?他们和变量插值几乎是一样的,除了 它们是以@符开头。另外,^符允许你插入另一个同类模板。下面的代码示例能说明这两种 插值。

  1. {-# LANGUAGE QuasiQuotes #-}
  2. {-# LANGUAGE OverloadedStrings #-}
  3. import Text.Hamlet (HtmlUrl, hamlet)
  4. import Text.Blaze.Html.Renderer.String (renderHtml)
  5. import Data.Text (Text)
  6. data MyRoute = Home
  7. render :: MyRoute -> [(Text, Text)] -> Text
  8. render Home _ = "/home"
  9. footer :: HtmlUrl MyRoute
  10. footer = [hamlet|
  11. <footer>
  12. Return to #
  13. <a href=@{Home}>Homepage
  14. .
  15. |]
  16. main :: IO ()
  17. main = putStrLn $ renderHtml $ [hamlet|
  18. <body>
  19. <p>This is my page.
  20. ^{footer}
  21. |] render
属性

在上一个例子中,我们给“a“标签赋予了href属性值。让我们来讲讲(属性的)语法:

  • 你可以在属性值中使用变量插值。

  • 等号和属性值是可选的,就像在HTML里那样。因此 是完全合法的。

  • 有两种便捷的属性定义方法:对于id,你可以用#号,对于class,你可以用点(.)。换 句话说,(可以这样定义:)

  • 属性值两边的引号是可选的,但如果你要属性值内有空格,则引号是必须的。

  • 你可以用冒号来选择性的增加属性。要让一个复选框(checkbox)仅当isChecked变量为 真时才被选中,可以这样写

条件语句

最终,你会想要在页面上增加(控制)逻辑。Hamlet的目标是尽量简化逻辑,将繁重的任务 交给Haskell代码。因此,我们的逻辑语句非常基础… 基础到只有if、elseif和 else。

  1. $if isAdmin
  2. <p>Welcome to the admin section.
  3. $elseif isLoggedIn
  4. <p>You are not the administrator.
  5. $else
  6. <p>I don't know who you are. Please log in so I can decide if you get access.

普通的插值规则,同样也可以用在条件语句中。

或许(Maybe)

类似的,我们也有专门的结构来处理Maybe值。技术上它们可以用if、isJust和 fromJust来实现,但这样(的专门结构)更方便,也避免了偏函数(partial functions) 。

  1. $maybe name <- maybeName
  2. <p>Your name is #{name}
  3. $nothing
  4. <p>I don't know your name.

在(←)左侧,你除了可以用简单的标识符,还可以使用更复杂的值,比如构造函数和元组 (tuple)。

  1. $maybe Person firstName lastName <- maybePerson
  2. <p>Your name is #{firstName} #{lastName}

(←)右侧遵循与插值相同的规则,允许使用变量、调用函数等。

Forall语句

怎么对列表做循环呢?我们也有相应的结构:

  1. $if null people
  2. <p>No people.
  3. $else
  4. <ul>
  5. $forall person <- people
  6. <li>#{person}
Case语句

模式匹配是Haskell的一大强项。汇合类型(Sum types)让你能清晰的建模真实世界的类型 ,case语句让你安全的做模式匹配,如果你忘了处理某一种情况,编译器会发出警告。 Hamlet也能做到这点。

  1. $case foo
  2. $of Left bar
  3. <p>It was left: #{bar}
  4. $of Right baz
  5. <p>It was right: #{baz}
With语句

我们可以用with来概括一个语句。这样做基本上是为了给一条长语句声明一个别名。

  1. $with foo <- some very (long ugly) expression that $ should only $ happen once
  2. <p>But I'm going to use #{foo} multiple times. #{foo}
Doctype语句

最后一个语法糖:doctype语句。我们支持不同版本的doctype,不过我们给时下大多数 web应用推荐$doctype 5,它会生成<!DOCTYPE html>。

  1. $doctype 5
  2. <html>
  3. <head>
  4. <title>Hamlet is Awesome
  5. <body>
  6. <p>All done.
注意 还有一种旧的但仍被支持的语法:三个感叹号(!!!)。你可能在代码里还见过这 种用法。我们不打算移除它,但通常来说,用$doctype更易读。

Lucius语法

Lucius是莎氏模板里两种CSS模板语言之一。它被设计成CSS的超集,依托现有的CSS语法 ,同时加入更多功能。

  • 与Hamlet类似,我们允许变量和URL插值。

  • 允许嵌套CSS块。

  • 可以在模板中定义变量。

  • 可以将一组CSS属性定义成mixin,然后多处复用。

从第二点开始:假设你想对article标签内的几个标签应用特殊的样式。用普通的CSS, 你必须这样写:

  1. article code { background-color: grey; }
  2. article p { text-indent: 2em; }
  3. article a { text-decoration: none; }

这个例子虽然只有几句话,但每次都必须打出article还是有点讨厌。想象你有十几个这 样的语句。虽然不是最糟糕的事,但还是有点烦人。Lucius可以帮到你:

  1. article {
  2. code { background-color: grey; }
  3. p { text-indent: 2em; }
  4. a { text-decoration: none; }
  5. }

Lucius变量可以让你避免重复。一个简单的例子是定义共用的颜色:

  1. @textcolor: #ccc; /* just because we hate our users */
  2. body { color: #{textcolor} }
  3. a:link, a:visited { color: #{textcolor} }

Mixin是Lucius较新的特性。它的基本思想是将一组属性声明为一个mixin,然后在模板中 使用^做嵌套。下面的例子说明了我们可以怎样用mixin来处理浏览器前缀(vendor prefix)的问题。

  1. {-# LANGUAGE QuasiQuotes #-}
  2. import Text.Lucius
  3. import qualified Data.Text.Lazy.IO as TLIO
  4. -- 假的呈现函数。
  5. render = undefined
  6. -- 我们的mixin,为transition属性提供了很多浏览器前缀。
  7. transition val =
  8. [luciusMixin|
  9. -webkit-transition: #{val};
  10. -moz-transition: #{val};
  11. -ms-transition: #{val};
  12. -o-transition: #{val};
  13. transition: #{val};
  14. |]
  15. -- 我们实际的Lucius模板,其中使用了mixin
  16. myCSS =
  17. [lucius|
  18. .some-class {
  19. ^{transition "all 4s ease"}
  20. }
  21. |]
  22. main = TLIO.putStrLn $ renderCss $ myCSS render

Cassius语法

Cassius是空格敏感的Lucius版本。在概要中我们已经提到,它和Lucius使用相同的处理 引擎,不过在预处理的时候会在代码块前后插入括弧,在每行末尾插入分号。这意味着你 在写Cassius的时候可以用上Lucius的所有特性。下面是一个简单的例子:

  1. #banner
  2. border: 1px solid #{bannerColor}
  3. background-image: url(@{BannerImageR})

Julius Syntax

Julius是这四种语言里最简单的。实际上,有人甚至会说它就是Javascript。Julius可以 使用我们提过的三种形式的插值,除此以外,不会对内容做任何转换。

注意 如果你在Yesod脚手架站点中使用了Julius,你可能会发现你的Javascript被自动 压缩了。这不是Julius的特性;而是因为Yesod使用了hjsmin包来压缩Julius的输出。

调用莎氏模板

问题来了:我到底怎么使用这些模板呢?有三种方法可以从Haskell代码调用莎氏模板:

  • 准引用
  • 准引用允许你在Haskell代码中嵌套任意内容,并且在编译时将其转换为 Haskell代码。

  • 外部文件

  • 这种情况下,模板代码保存在外部文件中,通过Haskell模板来1调用。

  • 重载模式

  • 上面两种方法在修改代码后都需要完全重编译。在重载模式中,你的模板放 在外部文件里,通过Haskell模板调用。但在运行时,外部文件每次都重新解析。
注意 Hamlet不能使用重载模式,Cassius、Lucius和Julius可以。Hamlet里有太多复杂 的功能直接依赖于Haskell编译器,因此无法在运行时重新实现。

在生产环境中,应该使用前两种方法。它们都把模板整个嵌入最后的可执行文件中,简化 了部署,优化了性能。准引用的好处是简单:所有内容在一个文件中。对于内容少的模板 这样很好。然而,一般都推荐外部文件的方法,因为:

  • 它遵循了将逻辑层与表示层分离的惯例。

  • 你可以方便的用CPP宏在外部文件和调试(重载)模式间切换,也就是说你既可以进行快 速开发,也能在生产环境得到高性能。

由于用到了特殊的准引用和Haskell模板函数,你需要确保启用了相应的语言扩展,并正 确的使用语法。你可以在下面的代码里看到这两种用法。

准引用

  1. {-# LANGUAGE OverloadedStrings #-} -- 我们下面要用Text类型
  2. {-# LANGUAGE QuasiQuotes #-}
  3. import Text.Hamlet (HtmlUrl, hamlet)
  4. import Data.Text (Text)
  5. import Text.Blaze.Html.Renderer.String (renderHtml)
  6. data MyRoute = Home | Time | Stylesheet
  7. render :: MyRoute -> [(Text, Text)] -> Text
  8. render Home _ = "/home"
  9. render Time _ = "/time"
  10. render Stylesheet _ = "/style.css"
  11. template :: Text -> HtmlUrl MyRoute
  12. template title = [hamlet|
  13. $doctype 5
  14. <html>
  15. <head>
  16. <title>#{title}
  17. <link rel=stylesheet href=@{Stylesheet}>
  18. <body>
  19. <h1>#{title}
  20. |]
  21. main :: IO ()
  22. main = putStrLn $ renderHtml $ template "My Title" render

外部文件

  1. {-# LANGUAGE OverloadedStrings #-} -- 我们下面要用Text类型
  2. {-# LANGUAGE TemplateHaskell #-}
  3. {-# LANGUAGE CPP #-} -- 在生产环境和调试(开发)模式间切换
  4. import Text.Lucius (CssUrl, luciusFile, luciusFileDebug, renderCss)
  5. import Data.Text (Text)
  6. import qualified Data.Text.Lazy.IO as TLIO
  7. data MyRoute = Home | Time | Stylesheet
  8. render :: MyRoute -> [(Text, Text)] -> Text
  9. render Home _ = "/home"
  10. render Time _ = "/time"
  11. render Stylesheet _ = "/style.css"
  12. template :: CssUrl MyRoute
  13. #if PRODUCTION
  14. template = $(luciusFile "template.lucius")
  15. #else
  16. template = $(luciusFileDebug "template.lucius")
  17. #endif
  18. main :: IO ()
  19. main = TLIO.putStrLn $ renderCss $ template render
  20. {- @template.lucius
  21. foo { bar: baz }
  22. -}

(调用莎氏模板的)函数有统一的命名规则。

模板语言 准引用 外部文件 重载模式
Hamlet hamlet hamletFile
Cassius cassius cassiusFile cassiusFileReload
Lucius lucius luciusFile luciusFileReload
Julius julius juliusFile juliusFileReload

其它Hamlet类型

目前为止,我们已经看到如何从Hamlet生成HtmlUrl值,它是一段内嵌类型安全URL的 HTML代码。还有三种值可以用Hamlet生成:普通的HTML、含URL以及多语言消息的HTML 、控件。用Hamlet生成控件会在“控件”一章中讲解。

要生成不含URL的普通HTML,我们就使用“简化的Hamlet“。它(与普通的Hamlet相比)有一 些改动:

  • 我们使用另一组以字母“s”打头的函数。因此准引用名为shamlet,外部文件模板函数 为shamletFile。至于这些函数怎么发音仍存争议。

  • 不允许URL插值。URL插值会导致编译错误。

  • ^插值不允许任意的HtmlUrl值。被嵌套的值必须与模板本身的类型一致,所以它必须 也是Html类型。这意味着对于shamlet,内嵌可以完全用普通的变量插值(用#号)替代 。

在Hamlet中处理多国语言(i18n)会有些复杂。Hamlet通过消息数据类型来支持i18n,与类 型安全URL在概念和实现上都非常近似。比如说,我们想要让应用对你说hello并告诉你吃 掉的苹果数。我们可以用这样的数据类型表示消息。

  1. data Msg = Hello | Apples Int

然后,我们希望能将它转换为可读文本,因此定义一些呈现函数:

  1. renderEnglish :: Msg -> Text
  2. renderEnglish Hello = "Hello"
  3. renderEnglish (Apples 0) = "You did not buy any apples."
  4. renderEnglish (Apples 1) = "You bought 1 apple."
  5. renderEnglish (Apples i) = T.concat ["You bought ", T.pack $ show i, " apples."]

现在我们想把这些Msg值直接插入模板。我们要用到下划线插值方法。

  1. $doctype 5
  2. <html>
  3. <head>
  4. <title>i18n
  5. <body>
  6. <h1>_{Hello}
  7. <p>_{Apples count}

还需要能将这样的模板转换为HTML的方法。因此就像类型安全URL,我们传入一个呈现 函数。为了表示它,我们定义一个新的类型别名:

  1. type Render url = url -> [(Text, Text)] -> Text
  2. type Translate msg = msg -> Html
  3. type HtmlUrlI18n msg url = Translate msg -> Render url -> Html

至此,我们可以将renderEnglish,renderSpanish或renderKlingon传入模板,它 会输出翻译好的结果(当然,取决于翻译得有多好)。完整的程序是:

  1. {-# LANGUAGE QuasiQuotes #-}
  2. {-# LANGUAGE OverloadedStrings #-}
  3. import Data.Text (Text)
  4. import qualified Data.Text as T
  5. import Text.Hamlet (HtmlUrlI18n, ihamlet)
  6. import Text.Blaze.Html (toHtml)
  7. import Text.Blaze.Html.Renderer.String (renderHtml)
  8. data MyRoute = Home | Time | Stylesheet
  9. renderUrl :: MyRoute -> [(Text, Text)] -> Text
  10. renderUrl Home _ = "/home"
  11. renderUrl Time _ = "/time"
  12. renderUrl Stylesheet _ = "/style.css"
  13. data Msg = Hello | Apples Int
  14. renderEnglish :: Msg -> Text
  15. renderEnglish Hello = "Hello"
  16. renderEnglish (Apples 0) = "You did not buy any apples."
  17. renderEnglish (Apples 1) = "You bought 1 apple."
  18. renderEnglish (Apples i) = T.concat ["You bought ", T.pack $ show i, " apples."]
  19. template :: Int -> HtmlUrlI18n Msg MyRoute
  20. template count = [ihamlet|
  21. $doctype 5
  22. <html>
  23. <head>
  24. <title>i18n
  25. <body>
  26. <h1>_{Hello}
  27. <p>_{Apples count}
  28. |]
  29. main :: IO ()
  30. main = putStrLn $ renderHtml
  31. $ (template 5) (toHtml . renderEnglish) renderUrl

其它莎氏模板

除了用于生成HTML、CSS和Javascript,还有一些特殊用途的莎氏模板。 shakespeare-text包提供了一种创建文本插值的简单方法,它与Ruby和Python这些脚本语 言中的用法非常相似。这个包的用途绝不限于Yesod。

  1. {-# LANGUAGE QuasiQuotes, OverloadedStrings #-}
  2. import Text.Shakespeare.Text
  3. import qualified Data.Text.Lazy.IO as TLIO
  4. import Data.Text (Text)
  5. import Control.Monad (forM_)
  6. data Item = Item
  7. { itemName :: Text
  8. , itemQty :: Int
  9. }
  10. items :: [Item]
  11. items =
  12. [ Item "apples" 5
  13. , Item "bananas" 10
  14. ]
  15. main :: IO ()
  16. main = forM_ items $ \item -> TLIO.putStrLn
  17. [lt|You have #{show $ itemQty item} #{itemName item}.|]

关于这个例子的几点快速说明:

  • 注意我们涉及到三种不同的文本数据类型(String,strict Text和lazy Text)。 它们可以混合使用。

  • 我们使用了叫lt的准引用,它能生成lazy文本。相应的有st。

  • 另外,这两个准引用也有较长名字的版本(ltext和stext)。

一般建议

以下是来自Yesod社区关于最有效使用莎氏模板的几点提示。

  • 对于真实的网站,使用外部文件。对于类库,如果模板不长的话,可以使用准引用。

  • Patrick Brisbin整理了一个 (莎氏模板)Vim代码高亮 脚本,会很有帮助。

  • 你应该总是给每个Hamlet标签另起一行,而不要在现有标签中嵌套开始/关闭标签。唯 一的例外是大段文本中偶尔的标签。