Yesod - 路由和响应 (6)

如果我们把Yesod看成一个MVC的框架,路由和响应就对应着C(控制器)。作为对比让我们先看看其他两种Web框架中的路由响应方法:

  • 根据文件名响应,php和asp就是这么做的。
  • 有一个可以根据正则表达式解析路由的中心函数,Django和Rails就是用的这种方法。

Yesod更接近第二种方法,但是还是存在着一些差异的。Yesod不是使用正则表达式而是匹配路由的每个路径段。而且Yesod不是只有一种从路由到响应的映射。Yesod拥有中间的数据类型(称为路由数据,或者类型安全的URL),并且有双向的转换函数。
手动为一些高级的系统编码是乏味并且容易出错的。因此Yesod为路由定义了领域特定语言(DSL),并且提供了Template Haskell函数把DSL转化为Haskell代码。这章将展开说明路由的声明语法,并让你看到为你生成了哪些代码。还有路由和响应函数之间的相互关系。

路由语法

Yesod使用专为路由设计的简化语法,而不是使用现有语法试图声明路由。这不仅使Yesod的代码利于阅读,而且能使一些不懂Yesod很容易理解你的站点地图。   一些简单的例子:

/             HomeR     GET
/blog         BlogR     GET POST
/blog/#BlogId BlogPostR GET POST

/static       StaticR   Static getStatic

下面将详细介绍路由定义的详细细节。

路由段

Yesod在收到请求时所做的第一件事就是将请求的路径拆分成路由段。这些路由段通过/分割 例如:

toPieces "/" = []
toPieces "/foo/bar/baz/" = ["foo", "bar", "baz", ""]

