一个Yesod简要教程。Yesod是Haskell的Web框架。

为什么选择Haskell?

haskell-benchmark

光看这图就知道,Haskell比同是解释语言如:Ruby、Python2、PHP快一个数量级。可是他又能有编译型语言的强类型特点;Haskell Web框架能够完美的处理并发任务(更确切的说是隐式并发),甚至比node.js更快。

从技术的角度来说,Haskell是一个优秀的Web工具,当然也会有一些缺点:

  • Haskell 语言学习曲线非常高。
  • 学习氛围和资源太少了。
  • 没有强力IDE。

Haskell通常会使用以下三种Web框架:

这三者并没有真正的赢家,而这篇文章是基于Yesod,因为Yesod团队相对于比较活跃,而且文档也比较清晰。

1、开始

1.1 安装

推荐安装方法最好直接安装Haskell Platform,这样就可以免了一些配置,特别是在Windows下,会显的比较麻烦。当然如果你愿意也可以自己下载源代码包,可以适当性的优化编译所需要的模块。

而Yesod推荐使用Stack构建工具来开发,而且真的非常简单,只需要:

  • 获取 Stack构建工具(就一个exe文件),最好设置一下 PATH
  • 通过stack安装yesod命令行工具:stack install yesod-bin cabal-install –install-ghc
  • 创建站点:stack exec — yesod init –bare && stack init。(会提示你输入项目名和默认需要安装的组件)
  • 编译:stack build
  • 启动服务器:stack exec — yesod devel
  • 默认会启用http和https两个服务,分别是:http://127.0.0.1:3000和https://127.0.0.1:3443。

怎么样,虽然没有像VS那种IDE,但构建一个站点出来还是很简单的。

1.2 项目结构

构建后我们有个接近8MB的文件,当我们启动服务器后,会自动监听,一但我们对文件的任何修Yesod都会重新编译,现在来看看默认的文件结构都是些什么意思:

  • config/routes:很明显路由配置,他爷的,比ASP.NET MVC的路由配置优美太多了。
  • Handler:就是MVC中的Controller。
  • templates/:存放HTML文件,就是MVC中的Views。
  • config/models:持久化对象模型,就是MVC中的Models。
  • static/:存放静态资源文件,诸如:JS、CSS等等。
  • test/:测试目录。

在本教程中,将会修改其他文件,但代码细节并不会探讨太多。

另外注意,除特殊说明,否则都将在项目的根目录下执行命令。

2、输出

为了验证Yesod框架的安全性,我们来实现这么个程序。当访问 /echo/你好,返回一个带有【你好】的 h1 元素。

首先通过 yesod 来构建一个handler:

C:\demo>yesod add-handler
Name of route (without trailing R): Echo
Enter route pattern (ex: /entry/#EntryId): /echo/#String
Enter space-separated list of methods (ex: GET POST): GET

add-handler 命令会自动帮我们做以下这些事:

A、路由配置

/echo/#String EchoR GET

这一行里面有包括三个要素:URL匹配模式、Handler名称,HTTP请求方法,这种配置文件简明扼要。

B、新增Handler文件:Handler/Echo.hs

module Handler.Echo where

import Import

getEchoR :: String -> Handler Html
getEchoR string = error "Not yet implemented: getEchoR"

他们意思从上至下为:

  • module Handler.Echo where:构造名为Handler.Echo的模块。
  • import Import,载入Import模块的所有函数。
  • getEchoR 函数,允许接受一个String参数。

现在我们可以尝试在浏览器访问: http://127.0.0.1:3000/echo/你好,不过可惜你只会返回一个Internal Server Error的错误页面。所以我们需要修改一下Echo.hs文件。

module Handler.Echo where

import Import

getEchoR :: String -> Handler Html
getEchoR theText = defaultLayout [whamlet|<h1>#{theText}|]

先不管细节的做法,以上短短的一行是指当调用 getEchoR 时,会构建一个 Handler Html

保存文件后,只要你的yesod服务没有关闭,就会自动重新编译,我们可以接着访问一下看看。

2.1 XSS

即使是一种再简单不过的Web应用,防止像XSS这种最基础的特性也是要有的,比如:

http://localhost:3000/echo/I'm <script>alert(\"Bad!\");</script>

你可以点击看看,他可是会自动会把 空格 转换成 %20;把 < 转换成 &lt;,从而防止小人的恶意代码。

Yesod 不只是很快,而且还有很多像这种特性来保护应用安全,虽然这些每一种Web框架都会存在,但是是否拥有这些也让我们无须关心这种会让程序出现异常的错误。

相比较 String 来做为URL请求参数,我们可以使用 Data.Text 会是更好的选择。

首先在 Foundation.hs 载入 Data.Text 所有函数:

import Data.Text

替换 config/routes 中的 #StringText

/echo/#Text EchoR GET

最后修改 Handler/Echo.hs 文件为:

module Handler.Echo where

import Import

getEchoR :: Text -> Handler Html
getEchoR theText = defaultLayout [whamlet|<h1>#{theText}|]

2.2 使用模板文件

从解耦的角度来说,我们不可能直接把HTML写到hs文件当中,因为对于一个纯函数式语言来说,相互过度依赖会让它变成命令式语言的。创建一个 templates/echo.hamlet 内容为:

<h1> #{theText}

*.hamlet 有点类似 Jade 同样都是用来构建安全的HTML代码,并且支持一些条件表达式等等。最后修改处理文件 Handler/Echo.hs

module Handler.Echo where

import Import

getEchoR :: Text -> Handler Html
getEchoR theText = defaultLayout $(widgetFile "echo")

接下来我们来看一下复杂一点的例子。

3. Form

构建另一个小应用,可以在页面上看到一个文本框和按钮,点击按钮后会跳转到另一个页面,并把你录入的文本进行反转显示。

我们依然还是使用 yesod add-handler 来构建。

C:\demo>yesod add-handler
Name of route (without trailing R): Mirror
Enter route pattern (ex: /entry/#EntryId): /mirror
Enter space-separated list of methods (ex: GET POST): GET POST

这一次的路径 /mirror 是同时允许 GET 和 POST 两种不同行为的HTTP请求。接着,修改 handler 文件:

module Handler.Mirror where

import Import
import qualified Data.Text as T

getMirrorR :: Handler Html
getMirrorR = defaultLayout $(widgetFile "mirror")

postMirrorR :: Handler Html
postMirrorR = do
    postedText <- runInputPost $ ireq textField "content"
    defaultLayout $(widgetFile, "posted")

我们需要使用 reverse 函数在反转我们的文本,所以需要额外导入 Date.Text,这里和之前的导入方法不一样,加了一个 qualified 目的是为了所导入的函数名可能会与我们写的函数名会有所冲突,所以除非很清楚的情况下,否则使用 qualified 导入更为安全一点,虽然我们需要多一个 T.函数名 有点不够简洁。

接下来,分别创建 templates/mirror.hamlettemplates/posted.hamlet 两个模板文件:

<h1> Enter your text
<form method=post action=@{MirrorR}>
    <input type=text name=content>
    <input type=submit>
<h1>You've just posted
<p>#{postedText}#{T.reverse postedText}
<hr>
<p><a href=@{MirrorR}>Get back

4、Blog

前面我们已经有一个完整的表单例子,那么接下来我们需要将这些内容保存到数据库中。

以一个迷你Blog为例,先来看看URL部分包括:

  • /blogGET 显示文章列表。
  • /blogPOST 添加一篇新文章。
  • /blog/<article id>GET 显示文章内容。

确认以上是我们要做的事后,我们使用 yesod add-hander 来构建我们初始化文件。

C:\demo>yesod add-handler
Name of route (without trailing R): Blog
Enter route pattern (ex: /entry/#EntryId): /blog
Enter space-separated list of methods (ex: GET POST): GET POST

C:\demo>yesod add-handler
Name of route (without trailing R): Article
Enter route pattern (ex: /entry/#EntryId): /blog/#ArticleId
Enter space-separated list of methods (ex: GET POST): GET

接下来还需要声明一个模型对象,将以下内容添加到 config/models 当中:

Article
    title Text
    content Html

第1行为模型对象名称,title为Text类型,content为Html类型(他源自于html库,其实对于Haskell来说,不光默认的Bool、Integer、String等等类型外,我们还可以自定义自己的类型)。

有了路由和对象模型外,我们可以编写 handler 文件:

module Handler.Blog
    ( getBlogR, postBlogR) where

import Import

import Yesod.Form.Nic (YesodNic, nicHtmlField)

instance YesodNic App

Yesod.Form.Nic 是一个富文本编辑器,最好是把这个实例放在 Foundation.hs 当中,这样不需要每次页面请求时都实例,放在这里主要是为了让大家看得更清楚。

接下来把 YesodNicnicHtmlField 导入到对象模型当中。

entryForm :: Form Article
entryForm = renderDivs $ Article
    <$> areq   textField "Title" Nothing
    <*> areq   nicHtmlField "Content" Nothing

以上定义了一个创建新文章的函数,先抛开语法不说,暂时只要记住 areq 表示必填项,它的参数形式为:areq type label default_value

-- The view showing the list of articles
getBlogR :: Handler Html
getBlogR = do
    -- Get the list of articles inside the database.
    articles <- runDB $ selectList [] [Desc ArticleTitle]
    -- We'll need the two "objects": articleWidget and enctype
    -- to construct the form (see templates/articles.hamlet).
    (articleWidget, enctype) <- generateFormPost entryForm
    defaultLayout $ do
        $(widgetFile "articles")

getBlogR 函数分别是从Sqlite读取所有文章列表。同时,构建两个 articleWidgetenctype 两个对象用于模板调用。接下来是相应的模板:

<h1> Articles
$if null articles
    <p> There are no articles in the blog
$else
    <ul>
        $forall Entity articleId article <- articles
            <li>
                <a href=@{ArticleR articleId} > #{articleTitle article}
<hr>
  <form method=post enctype=#{enctype}>
    ^{articleWidget}
    <div>
        <input type=submit value="Post New Article">

模板并没有很难懂,这里主要是利用 $forall 来循环列表。

这里的 articleWidget 实际就是通过 Yesod.Form.Functions.generateFormPost 函数来创建一个带Form表单的HTML对象,参数就是我们前面定义的 entryForm 对象模型。

接下来添加 POST 添加新文章函数:

postBlogR :: Handler Html
postBlogR = do
    ((res,articleWidget),enctype) <- runFormPost entryForm
    case res of
         FormSuccess article -> do
            articleId <- runDB $ insert article
            setMessage $ toHtml $ (articleTitle article) <> " created"
            redirect $ ArticleR articleId
         _ -> defaultLayout $ do
                setTitle "Please correct your entry form"
                $(widgetFile "articleAddError")

postBlogR 就是响应表单,如果有任何错误会跳转到错误页面,但如果一切都顺利的话,那么流程应该是:

  • 添加一个新的article到DB(runDB $ insert article)。
  • 添加一个添加成功消息文本(setMessage $)。
  • 跳转到文章明细页面。

错误页面模板:

<form method=post enctype=#{enctype}>
    ^{articleWidget}
    <div>
        <input type=submit value="Post New Article">

最后就剩下文章详情页面,首先Handler代码:

getArticleR :: ArticleId -> Handler Html
getArticleR articleId = do
    article <- runDB $ get404 articleId
    defaultLayout $ do
        setTitle $ toHtml $ articleTitle article
        $(widgetFile "article")

get404 函数是尝试从DB获取文章,如果失败则直接跳到404页面。相应的模板内容为:

<h1> #{articleTitle article}
<article> #{articleContent article}
<hr>
<a href=@{BlogR}>
    Go to article list.

整个Blog系统算是完成了,所有的代码可以Github查阅得到。

5、总结

虽然最后的Blog相对于完整的还很远,但是其实可以发现,虽然只有短短的几行代码,但是它已经包括了相对完整的而又安全的一个迷你Blog。如果你对Haskell Web编程更感兴趣,而且你英文比较好可以查阅Blog: i18n, authentication, authorization, and database来完善更复杂的BLOG。

另一方面,如果你对Haskell也感兴趣的话,以上的示例一定会让你很兴奋。Haskell是一种非常复杂,语法非常奇怪的语言(也许习惯时命令式语言上工作的人来说,刚开始会是一种噩梦,然而如果你真正去使用后你会发现这个语言真的不可思议)。为了能够尽快使用Haskell Web编程,我给出几条建议:

  • 马上使用Try Haskell学习Haskell。
  • 本篇文章底部的资料是非常好的学习资料。

资料