神刀安全网

Integrating Elm and Phoenix Channels via Elm-Phoenix-socket

In today’s episode, we’re going to look at elm-phoenix-socket , which is "a pure Elm interpretation of the Phoenix.Socket library that comes bundled with the Phoenix web framework."

Hey, this is an unusually long episode, enjoy!

Project

I’ve tagged a Phoenix.Presence-based chat application with before_episode_011.2 . You’ll need this pulled down and running in order to use it as your communications endpoint. Here’s how you can get it running:

git clone git@github.com:knewter/presence_chat cd presence_chat git checkout before_episode_011.2 mix deps.get mix ecto.create npm install mix phoenix.server 

For what it’s worth, we built this application in this episode in the Elixir topic .

Now it’s running. You can see the chat application in your browser, by visiting http://localhost:4000 . enter a username and start chatting. Open it up in multiple tabs, on other devices, etc. and check it out. We’ll leave that running in a tab.

Next, we’ll start a new elm project to interact with it:

mkdir elm_presence_chat cd elm_presence_chat elm package install -y fbonetti/elm-phoenix-socket elm package install -y elm-lang/html 

Now we’ll build out an application. I’m not going to build up the standard application structure just out of expediency, and because we can use the reactor here.

vim Main.elm 

We’ll start out just getting a basic app structure in place. I’ll just paste it in and chat through it so we can get on to the phoenix bits more quickly:

module Main exposing (..)  import Html.App as App import Html exposing (..) import Html.Attributes exposing (value, placeholder, class) import Html.Events exposing (onInput, onClick)   -- Our model will track a list of messages and the text for our new message to -- send.  We only support chatting in a single channel for now. type alias Model =     { newMessage : String     , messages : List String     }   -- We can either set our new message or join our channel type Msg     = SetNewMessage String     | JoinChannel   -- Basic initial model is straightforward initialModel : Model initialModel =     { newMessage = ""     , messages = []     }   -- We'll handle either setting the new message or joining the channel. update : Msg -> Model -> ( Model, Cmd Msg ) update msg model =     case msg of         SetNewMessage string ->             { model | newMessage = string } ! []          JoinChannel ->             model ! []   -- Our view will consist of a button to join the lobby, a list of messages, and -- our text input for crafting our message view : Model -> Html Msg view model =     div []         -- Clicking the button joins the lobby channel         [ button [ onClick JoinChannel ] [ text "Join lobby" ]         , div [ class "messages" ]             [ text "fake incoming message"             ]           -- On input, we'll SetNewMessage         , input [ placeholder "Message...", onInput SetNewMessage, value model.newMessage ] []         ]   -- Wire together the program main : Program Never main =     App.program         { init = init         , update = update         , view = view         , subscriptions = subscriptions         }   -- No subscriptions yet subscriptions : Model -> Sub Msg subscriptions model =     Sub.none   -- And here's our init function init : ( Model, Cmd Msg ) init =     ( initialModel, Cmd.none ) 

OK, so nothing here is new. Let’s fire up the elm-reactor and make sure that it’s all working.

elm-reactor # and visit http://localhost:8000 

Next, we’ll initialize the elm-phoenix-socket component:

-- Add the imports we need import Phoenix.Socket import Phoenix.Channel import Phoenix.Push  -- Modify our model to track the socket data type alias Model =     { newMessage : String     , messages : List String     , phxSocket : Phoenix.Socket.Socket Msg     }   -- We'll define initializing a phoenix socket in its own function initialModel : Model initialModel =     { newMessage = ""     , messages = []     , phxSocket = initPhxSocket     }   -- We need the URL for the websocket.  This will be the phoenix server url, then -- the route for the socket, then "websocket" because that's the transport we're -- communicating with. socketServer : String socketServer =     "ws://localhost:4000/socket/websocket"   -- initPhxSocket uses Phoenix.Socket.init on the socketServer, and we pipe it -- through Phoenix.Socket.withDebug so we can get debug information out of it. -- This will print every incoming Phoenix message to the console. initPhxSocket : Phoenix.Socket.Socket Msg initPhxSocket =     Phoenix.Socket.init socketServer         |> Phoenix.Socket.withDebug 

