Yesod是一个Haskell写的web框架,用于开发类型安全、RESTful、高性能的web应用
目录
引言
Haskell
基础
莎氏模板
控件
Yesod型类
路由和处理函数
表单
会话
Persistent
部署你的web应用

控件 (Widgets)

Web开发的一个挑战是我们要整合三种不同的客户端技术:HTML、CSS和Javascript。更糟 的是,我们必须把它们放在页面的不同位置:CSS要放在头部的style标签内,Javascript 要放在头部的script标签里,HTML要放在正文里。如果你想把CSS和Javascript放在外部 文件中,也完全没有问题!

在实践中,这种做法在构建单个网页时能很好工作,因为我们可以将结构(HTML)、样式 (CSS)和逻辑(Javascript)相互分离。但如果我们想构建容易组合的代码模块,要协调这 三个分离的部分就会有点头疼。控件是Yesod对这一问题的解决方法。控件也能帮助避免 重复引用类库,如jQuery。

我们的四门模板语言——Hamlet、Cassius、Lucius和Julius——提供了构建输出的原始工具 。控件是让它们完美结合在一起的黏合剂。

概要

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes       #-}
{-# LANGUAGE TemplateHaskell   #-}
{-# LANGUAGE TypeFamilies      #-}
import           Yesod

data App = App
mkYesod "App" [parseRoutes|
/ HomeR GET
|]
instance Yesod App

getHomeR = defaultLayout $ do
    setTitle "My Page Title"
    toWidget [lucius| h1 { color: green; } |]
    addScriptRemote "https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"
    toWidget
        [julius|
            $(function() {
                $("h1").click(function(){
                    alert("You clicked on the heading!");
                });
            });
        |]
    toWidgetHead
        [hamlet|
            <meta name=keywords content="some sample keywords">
        |]
    toWidget
        [hamlet|
            <h1>Here's one way of including content
        |]
    [whamlet|<h2>Here's another |]
    toWidgetBody
        [julius|
            alert("This is included in the body itself");
        |]

main = warp 3000 App

这会生成如下的HTML代码(缩进是我增加的):

<!DOCTYPE html>
<html>
  <head>
    <title>My Page Title</title>
    <meta name="keywords" content="some sample keywords">
    <style>h1{color:green}</style>
  </head>
  <body>
    <h1>Here's one way of including content</h1>
    <h2>Here's another</h2>
    <script>
      alert("This is included in the body itself");
    </script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js">
    </script><script>
      $(function() {
        $('h1').click(function() {
          alert("You clicked on the heading!");
        });
      });
    </script>
  </body>
</html>

控件由哪些元素构成?

从非常浅的层面来说,一个HTML文档不过是一丢嵌套的标签。大部分HTML生成工具都是这 样做的:你定义好标签的层级就可以了。但假设我需要给一个页面写一个组件用来显示导 航栏。我希望它可以“即插即用”:我在适当的时候调用这个函数,导航栏就会被插入到正 确的位置(标签层级)。

这是我们简陋的HTML生成工具失效的地方。我们的导航栏除了HTML,可能还包含了一些 CSS和JavaScript。当我们调用导航栏函数时,<head>标签已经生成过了,因此要将包 含(导航栏)CSS的<style>标签加进去已经太迟了。通常的办法是,我们需要将导航栏函 数分为三部分:HTML、CSS和JavaScript,并确保我们总是同时调用这三部分。

控件采取的是另一种做法。不把HTML文档视为一个单一的标签树,而是视为许多不同的页 面组件。特别是:

  • 标题

  • 外部CSS

  • 外部Javascript

  • CSS声明

  • Javascript代码

  • 任意的<head>内容

  • 任意的<body>内容

不同的组件有不同的语义。比如,只能有一个标题,但可以有多个外部脚本和样式表。然 而,每个外部脚本或样式表应该只被引用一次。另一方面,任意的head和body内容是没有 限制的(有些人可能只想放五段lorem ipsum而已)。

控件的任务是保持各个组件的相互独立,同时用适当的逻辑将它们组合在一起。这包括: 取第一个标题而忽略其它标题,过滤重复的外部脚本和样式表引用,拼接head和body的内 容,等等。

构造控件

为了使用控件,你当然需要能够构造他们。最常用的方法是通过ToWidget型类,以及它 的toWidget方法。它能把你的莎氏模板直接转换成Widget:Hamlet代码会出现在正文 ,Julius脚本会在头部的<script>标签里,Cassius和Lucius会在(头部的)<style>标 签里。

注意
你实际上可以覆盖默认行为,让脚本和样式代码出现在各自的外部文件里。脚手架 项目自动为你完成这一点。

但如果你想增加一些<meta>标签呢?它们也需要放在头部。或者如果你想让Javascript 出现在正文而不是头部呢?为此,Yesod提供了另外两个型类:ToWidgetHeadToWidgetBody。这两个类(的作用)就像它们名字说的一样。

另外,还有一些其它函数用来创建特殊的控件:

setTitle

将一些HTML代码转换成页面标题。

toWidgetMedia

与toWidget一样,但需要一个额外的参数来表示样 式所应用的媒介。这对于创建比如说打印样式会有用。

addStylesheet

通过<link>标签,增加一个外部样式表的引用。输入参数是类型安全 的URL。

addStylesheetRemote

addStylesheet一样,但输入参数是普通URL。对于引用托管 在CDN上的文件有用,比如Google CDN上的jQuery UI CSS文件。

addScript

通过<script>标签,增加一个外部脚本的引用。输入参数是类型安全的 URL。

addScriptRemote

addScript一样,但输入参数是普通URL。对于引用托管在CDN上 的文件有用,比如Google CDN上的jQuery文件。

组合控件 (Combining Widgets)

控件的目的是增强可组合性。你可以将单独的HTML、CSS和Javascript组合成更复杂的结 构,然后再进一步组合成完整的页面。这些都能通过WidgetMonad实例很自然地实 现,也就是说你可以用do语句来组合控件。

myWidget1 = do
    toWidget [hamlet|<h1>My Title|]
    toWidget [lucius|h1 { color: green } |]

myWidget2 = do
    setTitle "My Page Title"
    addScriptRemote "http://www.example.com/script.js"

myWidget = do
    myWidget1
    myWidget2

-- or, if you want
myWidget' = myWidget1 >> myWidget2
注意
如果你需要的话,Widget也是Monoid的实例。也就是说你可以使用mconcatWriter monad来组合控件。以我的经验来说,用do语句最简单也最自然。

生成ID

如果我们要进行真正的代码复用,我们总是会遇到命名冲突。假设我们有两个辅助库都用 了‘`foo’'这个类名来控制样式。我们想要避免这种情况。因此,我们有newIdent函数 。它会为当前的处理函数自动生成一个唯一的名字。

getRootR = defaultLayout $ do
    headerClass <- newIdent
    toWidget [hamlet|<h1 .#{headerClass}>My Header|]
    toWidget [lucius| .#{headerClass} { color: green; } |]

whamlet

假设我们有一个标准的Hamlet模板,它嵌套了另一个Hamlet模板来表示页脚:

page =
    [hamlet|
        <p>This is my page. I hope you enjoyed it.
        ^{footer}
    |]

footer =
    [hamlet|
        <footer>
            <p>That's all folks!
    |]

如果页脚是普通的HTML,它能正常工作,但如果我们想要增加一些样式呢?好吧,我们可 以很容易的将页脚转换成一个控件:

footer = do
    toWidget
        [lucius|
            footer {
                font-weight: bold;
                text-align: center
            }
        |]
    toWidget
        [hamlet|
            <footer>
                <p>That's all folks!
        |]

但我们有个问题:一个Hamlet模板只能嵌套另一个Hamlet模板;它不知道什么是控件。这 就是whamlet的用处了。它的语法与普通的Hamlet完全一致,并且变量插值(#{…})和 URL插值(@{…})也是一样的。但嵌套插值(^{…})的输入参数是一个控件,输出结果 也是一个控件。要使用它,只需要:

page =
    [whamlet|
        <p>This is my page. I hope you enjoyed it.
        ^{footer}
    |]

如果你更喜欢把模板放在外部文件里的话,还可以用whamletFile函数。

注意
脚手架项目有一个更方便的函数,widgetFile,它会自动引用你的Lucius、 Cassius和Julius文件。我们会在“脚手架”一章中详述。

类型

你可能注意到了我一直在回避控件的类型标识。简单的答案是每个控件的类型都是 Widget。但如果你去Yesod类库里找,却找不到Widget的定义。怎么回事?

Yesod定义了一个非常相似的类型:data WidgetT site m a。这个数据类型是一个 *monad transformer*。最后两个参数是底层monad类型和monad值。site是你的应用的基 础数据类型。因为基础数据类型随每个站点而不同,不可能在类库里定义一个适用于所有 应用的Widget数据类型。

取而代之,mkYesod这个Haskell模板函数会为你生成类型别名。假设你的基础数据类型 是MyApp,那你的Widget的定义是这样的:

type Widget = WidgetT MyApp IO ()

我们将monad的值设为(),因为一个控件的值最终是被丢弃的。IO是标准的基础monad ,几乎在所有情况下都会用到。唯一的例外是写子站(subsite)的时候。子站是一个更高 级的话题,会在它自己的章节中讲解。

一旦我们知道了Widget的类型,就很容易给前面的例子加上类型标识:

footer :: Widget
footer = do
    toWidget
        [lucius|
            footer {
                font-weight: bold;
                text-align: center
            }
        |]
    toWidget
        [hamlet|
            <footer>
                <p>That's all folks!
        |]

page :: Widget
page =
    [whamlet|
        <p>This is my page. I hope you enjoyed it.
        ^{footer}
    |]

等我们开始讲解处理函数时,我们会在HandlerTHandler类型身上看到相似的情况 。

使用控件

我们有这么漂亮的控件数据类型已经很好了,但到底怎么把它们转换成用户可以与之交互 的东西?最常用的做法是defaultLayout函数,它的类型标识是Widget → Handler Html

defaultLayout实际上是个型类的方法,它可以在每个应用中重新定义。这也是Yesod应 用定义主题的方法。所以我们剩下的问题是:在defaultLayout函数内,怎么拆开一个 Widget?答案是用widgetToPageContent函数。让我们看一下(简化了的)类型:

widgetToPageContent :: Widget -> Handler (PageContent url)
data PageContent url = PageContent
    { pageTitle :: Html
    , pageHead :: HtmlUrl url
    , pageBody :: HtmlUrl url
    }

距离我们的目标已经很近了。我们现在可以直接访问HTML的头部和正文,以及标题。至此 ,我们可以用Hamlet把它们与页面布局组合成一个文件,然后用giveUrlRenderer函数 将Hamlet的结果转换为实际呈现给用户的HTML。下面的代码说明了这个过程。

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes       #-}
{-# LANGUAGE TemplateHaskell   #-}
{-# LANGUAGE TypeFamilies      #-}
import           Yesod

data App = App
mkYesod "App" [parseRoutes|
/ HomeR GET
|]

myLayout :: Widget -> Handler Html
myLayout widget = do
    pc <- widgetToPageContent widget
    giveUrlRenderer
        [hamlet|
            $doctype 5
            <html>
                <head>
                    <title>#{pageTitle pc}
                    <meta charset=utf-8>
                    <style>body { font-family: verdana }
                    ^{pageHead pc}
                <body>
                    <article>
                        ^{pageBody pc}
        |]

instance Yesod App where
    defaultLayout = myLayout

getHomeR :: Handler Html
getHomeR = defaultLayout
    [whamlet|
        <p>Hello World!
    |]

main :: IO ()
main = warp 3000 App

这都很好,但还有一件事困扰我:就是style标签。它有一些问题:

  • 不像Lucius和Cassius,它不能在编译时做正确性检查。

  • 虽然这个例子很简单,但在复杂的情况下,我们会遇到字符转义的问题。

  • 我们会有两个style标签而不是一个:一个是myLayout生成的,另一个是pageHead 基于控件内设置的样式生成的。

我们还有一个锦囊可以用:我们在调用widgetToPageContent前对控件做一些最后的调 整。其实非常简单:我们只是再次用了do语句。

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes       #-}
{-# LANGUAGE TemplateHaskell   #-}
{-# LANGUAGE TypeFamilies      #-}
import           Yesod

data App = App
mkYesod "App" [parseRoutes|
/ HomeR GET
|]

myLayout :: Widget -> Handler Html
myLayout widget = do
    pc <- widgetToPageContent $ do
        widget
        toWidget [lucius| body { font-family: verdana } |]
    giveUrlRenderer
        [hamlet|
            $doctype 5
            <html>
                <head>
                    <title>#{pageTitle pc}
                    <meta charset=utf-8>
                    ^{pageHead pc}
                <body>
                    <article>
                        ^{pageBody pc}
        |]

instance Yesod App where
    defaultLayout = myLayout

getHomeR :: Handler Html
getHomeR = defaultLayout
    [whamlet|
        <p>Hello World!
    |]

main :: IO ()
main = warp 3000 App

使用处理函数

我们至今还没怎么讲处理函数,但一旦开始讲,问题就来了:我们怎么在控件中使用这 些函数?比如,如果一个控件需要使用lookupGetParam来查询请求参数?

第一种答案是用handlerToWidget函数,它将一个Handler动作转换为一个Widget。 然而,在很多情况下并不需要这么做。来看看lookupGetParam函数的类型标识:

lookupGetParam :: MonadHandler m => Text -> m (Maybe Text)

这个函数可以在任何MonadHandler的实例中使用。而且方便的是,Widget就是 MonadHandler的实例。这意味着大部分代码既可以在Handler中运行,也可以在 Widget中运行。而且如果你需要显式的将Handler转换为Widget,你还是可以用 handlerToWidget函数。

注意
这与Yesod 1.1及更早的版本有显著的区别。之前是没有MonadHandler这个型类 的,所有函数都需要显式的使用lift转换,而不是handlerToWidget。新版本不仅更 容易使用,而且也避免了旧版中使用的奇怪的monad transformer技巧。

小结

构筑每个页面的砖块是控件。独立的HTML、CSS和Javascript代码段可以通过多态的 toWidget函数转换成控件。使用do语句,可以将这些独立的控件组合成更大的控件,最 后构成页面的全部内容。

通常在defaultLayout函数中拆开这些控件,defaulLayout能将统一的外观风格应用到所 有页面。