Yesod - 组件 (4)

原创
2018/08/22 20:32
阅读数 113

组件

web 开发的困难之一,我们必须配合三种客户端技术:HTML,CSS,Javascript。更糟糕的是,我们必须把它放在网页的不同位置:CSS在head中一个style标签,Javascript在body标签前script标签中,HTML在body中。如果你想要把你的CSS和Javascript放在在单独的文件中,你不用担心这些!

在实践中,创建一个单页面,这种方式能够工作得很好,因为我们可以分离我们的结构(HTML),样式(CSS),逻辑(Javascript)。但是当我们想将其模块化以至于能够更容易的组合,协调这三块内容就会变得非常头疼。组件就是Yesod对这种问题的解决方案。这样对于防止重复包含库问题,也能很好的解决。

我们四种模板语言-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标签层级关系, 工具来生成 HTML 文档. 但是,仔细想想, 如果我要为一个页面写一个组件来显示导航栏, 而且能够像插头一样,插上即用: 在合适的时间来调用方法, 一个导航栏就会被插入到正确的地方.

这里就是传统 HTML 的短板了, 我们的导航栏由 html, JavaScript, CSS 组成. 等到我们调用导航栏方法的时候, head标签已经被渲染了, 所有,此时添加新的style标签来生命 CSS 已经太晚了.按照常规策略, 我们需要把我们的导航栏拆分成 JavaScript, CSS, HTML 三部分, 并且确保我们总能访问到这三部分内容.

组件使用了一种另外的途径, 小部件不是将HTML文档视为一个整体标签树,而是在页面中看到许多不同的组件 尤其是:

  • HTML 标题
  • 外部 CSS
  • 外部 JavaScript
  • CSS 声明
  • JavaScript 代码
  • 任意的 head 内容
  • 任意的 body 内容

不同的组件有不同的语义. 例如, 只可能有一个标题, 可以有多个外部 JavaScript 和 外部样式表.但是这些外部脚本和样式表都只能被引入一次. head 和 body里面可以有任意内容, 换句话说,就是没有任何限制(可能有人只想放几个毫无意义的区块而已). 组件的作用就是使用合适的逻辑组合不同的组件。比如取最后一个title替换掉其他的,过滤掉重复的外部JavaScript和CSS,连接head和body的内容。

构造组件

为了使用组件,你很显然需要能够掌握他们。最常用的方法是使用ToWidget类型类和toWidget函数。这些能让你把你的Shakespearean templates直接转化为组件。Hamlet代码出现在<body>里面,Julius脚本出现<script>,Cassius 和 Lucius在<style>标签里。

事实上你可以重载一些默认的行为,并且在一些独立的文件里使用script和style。默认的网站提供了这些支持。

但是如果你想添加一些<meta>标签呢,在head里需要写什么?或者如果你想把javascript写在body里而不是head里呢。为了解决这些,Yesod提供了另外的两个类型类ToWidgetHeadToWidgetBody

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

data App = App

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

instance Yesod App where

getHomeR :: Handler Html
getHomeR = defaultLayout $ do
    setTitle "toWidgetHead and toWidgetBody"
    toWidgetBody
        [hamlet|<script src=/included-in-body.js>|]
    toWidgetHead
        [hamlet|<script src=/included-in-head.js>|]

main :: IO ()
main = warp 3001 App

即使toWidgetHeadtoWidgetBody的后面调用,但是在生成的HTML里的<script>标签还是在前面。
还有许多其他功能用于创建特定类型的小部件:

setTitle

设置标题

toWidgetMedia

toWidget一样,但是需要一些其他的参数来指定用于那种类型。例如创建打印样式表

addStylesheet

使用类型安全的URL,通过link标签添加一个外部CSS。

addStylesheetRemote

addStylesheet一样,但是使用一个URL引用外部CDN上的文件。

addScript

使用类型安全的URL,通过<script>标签添加一个外部JavaScript。

addScriptRemote

