I started exploring Phoenix for one thing only: Channels (or actually fast real time communication over websockets). In this post I explore how to use them (yes this is a follow up of My first Phoenix-app-post).
Websockets don’t pass session cookies. Because we don’t have access to these we need to transfer the user’s identity in a different way. One of the recommendations I found was passing a user_token
using a <meta>
-tag (adjusting templates/layout/app.html.eex
):
<%= if Coherence.current_user(@conn) do %>
<%= tag :meta, name: "user_token", content: Phoenix.Token.sign(@conn, "user", Coherence.current_user(@conn).id) %>
<% end %>
We can access this with a simple query selector in javascript:
document.querySelector("meta[name=user_token]").content
But that’s for later. Let’s move to the server side, since we need something to connect to, a Socket.
In our default project there is already a user_socket.ex
in the web/channels
folder. Open it and uncomment the following line:
channel "room:*", MyApp.RoomChannel
Then rewrite the connect function to accept a token as a parameter:
def connect(%{"token" => token}, socket) do
case Phoenix.Token.verify(socket, "user", token, max_age: 1209600) do
{:ok, user_id} ->
socket = assign(socket, :user, MyApp.Repo.get!(MyApp.User, user_id))
{:ok, socket}
{:error, _} ->
:error
end
end
Phoenix.Token.verify does all the magic, and turns a given token into a user_id
, allowing us to access the user through Repo.get!
(namespaced by the application name here).
Note As said, I really found this something to get used to: Repo.get!(User, id)
, instead of User.find(id)
Above code is also mentioning a RoomChannel, which we will create next:
defmodule MyApp.RoomChannel do
use Phoenix.Channel
# intercept ["new_post"]
def join("room:" <> _private_room_id, _params, socket) do
{:ok, socket}
end
def handle_in("new_post", %{"message" => message}, socket) do
user = Coherence.current_user(socket)
changeset = MyApp.Post.changeset(%MyApp.Post{user: user}, %{message: message)
{status, post} = MyApp.Repo.insert(changeset)
if status == :ok do
broadcast! socket, "new_post", %{message: post.message, id: post.id, inserted_at: post.inserted_at}
end
{:noreply, socket}
end
# def handle_out("new_post", payload, socket) do
# user = socket.assigns[:user]
# if MyApp.User.mentioned?(payload.message, user) do
# push socket, "new_post", payload
# end
# {:noreply, socket}
# end
end
I’ve commented out the handle_out/3
code here (note /3
depicts that this function has 3 arguments.
When a message enters the room channel the user is retrieved from the socket, a changeset is prepared (like in our controller) and if ok a new message is broadcast in the room.
Note: This message will flow through handle_out
when it matches the intercept
, which allows us to do some checking. In this case the code calls the User.mentioned?/2
-function, which checks whether the logged in user-name is mentioned in the message and only pushes the message to the user when the user is mentioned.
Foundation comes with the necessary code. Simply uncomment the following line in app.js
:
import socket from "./socket"
And update the code to something along the lines of the following:
import {Socket} from "phoenix"
let socket = null
if (document.querySelector("meta[name=user_token]")) {
socket = new Socket("/socket", {
params: {
token:
document.querySelector("meta[name=user_token]").content
}
})
socket.connect()
// Now that you are connected, you can join channels with a topic:
let channel = socket.channel("room:1", {})
let message = document.querySelector("#post_message")
let messagesContainer = document.querySelector("#posts")
message.addEventListener("keypress", event => {
if(event.keyCode === 13){
channel.push("new_post", {message: message.value})
message.value = null
}
})
channel.on("new_post", post => {
let messageItem = document.createElement("li");
messageItem = post.message
messagesContainer.appendChild(messageItem)
})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
}
export default socket
I made sure I only connect to a socket when a user is actually logged in (when a token is mentioned in the header, this to prevent unneeded traffic).
The listener channel.on(“new_post”…)
displays any messages sent to the room tagged new_post
. The rest is straight forward JavaScript (ES6-style).
We’re relying here on some more dom code, but make sure the page you’re displaying the posts on contains an input field with id post_message
. And a container to be filled with posts with id posts
(in this case a <ol>
), but I’ll leave the very HTML up to you.
After two days of experimenting I’m confident building an actual application in Phoenix / Elixir. While the programming style is something to get used to, its response is fast. There are quite some things I really like about the framework, including the schema definition inclusion and valid parameter parsing in the model. I recommend you to try Phoenix yourself!
Enjoyed this? Follow me on Mastodon or add the RSS, euh ATOM feed to your feed reader.
Dit artikel van murblog van Maarten Brouwers (murb) is in licentie gegeven volgens een Creative Commons Naamsvermelding 3.0 Nederland licentie .