Pourquoi WebSockets avec Phoenix ?
Les développeurs modernes ont besoin de technologies réactives pour permettre une communication bidirectionnelle entre le client et le serveur en temps réel. Cela est particulièrement important dans les applications modernes où la performance, la latence et l'expérience utilisateur sont des priorités.
Un cas d'utilisation concret serait un chat en direct, où chaque message envoyé par un utilisateur doit être instantanément affiché à tous les autres utilisateurs connectés. Avec WebSockets, cette communication peut se faire avec une latence très faible, même pour des applications à grande échelle.
Prerequis
- Connaissance de base de Elixir et Phoenix
- Compréhension des bases d'HTTP et de JavaScript
- Installation de Erlang/OTP (version 23 ou plus récente)
- Installation de Node.js (pour les outils de développement frontend)
Concepts fondamentaux
WebSockets
WebSockets est une protocole de communication bidirectionnelle qui permet des connexions persistantes entre le client et le serveur. Différentement d'HTTP, qui est un protocole stateless avec des requêtes-réponses, WebSockets maintient une connexion ouverte tout au long du temps.
Schema mental :
Client <-> Server
phoenix
## Connexion WebSocket en Elixir
defmodule MyAppWeb.UserSocket do
use Phoenix.Socket
def connect(_params, socket) do
{:ok, assign(socket, :user_id, "user-123")}
end
end
Broadcasts et Channels
Les channels sont la couche d'abstraction entre les clients et le serveur. Ils permettent de diffuser des messages à un ensemble spécifique de clients.
Schema mental :
Client <-> Channel -> Server -> Client (diffusion)
phoenix
## Definition d'un channel en Elixir
defmodule MyAppWeb.RoomChannel do
use Phoenix.Channel
def join("room:lobby", _params, socket) do
{:ok, assign(socket, :user_id, "user-123")}
end
def handle_in("new_msg", %{"body" => body}, socket) do
broadcast(socket, "new_msg", %{body: body})
{:noreply, socket}
end
end
PubSub et Topic
La publication/abonnement (PubSub) est une architecture de diffusion qui permet aux messages d'être envoyés à des groupes spécifiques de clients. Les topics sont les groupes auxquels un client peut s'abonner.
Schema mental :
Client <-> Topic -> Server -> Client
phoenix
## Publication/abonnement en Elixir
defmodule MyAppWeb.PubSub do
use Phoenix.PubSub, otp_app: :my_app
end
## Abonnement d'un client
Phoenix.PubSub.subscribe(MyAppWeb.PubSub, "room:lobby")
## Publication d'un message
MyAppWeb.PubSub.broadcast("room:lobby", "new_msg", %{body: "Hello"})
Mise en pratique : projet fil rouge
Nous allons construire un simple chat en direct avec Phoenix. Le mini-projet comprendra une interface web et un serveur backend.
Étape 1 : Configuration du projet
Créez un nouveau projet Phoenix :
mix phx.new chat_app --no-ecto
cd chat_app
Installez les dépendances frontend avec Yarn :
yarn install
Étape 2 : Création des channels et controllers
Ajoutez un channel pour le chat :
## lib/chat_app_web/channels/chat_channel.ex
defmodule ChatAppWeb.ChatChannel do
use Phoenix.Channel
def join("chat:lobby", _params, socket) do
{:ok, assign(socket, :user_id, "user-123")}
end
def handle_in("new_msg", %{"body" => body}, socket) do
broadcast(socket, "new_msg", %{body: body})
{:noreply, socket}
end
end
Ajoutez un controller pour les messages :
## lib/chat_app_web/controllers/message_controller.ex
defmodule ChatAppWeb.MessageController do
use ChatAppWeb, :controller
def index(conn, _params) do
render(conn, "index.html")
end
end
Étape 3 : Configuration des routes
Ajoutez les routes pour le chat et les messages :
## lib/chat_app_web/router.ex
defmodule ChatAppWeb.Router do
use ChatAppWeb, :router
scope "/", ChatAppWeb do
pipe_through :browser
get "/", MessageController, :index
end
socket "/socket", ChatAppWeb.UserSocket do
channel "chat:lobby", ChatAppWeb.ChatChannel
end
end
Étape 4 : Création de la vue frontend
Créez une interface simple en HTML pour le chat :
<!-- lib/chat_app_web/templates/message/index.html.eex -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chat App</title>
<script src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
</head>
<body>
<h1>Chat Room</h1>
<div id="chat-messages">
<!-- Messages seront affichés ici -->
</div>
<input type="text" id="message-input" placeholder="Type a message...">
<button id="send-button">Send</button>
<script>
let socket = new Phoenix.Socket("/socket")
socket.connect()
let channel = socket.channel("chat:lobby", {})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
channel.on("new_msg", payload => {
let messageElement = document.createElement("div")
messageElement.textContent = payload.body
document.getElementById("chat-messages").appendChild(messageElement)
})
document.getElementById("send-button").addEventListener("click", () => {
let input = document.getElementById("message-input")
channel.push("new_msg", { body: input.value })
input.value = ""
})
document.getElementById("message-input").addEventListener("keypress", (e) => {
if (e.key === "Enter") {
e.preventDefault()
let input = document.getElementById("message-input")
channel.push("new_msg", { body: input.value })
input.value = ""
}
})
</script>
</body>
</html>
Étape 5 : Compilation des assets
Compilez les assets frontend :
yarn run deploy
Erreurs fréquentes et debugging
Erreur de connexion WebSocket
# Mauvais channel.join() .receive("ok", resp => { console.log("Joined successfully", resp) }) .receive("error", resp => { console.log("Unable to join", resp) }) # Correct channel.join() .receive("ok", resp => { console.log("Joined successfully", resp) }) .receive("error", resp => { console.error("Unable to join", resp) })Erreur de publication
# Mauvais MyAppWeb.PubSub.broadcast("room:lobby", "new_msg", %{body: "Hello"}) # Correct MyAppWeb.PubSub.broadcast!("room:lobby", "new_msg", %{body: "Hello"})Erreur de channel non trouvé
# Mauvais let channel = socket.channel("chat:lobby") # Correct let channel = socket.channel("chat:lobby", {})
Pour aller plus loin
- Authentification et sécurité : Ajouter une authentification JWT pour sécuriser les channels.
- Scalabilité : Utiliser Redis avec Phoenix.PubSub.RedisAdapter pour améliorer la performance de diffusion.
- Tests unitaires : Écrire des tests unitaires pour les channels.
Défi pratique
Implémentez une fonctionnalité qui permet aux utilisateurs d'envoyer des messages privés entre eux en utilisant des topics personnalisés.