haskell - 将 websockets 合并到 Yesod 中

标签 haskell yesod haskell-stack

如何将 websocket 合并到 Yesod 中?

我使用 yesod-postgres 模板创建了一个项目。

堆栈新 rl yesod-postgres

Handler/Home.hs 文件如下所示(尚未修改):

module Handler.Home where

import Import
import qualified Data.Text.Lazy as TL
import Yesod.Form.Bootstrap3 (BootstrapFormLayout (..), renderBootstrap3)
import Text.Julius (RawJS (..))

-- Define our data that will be used for creating the form.
data FileForm = FileForm
{ fileInfo :: FileInfo
, fileDescription :: Text
}

getHomeR :: Handler Html
getHomeR = do
    (formWidget, formEnctype) <- generateFormPost sampleForm
    let submission = Nothing :: Maybe FileForm
        handlerName = "getHomeR" :: Text
    defaultLayout $ do
        let (commentFormId, commentTextareaId, commentListId) = commentIds
        aDomId <- newIdent
        setTitle "Welcome To Yesod!"
        $(widgetFile "homepage")

postHomeR :: Handler Html
postHomeR = do
    ((result, formWidget), formEnctype) <- runFormPost sampleForm
    let handlerName = "postHomeR" :: Text
        submission = case result of
            FormSuccess res -> Just res
            _ -> Nothing
    defaultLayout $ do
        let (commentFormId, commentTextareaId, commentListId) = commentIds
        aDomId <- newIdent
        setTitle "Welcome To Yesod!"
        $(widgetFile "homepage")

sampleForm :: Form FileForm
sampleForm = renderBootstrap3 BootstrapBasicForm $ FileForm
    <$> fileAFormReq "Choose a file"
    <*> areq textField textSettings Nothing
    -- Add attributes like the placeholder and CSS classes.
    where textSettings = FieldSettings
            { fsLabel = "What's on the file?"
            , fsTooltip = Nothing
            , fsId = Nothing
            , fsName = Nothing
            , fsAttrs =
                [ ("class", "form-control")
                , ("placeholder", "File description")
                ]
            }

commentIds :: (Text, Text, Text)
commentIds = ("js-commentForm", "js-createCommentTextarea", "js-commentList")

这是来自 github 的 websockets 示例:

{-# LANGUAGE QuasiQuotes, TemplateHaskell, TypeFamilies, OverloadedStrings #-}
import Yesod.Core
import Yesod.WebSockets
import qualified Data.Text.Lazy as TL
import Control.Monad (forever)
import Control.Concurrent (threadDelay)
import Data.Time
import Data.Conduit
import qualified Data.Conduit.List

data App = App

instance Yesod App

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

timeSource :: MonadIO m => Source m TL.Text
timeSource = forever $ do
    now <- liftIO getCurrentTime
    yield $ TL.pack $ show now
    liftIO $ threadDelay 5000000

getHomeR :: Handler Html
getHomeR = do
    webSockets $ race_
        (sourceWS $$ Data.Conduit.List.map TL.toUpper =$ sinkWSText)
        (timeSource $$ sinkWSText)
    defaultLayout $
        toWidget
            [julius|
                var conn = new WebSocket("ws://localhost:3000/");
                conn.onopen = function() {
                    document.write("<p>open!</p>");
                    document.write("<button id=button>Send another message</button>")
                    document.getElementById("button").addEventListener("click", function(){
                        var msg = prompt("Enter a message for the server");
                        conn.send(msg);
                    });
                    conn.send("hello world");
                };
                conn.onmessage = function(e) {
                    document.write("<p>" + e.data + "</p>");
                };
                conn.onclose = function () {
                    document.write("<p>Connection Closed</p>");
                };
            |]

main :: IO ()
main = warp 3000 App

根据上面的示例,我尝试将下面的这些代码插入到 Handler/Home.hs 中。

...
import qualified Data.Text.Lazy as TL
....

timeSource :: MonadIO m => Source m TL.Text
timeSource = forever $ do
    now <- liftIO getCurrentTime
    yield $ TL.pack $ show now
    liftIO $ threadDelay 5000000
....
getHomeR = do
    webSockets $ race_
        (sourceWS $$ Data.Conduit.List.map TL.toUpper =$ sinkWSText)
        (timeSource $$ sinkWSText)
...
...
postHomeR = do
    webSockets $ race_
        (sourceWS $$ Data.Conduit.List.map TL.toUpper =$ sinkWSText)
        (timeSource $$ sinkWSText)

这是最终结果:

{-# LANGUAGE QuasiQuotes, TemplateHaskell, TypeFamilies, OverloadedStrings #-}

module Handler.Home where

import Import
import qualified Data.Text.Lazy as TL
import qualified Data.Conduit.List
import Yesod.Form.Bootstrap3 (BootstrapFormLayout (..), renderBootstrap3)
import Text.Julius (RawJS (..))

-- Define our data that will be used for creating the form.
data FileForm = FileForm
    { fileInfo :: FileInfo
    , fileDescription :: Text
    }

-- This is a handler function for the GET request method on the HomeR
-- resource pattern. All of your resource patterns are defined in
-- config/routes
--
-- The majority of the code you will write in Yesod lives in these handler
-- functions. You can spread them across multiple files if you are so
-- inclined, or create a single monolithic file.
--

timeSource :: MonadIO m => Source m TL.Text
timeSource = forever $ do
  now <- liftIO getCurrentTime
  yield $ TL.pack $ show now
  liftIO $ threadDelay 5000000

getHomeR :: Handler Html
getHomeR = do
    (formWidget, formEnctype) <- generateFormPost sampleForm
    let submission = Nothing :: Maybe FileForm
        handlerName = "getHomeR" :: Text
    webSockets $ race_
        (sourceWS $$ Data.Conduit.List.map TL.toUpper =$ sinkWSText)
        (timeSource $$ sinkWSText)      
    defaultLayout $ do
        let (commentFormId, commentTextareaId, commentListId) = commentIds
        aDomId <- newIdent
        setTitle "Welcome To Yesod!"
        $(widgetFile "homepage")

postHomeR :: Handler Html
postHomeR = do
    ((result, formWidget), formEnctype) <- runFormPost sampleForm
    let handlerName = "postHomeR" :: Text
        submission = case result of
            FormSuccess res -> Just res
            _ -> Nothing
    webSockets $ race_
        (sourceWS $$ Data.Conduit.List.map TL.toUpper =$ sinkWSText)
        (timeSource $$ sinkWSText)       
    defaultLayout $ do
        let (commentFormId, commentTextareaId, commentListId) = commentIds
        aDomId <- newIdent
        setTitle "Welcome To Yesod!"
        $(widgetFile "homepage")

sampleForm :: Form FileForm
sampleForm = renderBootstrap3 BootstrapBasicForm $ FileForm
    <$> fileAFormReq "Choose a file"
    <*> areq textField textSettings Nothing
    -- Add attributes like the placeholder and CSS classes.
    where textSettings = FieldSettings
            { fsLabel = "What's on the file?"
            , fsTooltip = Nothing
            , fsId = Nothing
            , fsName = Nothing
            , fsAttrs =
                [ ("class", "form-control")
                , ("placeholder", "File description")
                ]
            }

commentIds :: (Text, Text, Text)
commentIds = ("js-commentForm", "js-createCommentTextarea", "js-commentList")

但是当我进行堆栈构建时,我收到了这些错误:

rl-0.0.0: build (lib + exe)
Preprocessing library rl-0.0.0...
[10 of 11] Compiling Handler.Home     ( Handler/Home.hs, .stack-work/dist/x86_64-osx/Cabal-1.24.2.0/build/Handler/Home.o )

/Users/ee/Projects/Haskell Projects/rl/Handler/Home.hs:37:5: error:
    Variable not in scope: webSockets :: m0 () -> HandlerT App IO a0

/Users/ee/Projects/Haskell Projects/rl/Handler/Home.hs:37:18: error:
    • Couldn't match type ‘StM
                             m0 (constraints-0.9.1:Data.Constraint.Forall.Skolem (Pure m0))’
                     with ‘constraints-0.9.1:Data.Constraint.Forall.Skolem (Pure m0)’
        arising from a use of ‘race_’
      The type variable ‘m0’ is ambiguous
    • In the second argument of ‘($)’, namely
        ‘race_
           (sourceWS $$ Data.Conduit.List.map TL.toUpper =$ sinkWSText)
           (timeSource $$ sinkWSText)’
      In a stmt of a 'do' block:
        webSockets
        $ race_
            (sourceWS $$ Data.Conduit.List.map TL.toUpper =$ sinkWSText)
            (timeSource $$ sinkWSText)
      In the expression:
        do { (formWidget, formEnctype) <- generateFormPost sampleForm;
             let submission = ...
                 handlerName = ...;
             webSockets
             $ race_
                 (sourceWS $$ Data.Conduit.List.map TL.toUpper =$ sinkWSText)
                 (timeSource $$ sinkWSText);
             defaultLayout
             $ do { let ...;
                    aDomId <- newIdent;
                    .... } }

关于如何使 websocket 与 yesod-postgres 一起工作有什么想法吗?

PS:我目前使用的是 ghc-8.02。如果您想尝试上面的代码并拥有相同的 ghc,您可能会遇到 websockets 的 stm-lifted 依赖问题。解压 stm-lifted 并修改其 cabal 文件(更改 transformers 版本)。

更新 1:

下面的代码已编译。我会尝试添加一些julius。我稍后会发布更新。

{-# LANGUAGE QuasiQuotes, TemplateHaskell, TypeFamilies, OverloadedStrings #-}

module Handler.Home where

import Import
import qualified Yesod.WebSockets as YW
import qualified Data.Text.Lazy as TL
--import Control.Concurrent (threadDelay)
--import Data.Time
--import Data.Conduit
import qualified Data.Conduit.List
import Yesod.Form.Bootstrap3 (BootstrapFormLayout (..), renderBootstrap3)
import Text.Julius (RawJS (..))

-- Define our data that will be used for creating the form.
data FileForm = FileForm
    { fileInfo :: FileInfo
    , fileDescription :: Text
    }

-- This is a handler function for the GET request method on the HomeR
-- resource pattern. All of your resource patterns are defined in
-- config/routes
--
-- The majority of the code you will write in Yesod lives in these handler
-- functions. You can spread them across multiple files if you are so
-- inclined, or create a single monolithic file.
--

timeSource :: MonadIO m => Source m TL.Text
timeSource = forever $ do
  now <- liftIO getCurrentTime
  yield $ TL.pack $ show now
  liftIO $ threadDelay 5000000

getHomeR :: Handler Html
getHomeR = do
    (formWidget, formEnctype) <- generateFormPost sampleForm
    let submission = Nothing :: Maybe FileForm
        handlerName = "getHomeR" :: Text
    YW.webSockets $ YW.race_
        (YW.sourceWS $$ Data.Conduit.List.map TL.toUpper =$ YW.sinkWSText)
        (timeSource $$ YW.sinkWSText)
    defaultLayout $ do
        let (commentFormId, commentTextareaId, commentListId) = commentIds
        aDomId <- newIdent
        setTitle "Welcome To Yesod!"
        $(widgetFile "homepage")

postHomeR :: Handler Html
postHomeR = do
    ((result, formWidget), formEnctype) <- runFormPost sampleForm
    let handlerName = "postHomeR" :: Text
        submission = case result of
            FormSuccess res -> Just res
            _ -> Nothing
    YW.webSockets $ YW.race_
        (YW.sourceWS $$ Data.Conduit.List.map TL.toUpper =$ YW.sinkWSText)
        (timeSource $$ YW.sinkWSText)
    defaultLayout $ do
        let (commentFormId, commentTextareaId, commentListId) = commentIds
        aDomId <- newIdent
        setTitle "Welcome To Yesod!"
        $(widgetFile "homepage")

sampleForm :: Form FileForm
sampleForm = renderBootstrap3 BootstrapBasicForm $ FileForm
    <$> fileAFormReq "Choose a file"
    <*> areq textField textSettings Nothing
    -- Add attributes like the placeholder and CSS classes.
    where textSettings = FieldSettings
            { fsLabel = "What's on the file?"
            , fsTooltip = Nothing
            , fsId = Nothing
            , fsName = Nothing
            , fsAttrs =
                [ ("class", "form-control")
                , ("placeholder", "File description")
                ]
            }

commentIds :: (Text, Text, Text)
commentIds = ("js-commentForm", "js-createCommentTextarea", "js-commentList")

最佳答案

这里我解决了这个问题:

{-# LANGUAGE QuasiQuotes, TemplateHaskell, TypeFamilies, OverloadedStrings #-}

module Handler.Home where

import Import
import qualified Yesod.WebSockets as YW
import qualified Data.Text.Lazy as TL
--import Control.Concurrent (threadDelay)
--import Data.Time
--import Data.Conduit
import qualified Data.Conduit.List
import Yesod.Form.Bootstrap3 (BootstrapFormLayout (..), renderBootstrap3)
import Text.Julius (RawJS (..))

-- Define our data that will be used for creating the form.
data FileForm = FileForm
    { fileInfo :: FileInfo
    , fileDescription :: Text
    }

-- This is a handler function for the GET request method on the HomeR
-- resource pattern. All of your resource patterns are defined in
-- config/routes
--
-- The majority of the code you will write in Yesod lives in these handler
-- functions. You can spread them across multiple files if you are so
-- inclined, or create a single monolithic file.
--

timeSource :: MonadIO m => Source m TL.Text
timeSource = forever $ do
  now <- liftIO getCurrentTime
  yield $ TL.pack $ show now
  liftIO $ threadDelay 100000

getHomeR :: Handler Html
getHomeR = do
    (formWidget, formEnctype) <- generateFormPost sampleForm
    let submission = Nothing :: Maybe FileForm
        handlerName = "getHomeR" :: Text
    YW.webSockets $ YW.race_
        (YW.sourceWS $$ Data.Conduit.List.map TL.toUpper =$ YW.sinkWSText)
        (timeSource $$ YW.sinkWSText)
    defaultLayout $ do
        let (commentFormId, commentTextareaId, commentListId) = commentIds
        aDomId <- newIdent
        setTitle "Welcome To Yesod!"
        $(widgetFile "homepage")

postHomeR :: Handler Html
postHomeR = do
    ((result, formWidget), formEnctype) <- runFormPost sampleForm
    let handlerName = "postHomeR" :: Text
        submission = case result of
            FormSuccess res -> Just res
            _ -> Nothing
    YW.webSockets $ YW.race_
        (YW.sourceWS $$ Data.Conduit.List.map TL.toUpper =$ YW.sinkWSText)
        (timeSource $$ YW.sinkWSText)
    defaultLayout $ do
        let (commentFormId, commentTextareaId, commentListId) = commentIds
        aDomId <- newIdent
        setTitle "Welcome To Yesod!"
        $(widgetFile "homepage")

sampleForm :: Form FileForm
sampleForm = renderBootstrap3 BootstrapBasicForm $ FileForm
    <$> fileAFormReq "Choose a file"
    <*> areq textField textSettings Nothing
    -- Add attributes like the placeholder and CSS classes.
    where textSettings = FieldSettings
            { fsLabel = "What's on the file?"
            , fsTooltip = Nothing
            , fsId = Nothing
            , fsName = Nothing
            , fsAttrs =
                [ ("class", "form-control")
                , ("placeholder", "File description")
                ]
            }

commentIds :: (Text, Text, Text)
commentIds = ("js-commentForm", "js-createCommentTextarea", "js-commentList")

我将下面的代码添加到 homepage.julius 中。

var conn = new WebSocket("ws://localhost:3000/");
                conn.onopen = function() {
                    document.write("<p>open!</p>");
                    document.write("<button id=button>Send another message</button>")
                    document.getElementById("button").addEventListener("click", function(){
                        var msg = prompt("Enter a message for the server");
                        conn.send(msg);
                    });
                    conn.send("hello world");
                };
                conn.onmessage = function(e) {
                    document.write("<p>" + e.data + "</p>");
                };
                conn.onclose = function () {
                    document.write("<p>Connection Closed</p>");
                };
<小时/> <小时/>

结果如下:

<小时/>

enter image description here

<小时/>

关于haskell - 将 websockets 合并到 Yesod 中,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43255043/

相关文章:

haskell - 为什么 newtype 语法创建函数

haskell - 使用持久性输入与数据库的关系

haskell - 使用 esqueleto 计算行数

haskell - 通过堆栈安装的 LTS 版本

haskell - 使用 Haddock 和 stack 为我自己的代码生成文档

haskell - 什么时候泛型类型不是 monad?

haskell - 条件计算 Maybe (IO ())

haskell 。如何在纯 Haskell 函数中执行 IO?如何在执行函数时打印中间结果?

haskell - Yesod数据库持久记录访问

haskell - 什么会导致 `stack build` 每次都取消注册本地依赖项?