你可能会发现最后的斜杠和双斜杠(/foo//bar//)或者其他事情,有一些特殊的表现。Yesod遵循规范的URLs。如果用户的请求带有尾斜杠或者有双斜杠,他们会被重定向到规范的版本。这确保每一份URL对应一份资源,并且有利于搜索引擎优化。   这意味着你不用关心URL的结构:你可以安全的处理路径的各个部分,并且Yesod会自动处理斜线和转义字符问题。
顺便一提,如果你想精确的控制路径段的分割和连接。你可以查看上一节的Yesod类型类的cleanPathjoinPath函数。

路径段的类型

当你声明你的路径段时,您可以使用三种类型:

Static

这是一个纯字符串,需要在在URL中精确匹配。

Dynamic single

这是一个路径段(即两个正斜杠之间),但代表用户提交的值。 这是在页面请求上接收额外用户输入的主要方法。这个路径段必须以#开始后面跟一个数据类型,这个数据类型必须是PathPiece的实例。

Dynamic multi

和上一个基本相同,但是可以接收多个参数。这必须是最后一个路径段。他已*号开始后面跟数据类型,必须是PathMultiPiece的一个实例。这个并不像其他两个部分那样常见,尽管它们对于实现诸如表示文件结构的静态树或具有任意层次结构的wiki的功能非常重要。

从Yesod 1.4开始,您还可以使用+来表示动态Dynamic multi。这很重要,因为C预处理器可能会被/ *字符组合混淆。

让我们来看看您可能想要编写的一些标准资源模式。比较简单的,应用程序的根目录就是/。你可能希望将常见问题问答放在/page/faq中。
现在让我们假设你要写一个Fibonacci网站。你可以把你的URL定义为/fib/#Int。但是有个小问题我们不想要负数和零。幸运的是,类型系统可以保护我们:

newtype Natural = Natural Int
instance PathPiece Natural where
    toPathPiece (Natural i) = T.pack $ show i
    fromPathPiece s =
        case reads $ T.unpack s of
            (i, ""):_
                | i < 1 -> Nothing
                | otherwise -> Just $ Natural i
            [] -> Nothing

在第一行,我们定义了一个简单的newtype类型,来保护我们的免受无效的输入。我们可以看到PathPiece类型类有两个函数。toPathPiece把当前类型转换为TextfromPathPiece将当前类型尝试转换为Text。当无法转换时返回Nothing。通过使用这种数据类型,我们可以确保我们的处理函数只被赋予自然数,这使我们再次使用类型系统来解决边界问题。

在实际应用中,我们还希望确保我们不会在内部意外地为我们的应用构建无效的自然值。为此,我们可以使用像智能构造函数这样的方法。出于本示例的目的,我们保持代码简单。

定义PathMultiPiece同样简单。假设我们想要一个至少具有两级层次结构的Wiki;我们可以定义一个数据类型,例如:

data Page = Page Text Text [Text] -- 2 or more
instance PathMultiPiece Page where
    toPathMultiPiece (Page x y z) = x : y : z
    fromPathMultiPiece (x:y:z) = Just $ Page x y z
    fromPathMultiPiece _ = Nothing

重叠检测

默认情况下,Yesod将确保没有两条路由可能相互重叠。例如:

/foo/bar   Foo1R GET
/foo/#Text Foo2R GET

这样重叠的路由是会被拒绝的,因为/foo/bar会匹配两个路由。但是有两种情况我们会允许重叠: 1.我们通过数据类型的定义知道重叠永远不会发生。如果你把上面的Text替换成Int。这很容易确定这不会重叠,Yesod目前无法进行此类分析。 2.您对应用程序的运行方式有一些额外的了解,并且知道永远不应该允许这种情况。例如,Foo2R绝对不允许收到bar

你可以在路由开头加上感叹号!,例如:

/foo/bar    Foo1R GET
!/foo/#Int  Foo2R GET
!/foo/#Text Foo3R GET

路由重叠产生的一个问题是模糊性。在上面的例子中,/foo/bar到底是先响应Foo1R还是Foo3R/foo/42Foo2R先还是Foo3R先。Yesod对这些的规则很简单,越上面的越优先响应。

响应名称

每个路由也都有一个与之关联的名称。该名称将成为你应用程序关联的类型安全URL类型的构造函数。因此,它必须以大写字母开头。按照惯例,这些资源名称都以大写字母R结尾。没有什么可以迫使你这样做,这只是常见的做法。   构造函数的精确定义,依赖于他的资源定义。不管你的数据类型使用single-pieces还是multi-pieces,都会成为数据类型的参数。这使我们的类型安全URL值与应用程序中的有效URL之间保持一对一的对应关系。

这并不一定意味着每个值都是一个工作页面,只是它是一个潜在有效的URL。例如,如果数据库中没有Michael,则值PersonR“Michael”可能无法解析为有效页面。

让我们举一些实际的例子。/person/#Text对应PersonR,/year/#Int对应YearR,/page/faq对应FaqR。你最终得到的路由数据类型大致如下:

data MyRoute = PersonR Text
             | YearR Int
             | FaqR

如果用户请求/year/2009,Yesod会将他转化为YearR 2009/person/Michael对应PersonR "Michael",/page/faq对应FaqR。另一方面/year/two-thousand-nine``/person/michael/snoyman``/page/FAQ,如果没有看到你的代码,都会导致404错误。 ##Handler 规范 声明路由后最后一个难题是如何处理它们。Yesod有三个选项:

  • 一个请求对应一个单独的handler函数
  • 给定路由上每种请求方法的单独处理函数。任何其他请求方法都将生成405 Method Not Allowed响应。
  • 你想要传递给子网站。

前两个可以很容易地规范。一个单独的handler函数,将只有一行分别是路由声明和响应资源名。例如/page/faq FaqR。handler函数必须命名为handleFaqR
第二个请求方法必须全是大写字母。例如/person/#String PersonR GET POST DELETE,你需要定义对应的handler函数分别是getPersonR``postPersonR``deletePersonR
在Yesod,子网站是一个非常有用但更复杂的主题。我们稍后将介绍编写子网站,但使用它们并不太困难。最常用的子网站是静态子网站,它为您的应用程序提供静态文件。为了从/static提供静态文件,您需要一行,如:

/static StaticR Static getStatic

在这一行中,/static只是说你的URL结构中的哪个位置来提供静态文件。静态这个词没什么神奇的,你可以用/my/non-dynamic/files轻松替换它。   下一个单词StaticR给出了响应名称。接下来的两个单词指定我们使用的是子网站。Static是子网站foundation数据类型的名称,getStatic是一个从主 foundation数据类型的值获取静态值的函数。   现在让我们不要过于关注scaffolded的细节。我们将更详细地讨scaffolded网站章节中的静态子网站。

派发

一旦您指定了路由,Yesod将为您处理所有令人讨厌的URL派发细节。你只需提供适当的handler函数。对于子网站路由,您不需要编写任何处理函数,但是您可以执行其他两个。我们上面提到了命名规则(MyHandlerR GET变为getMyHandlerRMyOtherHandlerR变为handleMyOtherHandlerR)。
现在我们知道需要编写哪些函数,让我们弄清楚它们的类型签名应该是什么。

返回类型

让我们看一个简单的handler函数:

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

getHomeR :: Handler Html
getHomeR = defaultLayout [whamlet|<h1>This is simple|]

此返回类型有两个组件:Handler和Html。让我们更深入地分析他们。

Handler monad

与Widget类型一样,Handler数据类型没有在Yesod库中的任何位置定义。相反,库提供数据类型:

data HandlerT site m a

像WidgetT一样这里有三个参数,一个monad m,值a,和foundation的类型site。每个应用程序都定义一个Handler,它将站点约束到该应用程序的foundation数据类型,并将m设置为IO。你的foundation是MyApp,那么你就有:

type Handler = HandlerT MyApp IO

我们需要能够在编写子网站时修改底层monad,否则我们将使用IO。
HandlerT monad提供了访问用户请求信息的访问(例如查询字符串参数),修改响应头等。大部分代码都将存在于此。另外,还有一个名为MonadHandler的类型类。HadlerT同WidgetT一样,都实现了这个类型类。所以两个monad之间有很多相同的函数。如果您在任何API文档中看到MonadHandler,那么记住该函数可以在您的Handler函数中使用。

Html

这个类型没什么太令人惊讶的。这个功能通过Html数据类型返回一些HTML内容。但是,如果它只允许生成HTML响应,那么Yesod提供他就没有意义。我们想要返回CSS,Javascript,JSON,图像等。所以问题来了:可以返回哪些数据类型?   为了生成响应,我们需要知道两条信息:内容类型(例如,text/htmlimage/png)以及如何将其序列化为字节流。这由TypedContent数据类型表示:

data TypedContent = TypedContent !ContentType !Content

我们还有一个可以转换为TypedContent的所有数据类型的类型:

class ToTypedContent a where
    toTypedContent :: a -> TypedContent

许多常见的数据类型是此类型类的实例,包括Html,Value(来自aeson包,表示JSON),Text和even()(用于表示空响应)。

参数

让我们回到上面的简单示例:

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

getHomeR :: Handler Html
getHomeR = defaultLayout [whamlet|<h1>This is simple|]

并非每种路由都像HomeR一样简单。以我们之前的PersonR路由为例。需要将人的名称传递给处理函数。例如:

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes       #-}
{-# LANGUAGE TemplateHaskell   #-}
{-# LANGUAGE TypeFamilies      #-}
{-# LANGUAGE ViewPatterns      #-}
import           Data.Text (Text)
import qualified Data.Text as T
import           Yesod

data App = App
instance Yesod App

mkYesod "App" [parseRoutes|
/person/#Text PersonR GET
/year/#Integer/month/#Text/day/#Int DateR
/wiki/*Texts WikiR GET
|]

getPersonR :: Text -> Handler Html
getPersonR name = defaultLayout [whamlet|<h1>Hello #{name}!|]

handleDateR :: Integer -> Text -> Int -> Handler Text -- text/plain
handleDateR year month day =
    return $
        T.concat [month, " ", T.pack $ show day, ", ", T.pack $ show year]

getWikiR :: [Text] -> Handler Text
getWikiR = return . T.unwords

main :: IO ()
main = warp 3000 App

参数按顺序与类型与参数一一对应。另外,请注意我们如何使用Html和Text返回值。

Handler函数

由于您的大多数代码都将存在于Handler monad中,因此花一些时间更好地理解它是很重要的。本章的其余部分将简要介绍Handler monad中最常见的一些函数。我没有涵盖任何Session功能;这将在会议章节中讨论。

应用信息

有许多函数可以返回有关您的应用程序的整体信息,并且不提供有关各个请求的信息。其中一些是:

getYesod

返回应用程序的foundation值。如果您在foundation中存储配置信息,您可能会使用此功能。(如果你这么倾向,你也可以使用来自Control.Monad.Reader的ask; getYesod和它同构的。)

getUrlRender

返回URL的渲染函数,这个函数将类型安全的URL装换为Text。可能在Hamet中使用它。

getUrlRenderParams

getUrlRender的一种变体,它可以转换类型安全的URL和查询字符串参数列表。

请求信息

您希望获得的有关当前请求的最常见信息是请求的路径,查询字符串参数和POST的表单数据。第一个在路由中处理。其他两个最好使用表单模块处理。
也就是说你有时候需要以更原始的格式获取数据。为此,Yesod提供了YesodRequest数据类型你可以用getRequest函数来获取它。这使您可以访问GET参数,cookie和首选语言的完整列表。有一些方便的函数可以使这些查找更容易,例如lookupGetParam,lookupCookie和languages。对于POST参数的访问,您应该使用runRequestBody。
如果你需要更多原始数据(如请求标头),则可以使用waiRequest访问Web应用程序接口(WAI)请求值。有关详细信息,请参阅WAI附录。

Short Circuiting

以下函数立即结束处理函数的执行并将结果返回给用户。

redirect

向用户发送重定向响应(303响应)。如果要使用其他响应代码(例如,永久301重定向),可以使用redirectWith。

Yesod对HTTP / 1.1客户端使用303响应,对HTTP / 1.0客户端使用302响应。你可以在HTTP规范中阅读这个历史。

notFound

返回404回复。如果用户请求不存在的数据库值,这将非常有用。

permissionDenied

返回带有特定错误消息的403响应。

invalidArgs

带有无效参数列表的400响应。

sendFile

从具有指定内容类型的文件系统发送文件。这是发送静态文件的首选方法,因为底层WAI处理程序可能能够将其优化为sendfile系统调用。不需要使用readFile发送静态文件。

sendResponse

使用200状态代码发送正常响应。当你需要通过立即响应突破一些深度嵌套的代码时,这实际上只是一种便利。可以使用ToTypedContent的任何实例。

sendWaiResponse

当您需要获得低级别并发送原始WAI响应时。这对于创建流式响应或服务器发送事件等技术特别有用。

响应头

setCookie

在客户端上设置cookie。该函数不是采用过期日期,而是在几分钟内获取cookie持续时间。请记住,在以下请求之前,您不会使用lookupCookie看到此cookie。

deleteCookie

告诉客户删除cookie。在下一个请求之前,lookupCookie将再次反映此更改。

setHeader

设置任意响应头。

setLanguage

设置首选用户语言,该语言将显示在语言功能的结果中。

cacheSeconds

设置Cache-Control标头以指示可以缓存此响应的秒数。如果您在服务器上使用Varnish,这可能特别有用

neverExpires

将Expires标头设置为2037年。您可以将此内容用于永不过期的内容,例如请求路径具有与之关联的哈希值时。

alreadyExpired

将Expires标头设置为过期。

expiresAt

将Expires标头设置为指定的日期/时间。

I/O 和调试

HandlerT和WidgetT monad transformers是很多类型类的实例。对于本节,重要的类型类是MonadIO和MonadLogger。前者允许您在处理程序中执行任意IO操作,例如从读取文件。为了实现这一点,您只需要将liftIO添加到代码中。
MonadLogger提供内置的日志系统。您可以通过多种方式自定义此系统,例如记录哪些类型的消息以及输出日志的位置。默认情况下,日志会发送到标准输出,在开发阶段类型中会输出所有消息,但在生产阶段类型中只会记录警告和错误。   通常,在进行打印日志时,我们想知道日志对应的源代码中的位置。为此,MonadLogger提供了许多方便的Template Haskell函数,这些函数会自动将源代码位置插入到日志消息中。这些函数是$ logDebug$ logInfo$ logWarn$ logError。让我们看一些这些函数的简短示例。

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes       #-}
{-# LANGUAGE TemplateHaskell   #-}
{-# LANGUAGE TypeFamilies      #-}
import           Control.Exception (IOException, try)
import           Control.Monad     (when)
import           Yesod

data App = App
instance Yesod App where
    -- This function controls which messages are logged
    shouldLogIO App src level = return $
        True -- good for development
        -- level == LevelWarn || level == LevelError -- good for production

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

getHomeR :: Handler Html
getHomeR = do
    $logDebug "Trying to read data file"
    edata <- liftIO $ try $ readFile "datafile.txt"
    case edata :: Either IOException String of
        Left e -> do
            $logError $ "Could not read datafile.txt"
            defaultLayout [whamlet|An error occurred|]
        Right str -> do
            $logInfo "Reading of data file succeeded"
            let ls = lines str
            when (length ls < 5) $ $logWarn "Less than 5 lines of data"
            defaultLayout
                [whamlet|
                    <ol>
                        $forall l <- ls
                            <li>#{l}
                |]

main :: IO ()
main = warp 3000 App

查询字符串和Hash片段

我们已经研究了许多可以处理类似URL的事情的函数,例如重定向。这些函数都适用于类型安全的URL,但它们还能用于什么?有一个名为RedirectUrl的类型类,它包含将某些类型转换为文本URL的逻辑。这包括类型安全的URL,文本URL和两个特殊实例:

  1. URL的元组和查询字符串参数的键/值对列表。
  2. Fragment数据类型,用于将哈希片段添加到URL的末尾。

这两个实例都允许您将额外信息“添加”到类型安全的URL。让我们看一些如何使用它们的例子

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes       #-}
{-# LANGUAGE TemplateHaskell   #-}
{-# LANGUAGE TypeFamilies      #-}
import           Data.Text        (Text)
import           Yesod

data App = App

mkYesod "App" [parseRoutes|
/      HomeR  GET
/link1 Link1R GET
/link2 Link2R GET
/link3 Link3R GET
/link4 Link4R GET
|]

instance Yesod App where

getHomeR :: Handler Html
getHomeR = defaultLayout $ do
    setTitle "Redirects"
    [whamlet|
        <p>
            <a href=@{Link1R}>Click to start the redirect chain!
    |]

getLink1R, getLink2R, getLink3R :: Handler ()
getLink1R = redirect Link2R -- /link2
getLink2R = redirect (Link3R, [("foo", "bar")]) -- /link3?foo=bar
getLink3R = redirect $ Link4R :#: ("baz" :: Text) -- /link4#baz

getLink4R :: Handler Html
getLink4R = defaultLayout
    [whamlet|
        <p>You made it!
    |]

main :: IO ()
main = warp 3000 App

当然,在Hamlet模板中,这通常不是必需的,因为您可以直接在URL之后包含哈希,例如:

<a href=@{Link1R}#somehash>Link to hash

结语

路由和调度可以说是Yesod的核心:从这里我们定义了类型安全的URL,并且我们的大部分代码都是在Handler monad中编写的。本章介绍了Yesod的一些最重要和最重要的概念,因此正确消化它非常重要。   本章还暗示了一些我们将在稍后介绍的更复杂的Yesod主题。但是到这里你应该能够用你所学到的知识编写一些非常复杂的Web应用程序。

转载于:https://my.oschina.net/mzui/blog/1935328

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值