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

基础

学习任何新技术的第一步是让它运行起来。本章的目标是用一个简单的Yesod程序带你开始 ,并涉及一些基础的概念和术语。

Hello World

让我们来看一个例子:一个简单的网页显示Hello World:

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

data HelloWorld = HelloWorld

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

instance Yesod HelloWorld

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

main :: IO ()
main = warp 3000 HelloWorld

如果你将以上代码保存为helloworld.hs,然后用runhaskell helloworld.hs运行它 ,会有一个web服务器运行在端口3000。如果你用浏览器打开http://localhost:3000, 会得到如下的HTML:

<!DOCTYPE html>
<html><head><title></title></head><body>Hello World!</body></html>

本章剩余部分还会提及这个例子。

路由

与时下大多数web框架一样,Yesod遵循 前端控制器模式(front controller pattern)。这意味着Yesod应用的每一个请求都会先进入同一个点,然后再从 这个点路由出去。与此相比,在PHP和ASP这些系统中,你通常会创建很多文件,然后web服 务器自动将请求路由到对应的文件。

另外,Yesod使用声明风格来定义路由。在上面的例子中,是这样定义路由的:

mkYesod "HelloWorld" [parseRoutes|
/ HomeR GET
|]
注意
mkYesod是一个Haskell模板函数(Template Haskell function),而 parseRoutes是一个准引用(QuasiQuoter)。

用大白话说就是:在HelloWorld应用中,创建一条路由。我将其命名为HomeR,它侦听对 /的请求(应用的根路径),并且回应GET请求。我们称HomeR为一个资源(resource), 这也是它以"R"结尾的原因。

注意
以R为后缀只是个习惯,但是是一个被普遍遵循的习惯。这样做有助于阅读和理解代 码。

mkYesod这个TH函数会生成很多代码:一个路由数据类型,解析/呈现(parse/render)函 数,分发函数(dispatch function)和一些辅助类型。我们会在路由那一章更详细的讲这个 问题。通过使用-ddump-splices这个GHC选项,我们可以立即看到生成的代码。以下是一 个简化的版本:

instance RenderRoute HelloWorld where
    data Route HelloWorld = HomeR
        deriving (Show, Eq, Read)
    renderRoute HomeR = ([], [])

instance ParseRoute HelloWorld where
    parseRoute ([], _) = Just HomeR
    parseRoute _       = Nothing

instance YesodDispatch HelloWorld where
    yesodDispatch env req =
        yesodRunner handler env mroute req
      where
        mroute = parseRoute (pathInfo req, textQueryString req)
        handler =
            case mroute of
                Nothing -> notFound
                Just HomeR ->
                    case requestMethod req of
                        "GET" -> getHomeR
                        _     -> badMethod

type Handler = HandlerT HelloWorld IO

我们可以看到RenderRoute类定义了一个关联数据类型(associated data type), 它是应用的路由。在这个小例子中,我们只有一条路由:HomeR。在现实应用 中会有多得多的路由,而且它们会比HomeR复杂得多。

renderRoute的输入是路由,输出是路径段(path segments)和查询参数(query string)。再次说明,我们的例子很简单,所以代码也同样简单:这两个值都是空列表。

ParseRoute类提供了renderRoute的反函数parseRoute。这是我们首次看到Yesod依 赖于Haskell模板的原因:它保证了对路由的解析和呈现能够一致。这一类代码如果手写的 话,会很容易变得难以同步。借助于代码生成(code generation)技术,我们将这些细节交 给编译器(和Yesod)处理。

YesodDispatch类提供的yesodDispatch方法,将输入请求分发给正确的处理函数 (handler function)。这个过程本质上是这样的:

  1. 解析请求。

  2. 选择处理函数。

  3. 运行处理函数。

代码生成按照一种简单的格式来匹配路由和处理函数,我们会在下一节中讲解。

最后,我们定义了类型别名Handler,它能让我们的代码写起来更容易(更短)。