OK, so this is enough to get us initializing the socket server. Let’s run it and see if it connects.

((( test it, look in the phoenix server logs )))

Well, if it had connected, we’d see a notice in the logs here. Let’s keep moving on.

-- We'll add the code necessary to join the channel update : Msg -> Model -> ( Model, Cmd Msg ) update msg model =     case msg of         -- ...         JoinChannel ->             let                 channel =                     Phoenix.Channel.init "room:lobby"                  ( phxSocket, phxCmd ) =                     Phoenix.Socket.join channel model.phxSocket             in                 ( { model | phxSocket = phxSocket }                 , Cmd.map PhoenixMsg phxCmd                 )  -- We need to add the `PhoenixMsg` message type type Msg     = SetNewMessage String     | JoinChannel     | PhoenixMsg (Phoenix.Socket.Msg Msg)  -- We have to handle the `PhoenixMsg` in our update function: update : Msg -> Model -> ( Model, Cmd Msg ) update msg model =     case msg of         -- ...         PhoenixMsg msg ->             let                 ( phxSocket, phxCmd ) =                     Phoenix.Socket.update msg model.phxSocket             in                 ( { model | phxSocket = phxSocket }                 , Cmd.map PhoenixMsg phxCmd                 )  -- And we also need to include the subscriptions for Phoenix: subscriptions : Model -> Sub Msg subscriptions model =     Phoenix.Socket.listen model.phxSocket PhoenixMsg 

Alright, let’s check it out again and see if this is sufficient to connect.

((( split with the terminal on the left and the browser on the right )))

Refresh the page but don’t bother clicking the "Join Lobby" button yet. That’s got us working. So I guess you have to have wired up the subscriptions in order to successfully connect.

Now if you click the join button, we should see our socket connect to that channel…and it works.

Let’s open up another tab to see the non-elm interface in the phoenix chat and see if we can see our user joining:

((( open up http://localhost:4000 , fill in a username )))

Now if we join, we can see in the phoenix interface that our user’s in the channel. So we’ve got this…working…but we probably want to see messages and be able to send them. Let’s get that done next.

We’ll start out by sending messages. To do that, we’ll introduce a SendMessage Msg and wire it in:

-- We'll wrap our input in a form so we can just use the form's onSubmit for our -- message. view : Model -> Html Msg view model =     div []         [ button [ onClick JoinChannel ] [ text "Join lobby" ]         , div [ class "messages" ]             [ text "fake incoming message"             ]         , form [ onSubmit SendMessage ]             [ input [ placeholder "Message...", onInput SetNewMessage, value model.newMessage ] [] ]         ]   -- Which means we need to expose onSubmit import Html.Events exposing (onInput, onClick, onSubmit)   -- We'll add a SendMessage Msg type Msg     = SetNewMessage String     | JoinChannel     | PhoenixMsg (Phoenix.Socket.Msg Msg)     | SendMessage   -- Sending a message needs to encode it first, so we pull in Json.Encode and -- alias it so we don't have to type so much import Json.Encode as JE   -- We add a case branch for SendMessage update : Msg -> Model -> ( Model, Cmd Msg ) update msg model =     case msg of         -- ...         SendMessage ->             let                 -- We'll build our message out as a json encoded object                 payload =                     (JE.object [ ( "body", JE.string model.newMessage ) ])                  -- We prepare to push the message                 push' =                     Phoenix.Push.init "new:msg" "room:lobby"                         |> Phoenix.Push.withPayload payload                  -- We update our `phxSocket` and `phxCmd` by passing this push                 -- into the Phoenix.Socket.push function                 ( phxSocket, phxCmd ) =                     Phoenix.Socket.push push' model.phxSocket             in                 -- And we clear out the `newMessage` field, update our model's                 -- socket, and return our Phoenix command                 ( { model                     | newMessage = ""                     , phxSocket = phxSocket                   }                 , Cmd.map PhoenixMsg phxCmd                 ) 

That’s sufficient to send a message. Let’s go ahead and check it out, verifying that we receive the message on the phoenix side.

((( do that )))

OK, so all we have left to do is add the ability to receive messages. To do this, we’ll add a new Msg and handle it in our update:

-- When the message comes in we'll have to decode it -- We'll also expose := to use in our decoder import Json.Decode as JD exposing ((:=))   type Msg =     -- ...     | ReceiveChatMessage JE.Value   -- We want to introduce a ChatMessage type. it has a user and a body type alias ChatMessage =     { user : String     , body : String     }   -- We'll modify our Model to contain a list of these rather than strings type alias Model =     { newMessage : String     , messages : List ChatMessage     , phxSocket : Phoenix.Socket.Socket Msg     }   -- In our update, we'll gather the chat messages after decoding them. update : Msg -> Model -> ( Model, Cmd Msg ) update msg model =     case msg of         -- ...         ReceiveChatMessage raw ->             case JD.decodeValue chatMessageDecoder raw of                 Ok chatMessage ->                     ( { model | messages = chatMessage :: model.messages }                     , Cmd.none                     )                  Err error ->                     ( model, Cmd.none )   -- Our decoder will just decode a 2-field json object chatMessageDecoder : JD.Decoder ChatMessage chatMessageDecoder =     JD.object2 ChatMessage         ("user" := JD.string)         ("body" := JD.string)   -- Finally, we need to tell our Phoenix integration that we want to emit this -- Msg when we receive a "new:msg" on the channel initPhxSocket : Phoenix.Socket.Socket Msg initPhxSocket =     Phoenix.Socket.init socketServer         |> Phoenix.Socket.withDebug         |> Phoenix.Socket.on "new:msg" "room:lobby" ReceiveChatMessage  -- With that, we should be receiving the messages and placing them in our model. -- Now we just need to show them in the view:  -- We'll define a function that turns a ChatMessage into Html viewMessage : ChatMessage -> Html Msg viewMessage message =     div [ class "message" ]         [ span [ class "user" ] [ text (message.user ++ ": ") ]         , span [ class "body" ] [ text message.body ]         ]   -- And we'll use that view in place of our fake messages view : Model -> Html Msg view model =     div []         [ button [ onClick JoinChannel ] [ text "Join lobby" ]         , div [ class "messages" ]             (List.map viewMessage model.messages)         , form [ onSubmit SendMessage ]             [ input [ placeholder "Message...", onInput SetNewMessage, value model.newMessage ] [] ]         ] 

If we refresh and type something…we don’t see any messages on our side. Let’s go to the phoenix side and send a message.

OK, we see messages from the Phoenix side. Why is this? The use of Phoenix.Socket.withDebug is helpful here. We’ll open up the developer console and we can see the messages. We can see that the message from the Phoenix app has a user:

Phoenix message: { event = "new:msg", topic = "room:lobby", payload = { user = "knewter", body = "heya" }, ref = Nothing } 

However, our message does not:

Phoenix message: { event = "phx_reply", topic = "room:lobby", payload = { status = "ok", response = { msg = "wat" } }, ref = Just 2 } 

This is because our Phoenix application expects us to initiate the socket with an object that tells it what our user is. We can’t easily do that for now, so we’ll modify our chatMessageDecoder to default to an "anonymous" user if the user field is missing, rather than have it reject the message:

chatMessageDecoder : JD.Decoder ChatMessage chatMessageDecoder =     JD.object2 ChatMessage         (JD.oneOf             [ ("user" := JD.string)             , JD.succeed "anonymous"             ]         )         ("body" := JD.string) 

If we try it now, we can see our messages as well.

Summary

So that’s it! In today’s episode, we saw how to build a chat system that talks to Phoenix from scratch, using elm-phoenix-socket . We only support a single channel for now, but it’s just a matter of extending the model a bit to be able to easily track messages across multiple channels. Go out and build awesome stuff with Elm and Phoenix Channels. See you soon!

Resources

  • elm-phoenix-socket
  • presence_chat – Tagged with before_episode_011.2
    • NOTE: There is a breaking change coming to phoenix.js based on a suggestion I provided, so while this tag should continue working because it’s referencing an RC, know that the presence-related JS in this project will have to change after the next RC of Phoenix 1.2.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Integrating Elm and Phoenix Channels via Elm-Phoenix-socket

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址