Hello, my name is #{name} and I am #{age} years old. |] provideRep $ return $ object [ "name" .= name , "age" .= age ] where name = "Michael" :: Text age = 28 :: Int main :: IO () main = warp 3000 App ``` `selectRep`函数的功能是从一个不同的数据实例列表中获取一个对应的。每个`provideRep`都对应其中一种。Yesod使用Haskell类型来确定每个对应的mime类型。例如`shamlet`对应`HTML`mime类型则是`text/html`。object生成一个JSON值,这意味着mime类型`application/json`。TypedContent是Yesod为某些带有附加mime类型的原始内容提供的数据类型。我们将在稍后详细介绍它。 要对此进行测试,请启动服务器,然后尝试运行以下不同的curl命令: ```Haskell curl http://localhost:3000 --header "accept: application/json" curl http://localhost:3000 --header "accept: text/html" curl http://localhost:3000 ``` 请注意如何响应是根据accept标头值变化的。当您不设置值时,会是HTML响应。这里的规则是,如果没有设置`accept`值,则返回你的列表的第一个。如果存在`accept`标头,但我们没有匹配到,则返回406“not acceptable”的响应。 默认情况下,Yesod提供了一个便利中间件,允许您通过查询字符串参数设置`accept`标头。这样可以更轻松地进行测试。可以访问`http://localhost3000/?_accept=application/json`,试一试。 ## Json 辅助工具 由于JSON是当今Web应用程序中常用的数据格式,因此我们提供了一些用于提供JSON的内置辅助函数。这些都是由`aeson`库构建的,所以让我们先快速解释一下这个库是如何工作的。 aeson有一个核心数据类型Value,它表示任何有效的JSON值。他还提供了两个类型类`ToJSON`,和`FromJSON`。来自动对Json进行编码和解码。这里我们先来看一个`ToJSON`。让我们看一个Person数据类型创如何实现这个实例。 ```Haskell {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} import Data.Aeson import qualified Data.ByteString.Lazy.Char8 as L import Data.Text (Text) data Person = Person { name :: Text , age :: Int } instance ToJSON Person where toJSON Person {..} = object [ "name" .= name , "age" .= age ] main :: IO () main = L.putStrLn $ encode $ Person "Michael" 28 ``` 我不会详细介绍aeson,因为Haddock文档已经提供了对库的详细介绍。到目前为止,我所介绍的足以说清我们的辅助工具了。 假设您有一个具有相应值的Person数据类型,并且您希望将其用作当前页面的返回值。您可以使用returnJson函数。 ```Haskell {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Data.Text (Text) import Yesod data Person = Person { name :: Text , age :: Int } instance ToJSON Person where toJSON Person {..} = object [ "name" .= name , "age" .= age ] data App = App mkYesod "App" [parseRoutes| / HomeR GET |] instance Yesod App getHomeR :: Handler Value getHomeR = returnJson $ Person "Michael" 28 main :: IO () main = warp 3000 App ``` returnJson实际上是一个微不足道的函数;它其实就是`return . toJSON`。但是,它使事情变得更方便。同样,如果您想在selectRep中提供JSON值作为返回,则可以使用`provideJson`。 ```Haskell {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Data.Text (Text) import Yesod data Person = Person { name :: Text , age :: Int } instance ToJSON Person where toJSON Person {..} = object [ "name" .= name , "age" .= age ] data App = App mkYesod "App" [parseRoutes| / HomeR GET |] instance Yesod App getHomeR :: Handler TypedContent getHomeR = selectRep $ do provideRep $ return [shamlet|
Hello, my name is #{name} and I am #{age} years old. |] provideJson person where person@Person {..} = Person "Michael" 28 main :: IO () main = warp 3000 App ``` provideJson同样是微不足道的,其实就是 `provideRep . returnJson`。 ## 新数据类型 假设我们创建一个基于Haskell的Show实例的新数据格式;我将它称为`Haskell Show`,并给它一个mime类型`text/haskell-show`。然后我们决定从我的Web应用程序中包含这种类型。我该怎么做?作为第一次尝试,让我们直接使用TypedContent数据类型。 ```Haskell {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Data.Text (Text) import Yesod data Person = Person { name :: Text , age :: Int } deriving Show data App = App mkYesod "App" [parseRoutes| / HomeR GET |] instance Yesod App mimeType :: ContentType mimeType = "text/haskell-show" getHomeR :: Handler TypedContent getHomeR = return $ TypedContent mimeType $ toContent $ show person where person = Person "Michael" 28 main :: IO () main = warp 3000 App ``` 这里有一些重要的事情需要注意。 * 我们使用了`toContent`函数。这是一个类型类函数,可以将许多原始数据类型转换为可以发送的数据类型。在上面这种情况下,我们使用了String的实例,它使用UTF8编码。 其他常见数据类型是Text,ByteString,Html和`aeson`的Value。 * 我们直接使用了TypedContent构造函数。它有两个参数:mime类型和数据值。请注意,ContentType只是ByteString的类型别名。 这一切工作的都很好,但令我困扰的是getHomeR的类型签名是如此无法提供信息。虽然getHomeR的实现看起来很漂亮。但是我宁愿有一个表示`Haskell Show`数据的数据类型,并提供一些创建这些值的简单方法。让我们看看这种方法: ```Haskell {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Data.Text (Text) import Yesod data Person = Person { name :: Text , age :: Int } deriving Show data App = App mkYesod "App" [parseRoutes| / HomeR GET |] instance Yesod App mimeType :: ContentType mimeType = "text/haskell-show" data HaskellShow = forall a. Show a => HaskellShow a instance ToContent HaskellShow where toContent (HaskellShow x) = toContent $ show x instance ToTypedContent HaskellShow where toTypedContent = TypedContent mimeType . toContent getHomeR :: Handler HaskellShow getHomeR = return $ HaskellShow person where person = Person "Michael" 28 main :: IO () main = warp 3000 App ``` 这里的魔力在于两个类型类。正如我们之前提到的,ToContent提供了如何将值转换为响应。在我们的例子中,我们希望`show`一下值获得`String`,然后将该`String`转换为原始内容。通常,ToContent的实例将以这种方式相互构建。 ToTypedContent由Yesod在内部使用,并在所有处理函数的返回值上调用。正如您所看到的,实现非常简单,只需传入mime类型然后调用toContent。 最后,让我们让它变得更复杂,并使其与selectRep配合使用。 ```Haskell {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} import Data.Text (Text) import Yesod data Person = Person { name :: Text , age :: Int } deriving Show instance ToJSON Person where toJSON Person {..} = object [ "name" .= name , "age" .= age ] data App = App mkYesod "App" [parseRoutes| / HomeR GET |] instance Yesod App mimeType :: ContentType mimeType = "text/haskell-show" data HaskellShow = forall a. Show a => HaskellShow a instance ToContent HaskellShow where toContent (HaskellShow x) = toContent $ show x instance ToTypedContent HaskellShow where toTypedContent = TypedContent mimeType . toContent instance HasContentType HaskellShow where getContentType _ = mimeType getHomeR :: Handler TypedContent getHomeR = selectRep $ do provideRep $ return $ HaskellShow person provideJson person where person = Person "Michael" 28 main :: IO () main = warp 3000 App ``` 这里补充的是实现`HasContentType`类型类。这似乎是多余的,但它起着重要的作用。我们需要能够在创建返回值之前确定可能返回的mime类型。 `ToTypedContent`仅适用于具体值,因此在创建值之前无法使用。 `getContentType`代替一个代理值,返回类型而不提供任何具体的东西。 > 如果要为没有HasContentType实例的值提供返回,可以使用`provideRepType`函数,该函数要求您显式声明存在的mime类型。 ## 其他请求标头 还有很多其他请求标头可供使用。其中一些仅影响服务器和客户端之间的数据传输,并且根本不应影响应用程序。例如,Accept-Encoding通知服务器客户端理解哪些压缩方案,Host通知服务器提供哪个虚拟主机。 其他会影响应用程序,但Yesod会自动读取。例如,Accept-Language标头指定客户喜欢的人类语言(英语,西班牙语,德语,瑞士语 - 德语)。有关如何使用此标头的详细信息,请参阅i18n章节。 ## 结语 Yesod坚持REST的以下原则: * 使用正确的请求方法。 * 每个资源应该只有一个URL。 * 允许在同一URL上进行多种数据表示 * 检查请求标头以确定有关客户端需要的额外信息。 这使得Yesod不仅可以轻松地用于构建网站,还可以用于构建API。事实上,使用`selectRep/provideRep`等技术,您可以同时提供用户友好的HTML页面和机器友好的JSON页面。