基本语法
学习任何新技术的第一步就是把它运行起来。本章的目标就是让你做一个简单的Yesod应用,涉及到一些基本概念和技术。
Hello World
我们以一个简单的Hello World 网页开始:
{-# LANGUAGE OverloadedStrings #-}
{-# 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
,然后你会在3000端口上运行一个web服务。注意,如果你是根据快速指南,使用stack
来安装的Yesod,你可能没有安装runhaskell
,这样你需要使用stack runghc helloworld.hs
替代。如果你访问http://localhost:3000,你会得到以下HTML:
<!DOCTYPE html>
<html><head><title></title></head><body>Hello World!</body></html>
本章后面的内容会逐步分析这个例子。
路由
与大多数现代web框架一样,Yesod使用前端控制器模式。这就意味着,每个请求都会集中到一个地方,从这里进行路由分发。做一个对比,PHP和ASP之类的,你通常会创建大量不同的文件,然后web服务器会自动将请求映射到相应的文件。
另外,Yesod使用声明样式来指定路由。在我们的例子中,它长这样:
mkYesod "HelloWorld" [parseRoutes|
/ HomeR GET
|]
mkYesod
是一个Template Haskell函数,parseRoutes
是一个QuasiQuoter。
用白话描述这句代码就是:在Hello World应用中,创建一个名字叫做HomeR
的路由,它监听映射到/
(应用的根)的请求,而且能响应GET
请求。我们把HomeR
称作一个资源(Resource),这是最后"R"的来历。
对资源命名使用R后缀是只一个惯例,但是大家通常都会遵循这个惯例。因为它让代码更容易读,容易理解。
mkYesod
TH函数产生了相当多的代码:一个路由数据类型,解析器和渲染器函数,一个分发函数,和一些辅助类型。我们会在路由那一章了解更多细节。现在我们使用-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
除了使用
-ddump-splices
,为你的应用生成Haddock文档,查看到底生成了什么数据类型和函数,这个方法也是非常有用的。
我们可以看到RenderRoute
类定义了一个绑定数据类型,它给我们的应用提供了一个路由。这个简单例子中,我们只有一个路由:HomeR
。在真正的应用中,我们会有更多路由,而且要比我们的HomeR
复杂的多。
renderRoute
将一个路由转换成路径片段和查询参数。再次说明,我们的例子非常简单,所以代码看起来也很简单:所以两个值都是空数组。
ParseRoute
提供一个相反的函数,parseRoute
。 这里我们可以看到我们的使用Template Haskell的目的之一:它保证路由的解析和渲染的正确性。这类代码如果手工写的话,很难将他们保持同步。我们使用代码生成,我们可以让编译器(还有Yesod)为我们处理这些细节。
YesodDispatch
提供一个把输入请求传递到相应的处理函数的途径。这个处理过程本质上是:
- 解析请求
- 选择一个处理函数
- 运行处理函数
代码生成会根据一个简单的格式来匹配处理函数的名字,这个我们会在后面介绍。
最后,我们有一个Handler
的类型别名,使得我们写代码更容易。
我们还有很多东西在这里并没有叙述。生成的分发代码为了效率,其实使用了视图模式语言扩展,这样创建了更多类型类实例,这里还有其他情况处理,比如子站点。随着阅读本书,你会深入了解这些细节,特别是到"理解请求"这一章。
处理函数
我们有一个叫HomeR
的路由,它能相应GET
请求。你怎样定义你的响应呢?你写了一个处理函数。Yesod 处理函数命名遵循了一个标准的命名格式:请求方法名字小写后拼接上路由名称。这样,这个处理函数名称就是getHomeR
。
在Yesod中,你写的大多数代码都是在处理函数中。处理用户输入,执行数据库查询和创建相应都是在这里的。在我们简单的例子中,我们使用defaultLayout
函数创建了一个响应。这个函数把你网页模板中给定的内容包装起来。默认情况下,这个函数会产生一个包含html
,head
和body
标记的HTML文件。就像我们在Yesod类型类章节看到的那样,这个方法可以被复写来做更多的事情。
在我们这个例子中,我们把[whamlet|Hello World!|]
传入到defaultLayout
中。whamlet
是另外的quasi-quoter。在这种情况下,它会将Hamlet语法转化成一个组件(Widget)。Hamlet 是Yesod默认的模板引擎。和它的兄弟姐妹,Cassius,Lucius,Julius一起,你可以以一个全类型安全和编译时检查的方式创建HTML,CSS和JavaScript。我们会在Shakespeare章节了解更多内容。
组件(Widget)是Yesod的另外一个基石。它允许你创建包含HTML,CSS和JavaScript的网页的模块化组件,以便于在其他地方复用。我们会在组件那一章详细说明。
Foundation
在我们的例子中,"Hello World"已经出现多次。每个Yesod应用都有一个基础数据类型。这个数据类型必须是Yesod
类型类的实例,它提供了集中的位置来声明一些列不同的设置,来控制我们应用的执行。
在这个例子中,这个数据类型相当的无聊:它没有包含任何信息。但是,这个foundation确实是我们例子怎样运行的核心:它把路由和实例声明绑定起来,使之可以运行。我们会在本书的很多地方见到这个foundation。
但是不是所有foundation都是很无聊:它可以用来存储很多有用的信息,通常是那些在程序加载刚加载完毕就需要被初始化而且一直会被使用的东西。下面是一些常见的例子:
- 数据库连接池
- 从一个配置文件加载设置
- HTTP链接管理器
- 随机数产生器
顺便说一句,这个词Yesod(יסוד)在希伯来语中意为基础(foundation)。
运行
我们不止一次的在我们的main函数中提到HelloWorld
。我们的foundation包含了所有路由和响应请求所需要的信息。现在我们需要把它变得能够运行。在Yesod中,一个非常有用的函数是warp
,这个函数使用一系列默认参数在指定端口(这里是3000)上启动Warp网页服务器.
Yesod有一个特性:不限定于一种发布策略。Yesod 在网页应用接口(Web Application Interface (WAI))上构建,能够在FastCGI,SCGI,Warp,甚至是一个使用WebKit库的桌面应用程序上运行。我们将会在部署那一章详细介绍这些选项,在本章的结尾,我们将会讲解开发使用的服务器。
Warp是Yesod首选的部署选择。它是专为托管Yesod开发了一种轻量,高效的网络服务器。它也可以在Yesod之外的其他Haskell开发中使用(无论是框架或者是非框架应用),在许多生产环境中充当标准文件服务器。
资源和类型安全的URL
在我们的hello world中,我们只是定义了一个简单的资源(HomeR)。一个网页应用通常会不止一个页面:
{-# 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很相似。我们的foundation 使用Links
替换了HelloWorld
,另外,对于HomeR
资源,我们添加了Page1R
和Page2R
。这样,我们也添加了两个处理函数:getPage1R
和getPage2R
。
唯一一点新特性在whamlet
quasi-quotation中。我们将在"Shakespeare"一章中深入了解这种语法,现在我们能看到:
<a href=@{Page1R}>Go to page 1!
创建了一个到Page1R
的链接。注意一下,重点是Page1R
是一个数据构造器。通过给每个资源创建一个数据构造器,我们便有了一个特性叫做type-safe
URL。 我们简单地创建一个原始的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"], [])
在Route Links
类型中,我们有另外两个数据构造器Page1R
和Page2R
。对于renderRoute返回值,我们也有了更好的理解。元组的第一部分是路由给定的文件路径,第二部分是给定的查询参数;对于大多数用例,它可能是一个空列表。
当你开发应用时,type-safe URL带来了巨大的伸缩性和稳定性。你可以随意移动URL,甚至不需要终端连接。(You can move URLs around at will without ever breaking links. )。在路由章节,我们会看到路由可以接受参数,比如一个博客URL附带一个post ID。
比如说,你想通过路由传递一个数字类型的post ID来设置年月日。在传统web框架中,你可能通过每篇博客的引用来传递路由和进行合理的更新。如果你忘了其中一个,你就会得到一个404。在Yesod中,所有你需要做的事情就是更新你的路由和编译:GHC会检查每一行代码,确保其正确性。
没有HTML的响应
Yesod 能够服务于任何你需要的内容类型。对许多常用的响应格式提供了一流的支持。你已经看到了HTML。但是JSON数据也是非常简单的,同过aeson包:
{-# LANGUAGE ExtendedDefaultRules #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import Yesod
data App = App
mkYesod "App" [parseRoutes|
/ HomeR GET
|]
instance Yesod App
getHomeR = return $ object ["msg" .= "Hello World"]
main = warp 3000 App
我们会以后的章节中进一步介绍JSON响应,包括怎样在HTML和JSON之间自动的转换,依赖于请求中的Accept
字段的值。
工具网页(The scaffolded site)
安装Yesod不仅仅会安装Yesod库,也会安装yesod
可执行文件。这个可执行文件接受一些参数,第一个你需要掌握的是yesod init
。它会询问你几个问题,然后创建一个包含默认The scaffolded site的文件夹。在这个文件夹中,你可以运行cabal install --only-dependencies
来构建任何额外的依赖(比如你的数据库后端),然后yesod devel
来运行你的网页。
开始设置你的包环境,请根据快速指南。
The scaffolded site给你带来许多开箱即用的最佳实践,来设置文件和依赖。然而,这些好处都会在学习Yesod中掌握。因此,本书的大多数会避免使用scaffolding工具,而直接把Yesod当成库来处理。但是如果你正在开发一个真正的网站,我强烈建议你使用scaffolding。
在scaffolding那一章,我们会涉及到scaffolded site的结构。
开发服务器
相对于编译型语言,解释型语言的一个好处就是快速原型(fast prototyping):你保存改动,然后刷新。如果你想在Yesod 应用中进行任何改动,你都需要运行runhaskell,这样确实有点烦人。
幸运的是,这里有个解决方案:yesod devel
会为你自动重新构建和加载你的代码。这可能是一个非常好的方式来开发你的Yesod项目,当你准备将应用发布到生产环境,你仍然可以将其编译成高效的代码。Yesod scaffolded会帮你设置好一切。这样带来的好处就是,快速原型和高效的代码两者兼顾。
这里其实还有一点点东西需要设置,我们的例子只会使用warp
,幸运的是,The scaffolded site已经帮你完全的配置好了,当你准备好了上生产环境,它会等待你的进一步指示。
总结
每个Yesod应用都是围绕一个foundation类型创建的。我们关联一些资源和数据类型,定义一些处理函数,然后Yesod会做好所有路由工作,这些资源也是数据构造器,正是这些数据构造器,使我们拥有了类型安全的URLs。
因为实在WAI上构建的,所以Yesod应用可以运行在不同的后端上。举个例子,warp
函数提供了一个方便的Warp web 服务器。对于快速开发,使用yesod devel
是一个很好的选择。当我们准备好将其发布到生产环境,你能很灵活的配置Warp(或者任何其他的WAI处理程序)去满足你的需求。
在Yesod开发时,我们有很多编码方式可供选择:quasi-quotation或者外部文件,warp
或者yesod devel
等等。本书中的例子会倾向于便于复制粘贴的方式,但是当你真正在项目中使用的时候,需要根据实际情况进行选择。