实际上有很多我们没有讲到的地方。生成的分发代码实际上使用的是一种更高效的数据结 构,它会创建更多的型类(typeclass)及其实例(instance),而且需要处理其它的情况比如 子站(subsites)。我们会渐渐深入细节,尤其是在‘`理解HTTP请求’'那一章。

处理函数(Handler function)

上例中,我们定义了名为HomeR的路由,它可以响应GET请求。那响应是怎么定义的呢 ?写一个处理函数就可以。Yesod要求处理函数遵循标准的命名规则:小写的请求方法(比 如GET在函数名中是get),然后紧接路由名。在本例中,处理函数的名字就是 getHomeR

你写的大部分Yesod代码都会是处理函数。在处理函数中,你处理用户输入、执行数据库查 询、创建应答。在上例中,我们用defaultLayout函数来创建应答。这个函数将内容嵌入 到网站模板中。默认情况下,它会生成一个HTML文件,文件有doctype和htmlheadbody等标签。在Yesod型类一章中,我们将会看到可以重定义(override)这个函数以实现 更多功能。

在上例中,我们把[whamlet|Hello World!|]传给defaultLayoutwhamlet是另一 个准引用。在这里,它把Hamlet语句转化成一个控件(Widget)。Hamlet是Yesod默认的HTML 模板引擎。将Hamlet与Cassius、Lucius和Julius配合使用,你可以以完全类型安全且编译 时检验的方式创建HTML、CSS和Javascript。我们会在莎氏模板一章中详述。

控件是Yesod的另一个基石。借助控件,你可以给一个网站创建由HTML、CSS和Javascript 组成的模块化组件,然后在整个网站中复用它们。我们会在控件一章中讲解。

Yesod基石

在上例中`HelloWorld'这个词出现了数次。每一个Yesod应用中都有一个基础数据类型 (foundatoin datatype)。这个数据类型必须是Yesod型类的实例,它是集中声明配置信 息的地方,这些配置控制了应用的执行。

上例中的基础数据类型比较枯燥:它不包含任何信息。尽管如此,它对于该程序如何运行 有关键作用:它将路由与实例声明绑定,并运行它们。我们会在本书中反复看到基础数据 类型的出现。

但基础数据类型不总是枯燥的:它们可以用来存储大量有用的信息,通常是在程序启动时 被初始化、然后在程序运行过程中需要反复用到的内容。一些常见的样例有:

  • 数据库连接池

  • 从配置文件加载的配置

  • HTTP连接管理器

  • 随机数生成器

注意
顺便说一下,Yesod(יסוד)这个词在希伯来语中就是基础(foundation)的意思。

运行

在上例的main函数中,再一次出现了HelloWorld。在基础数据类型中包含了所有用来路 由和响应请求的信息;我们现在只需要把它们转化成可执行代码就行。对此,Yesod里一个 有用的函数是warp,它用若干默认配置、在指定端口(这里是3000)上运行一个Warp网络 服务器。

Yesod的特性之一是你不只有一种布署策略。Yesod构建于网络应用接口(WAI: Web Application Interface)之上,因此它可以运行在FastCGI、SCGI、Warp上,甚至可以通过 Webkit库以桌面应用的方式运行。其中一些方案我们会在布署一章中讨论。在本章末尾, 我们会讲解开发服务器。

Warp是Yesod的首选布署方案。它轻量、高效,并且是专为托管Yesod应用而开发的网络服 务器。它也被用在Yesod以外的Haskell开发(包括web框架和非web框架应用),也在很多生 产环境中被用作标准的文件服务器。

资源以及类型安全的URL

在hello world例子中,我们只定义了一个资源(HomeR)。一个web应用通常有超过一页纸 那么多的资源。让我们来看一个例子:

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

data Links = Links

mkYesod "Links" [parseRoutes|
/ HomeR GET
/page1 Page1R GET
/page2 Page2R GET
|]

instance Yesod Links

getHomeR  = defaultLayout [whamlet|<a href=@{Page1R}>Go to page 1!|]
getPage1R = defaultLayout [whamlet|<a href=@{Page2R}>Go to page 2!|]
getPage2R = defaultLayout [whamlet|<a href=@{HomeR}>Go home!|]

main = warp 3000 Links

总体上,这与Hello World那个例子非常近似。在这里,基础数据类型是Links而不是 HelloWorld,在HomeR之外,我们又增加了Page1RPage2R这两个资源。因此, 我们也需要增加两个处理函数:getPage1RgetPage2R