addScript一样,但是使用一个URL引用外部CDN上的文件。

组合组件

组件的主体思想是增加可组合性。你可以使用这些由HTML,CSS和JavaScript组成的单独的组件组合到一起。完成更复杂的任务,这些组合起来的大的实体再组成这个页面。这一切通过Monad的实例Widget自然的实现。这意味着你可以使用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的实例,意味着你能使用mconcat或者Write Monad。但是以我的经验来看使用do语法糖是最容易和自然的。

生成IDs

如果我们真的想重用代码,我们最终都会碰到命名冲突的问题。假设有两个Helper库都使用类名“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!
    |]

但是现在我们有一个问题Hamlet模板只能嵌入另一个Hamlet模板,它并不能解析Widget。这就是whamlet出现的原因了。他采用和Hamlet完全相同的语法。引用变量#{...}引用URL@{...},但是使用^{...}来引用组件。我们可以这样使用它

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

这当然也支持whamletFile,如果你习惯把模板放在文件里。

默认的模板项目有更多的辅助函数,widgetFile,我们将在模板项目章节里介绍这些

Types

你可能已经注意到,我们一直在避免提及类型签名。简单的说明是每个widget组件都是Widget类型的值。但是如果你阅读Yesod的库你会发现根本没有Widget的定义。
Yesod定义了一个非常简单的类型data WidgetT site m a,这是一个Monad transformer,最后两个参数分别是其可以变换的Monad和当前的类型。site参数是你的APP里具体的foundation类型,由于这种类型因每个站点而异,因此库不可能定义一个适用于每个应用程序的Widget数据类型。
所以mkYesodTemplate Haskell 函数会生成一个类型给你。假如你的foundation类型是MyApp那么给你生成的Widget类型就是

type Widget = WidgetT MyApp IO ()

我们把monadic value设置为(),因为Widget的值都会被忽略,IO是标准的基础Monad,并且有可能在任何情况下使用。唯一的类外是在编写子网站时,这是一个更高级的问题,我们将在后面的章节介绍它。
一旦我们了解了我们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}
    |]

当我们深入了解handler函数时,我们将遇到类似的HandlerTHandler类型。

使用Widgets

现在我们拥有了这些良好的Widget类型了,但是我们究竟该怎么使用他们呢。最常用的是使用defaultLayout函数,他的类型签名是 Widget → Handler Html
defaultLayout其实是一个typeclass的方法。我们可以为每个应用重写它。Yesod使用它来定制每个App的主题。那么现在问题是我们是怎么把Widget打包成defaultLayout的。答案是widgetToPageContent,我们来看一下(简化)类型:

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

这已经接近我们想要的了,我们可以直接访问构成HTML的head bodytitle,现在我们可以使用hamlet把他们组合成一个单独的文档,以及我们的网站layout。我们再用withUrlRenderer将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
    withUrlRenderer
        [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生成的那个,另一个是pageHeadWidget中生成的。

我们其实只要做一些操作就可以解决这个问题。

{-# 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 } |]
    withUrlRenderer
        [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

使用Handler函数

我们还有没深入介绍Handler函数。现在的问题是我们怎么在Widget中使用这些函数,例如如果你的小部件需要一些查询字符串怎么办,使用 lookupGetParam
第一个答案是使用handlerToWidget他可以将Handler转化为Widget。但一些情况下这不是必要的。看一下lookupGetParam的类型签名:

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

这个函数可以再任何MonadHandler的实例中使用。Widget刚好是一个MonadHandler实例。这意味着大多数Handler的函数都可以在Widget中使用。并且你真的需要把Handler转化为Widget你照样可以使用handlerToWidget。 ##总结 每个页面都是由Widget组成的。HTML,CSS,JavaScript片段可以通过toWidget函数转化成Widget。通过do语法糖你可以把这些Widget组合成更复杂的Widget。最终所有内容组成了你的页面。
打包这些Widget一般在defaultLayout中,可以定制页面的外观。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部