在这里算得上新特性的只有whamlet这个准引用。我们会在‘`莎氏模板’'一章中深入讲解 其语法。不过我们可以看到:

<a href=@{Page1R}>Go to page 1!

创建了一个指向Page1R的超链接。这里需要注意的是Page1R是一个数据构造函数(data constructor)。通过 使每个资源都是一个数据构造器,我们就实现了所谓的类型安全URL(type-safe URLs )这个特性。我们只需要创建一个普通的Haskell值,而不用通过拼接字符串来创建URL。 使用@符号(@{…})插值,Yesod会自动将这些值转换为文本形式的URL,然后 发送给用户。还是通过-ddump-splices,我们可以看到它是怎么实现的:

instance RenderRoute Links where
    data Route Links = HomeR | Page1R | Page2R
      deriving (Show, Eq, Read)

    renderRoute HomeR  = ([], [])
    renderRoute Page1R = (["page1"], [])
    renderRoute Page2R = (["page2"], [])

在这个例子中,RouteLinks类的关联类型中,除了HomeR,我们还新增了Page1RPage2R这两个数据构造函数。我们还能更好的看到renderRoute函数的返回值。元组 (tuple)的第一部分是该路由的路径段。第二部分是请求参数;在大多数情况下,它是个空 列表。

怎么高估类型安全URL都不为过。它们在你的应用开发中提供了伸缩性和稳健性保证。你可 以在代码中任意移动URL而不致破坏超链接。在路由一章中,我们会看到路由是怎么接受参 数传入的,比如blog条目的URL传入的参数可以是该条目的ID。

假设你想把路由从数字的ID转换成年/月/段名。在传统的web框架里,你需要更新每个指向 博客文章的链接。如果忘了更新其中一个,你就会在运行时得到404的返回结果。在Yesod 中,你需要做的只是更新路由和重编译:GHC会修正每处指向路由的代码。

脚手架站点(scaffolded site)

安装完Yesod后,你既有Yesod类库,也有一个名为yesod的可执行文件。这个可执行文件 可以接受参数传入,但你首先需要熟悉的命令是yesod init。它会问你一些问题,然后 生成一个包含默认脚手架站点的文件夹。在生成的文件夹中,执行cabal install --only-dependencies来建构额外的依赖(比如与数据库后端有关的包),然后执行yesod devel来运行你的站点。

脚手架站点开箱即用,带给你最佳实践,为你配置好文件和依赖。这些配置都是经过生产 环境Yesod站点长时间检验的。尽管如此,这些方便之处也可能防碍你真正学习Yesod。因 此,本书大部分时候都避免使用脚手架工具,而是直接使用Yesod库。但如果你要搭建一个 真实的网站,我强烈建议你使用脚手架工具。

我们会在脚手架一章中讲解脚手架站点的结构。

开发服务器

解释型语言相比于编译型语言的一个优势是原型快速迭代:你只要保存文件然后点击刷新 即可。我们对上面的Yesod例子做任何改动,都需要重新执行runhaskell,这会有点繁琐 。

幸运的是,我们有解决方法:yesod devel会自动重新构建和加载你的代码。在开发 Yesod项目时会极为有用。在你要布署到生产环境时,你还是应该完全编译,以生成高效 代码。Yesod脚手架会自动帮你做好配置。这样你会得到两种语言的结合优势:快速原型 化*和*高效的生产代码。

让你的代码能够用上yesod devel需要做一些工作,因此我们的例子仍将只使用warp。 幸运的是,脚手架项目已经为你配置好开发服务器,所以当你进行真实的Yesod项目开发时 ,它就在那等着你。

小结

每一个Yesod应用都围绕一个基础数据类型构建。我们将一些资源与这个数据类型相关联, 然后定义相应的处理函数,Yesod就会处理所有的路由。这些资源同时也是数据构造函数, 为我们提供了类型安全的URL。

通过构建于WAI之上,Yesod应用可以运行在大量的后端上。对于简单的应用,warp函数 提供了易于使用的Warp服务器。如果需要快速开发,使用yesod devel是不错的选择。当 你要布署到生产环境时,你可以完全且方便的配置Warp(或其它WAI处理器)以匹配你的需求 。

当开发Yesod应用时,我们有很多代码风格可以选:准引用或外部文件,warpyesod devel等等。本书的例子都偏向于容易复制粘贴,但在开发真实的Yesod应用时,你可以 选择更强大的方案。