Appearance
PartyKit avec Angular : Créez des applications temps réel collaboratives
Imaginez que vous développez une application de dessin collaboratif où plusieurs utilisateurs peuvent dessiner en même temps sur la même toile. Chaque trait dessiné par un utilisateur doit apparaître instantanément sur l'écran des autres utilisateurs. C'est exactement ce type d'interaction en temps réel que PartyKit permet de réaliser facilement !
Qu'est-ce que PartyKit ?
PartyKit est une plateforme qui simplifie considérablement le développement d'applications collaboratives en temps réel. Au lieu de gérer vous-même toute l'infrastructure complexe nécessaire pour les communications en temps réel, PartyKit s'occupe de tout :
Avantages clés
- Simplicité d'utilisation : Ajoutez des fonctionnalités temps réel avec quelques lignes de code
- Scalabilité automatique : Supporte des milliers d'utilisateurs grâce au réseau edge computing
- JavaScript natif : Écrivez votre code serveur en JavaScript (ou optionnellement en WebAssembly)
- Tout inclus : Outils de développement local, déploiements de preview, gestion des secrets...
Cas d'utilisation
PartyKit est particulièrement adapté pour :
- Les applications collaboratives (tableaux blancs, éditeurs de code, éditeurs de texte)
- Les jeux multijoueurs
- Les chats en temps réel
- Les curseurs collaboratifs
- Les bots IA persistants
- Les compteurs de réactions en direct
- Les applications de sondage en direct
Comment ça fonctionne ?
PartyKit utilise une architecture client-serveur où :
- Chaque "room" est une instance isolée qui gère son propre état
- Les clients se connectent via WebSocket pour une communication bidirectionnelle
- Le serveur peut persister les données et gérer la logique métier
- Tout est automatiquement distribué sur le réseau edge pour des performances optimales
Qui utilise PartyKit ?
De nombreuses applications populaires utilisent déjà PartyKit :
- BlockNote : Un éditeur de texte riche open source
- tldraw : Un tableau blanc collaboratif
- Stately : Des outils visuels pour la logique applicative
- SiteGPT : Pour alimenter des agents IA
Installation et configuration
Commençons par installer les dépendances nécessaires :
Installation
bash
npm install partysocket@latest
1. Création du service PartyKit
Le service PartyKit est responsable de :
- La gestion de la connexion WebSocket
- La synchronisation de l'état entre les clients
- L'envoi et la réception des messages
- La gestion des événements en temps réel
Structure du service
1. Gestion de l'état avec les signaux
Les signaux permettent une mise à jour réactive de l'interface utilisateur. L'état est en lecture seule pour les composants, seul le service peut le modifier.
2. Configuration de la connexion
ts
this.socket = new PartySocket({
host: 'votre-projet.username.partykit.dev', // ou localhost:1999 en développement
room: 'ma-salle-collaborative',
id: `user-${Math.random().toString(36).slice(2, 9)}`
});
host
: URL du serveur PartyKitroom
: Identifiant de la salle de collaboration (les clients dans la même room sont connectés)id
: Identifiant unique pour chaque connexion client
Les rooms
Les rooms sont des espaces isolés où :
- Chaque room a son propre état et ses propres participants
- Les messages sont diffusés uniquement aux clients de la même room
- Parfait pour séparer différentes sessions collaboratives
3. Gestion des événements
ts
this.socket.addEventListener('open', () => {
});
this.socket.addEventListener('close', () => {
});
this.socket.addEventListener('message', (event) => {
});
Les événements WebSocket permettent de :
- Détecter l'état de la connexion (
open
/close
) - Recevoir et traiter les messages (
message
) - Mettre à jour l'état local en fonction des messages reçus
4. Communication client-serveur
ts
sendMessage<T>(type: string, payload: T): void {
this.socket.send(JSON.stringify({
type,
payload
}));
}
La communication est structurée avec :
- Un type pour identifier l'action
- Un payload contenant les données
- Une sérialisation JSON pour la transmission
Points essentiels
Connexions :
- Une connexion unique par instance client
- Gestion automatique des reconnexions par PartyKit
Messages :
- Format JSON pour la transmission
- Structure type/payload pour le routage des messages
- Validation des données côté serveur
État :
- Centralisé dans le service
- Mis à jour via les signaux
- Propagé aux composants en lecture seule
Créons maintenant le service :
ts
import { Injectable, signal } from '@angular/core';
import PartySocket from 'partysocket';
import { User } from './user.interface';
@Injectable({
providedIn: 'root'
})
export class PartyKitService {
// Signal pour gérer l'état de la connexion
private connectionStatusSignal = signal<'connected' | 'disconnected'>('disconnected');
public connectionStatus = this.connectionStatusSignal.asReadonly();
// Signal pour la liste des utilisateurs connectés
private usersSignal = signal<User[]>([]);
public users = this.usersSignal.asReadonly();
private socket: PartySocket;
constructor() {
/**
* Initialisation de la connexion PartyKit
* - host: l'URL de votre serveur PartyKit
* - room: l'identifiant unique de la "salle" de collaboration
* - id: identifiant unique pour ce client (optionnel)
*/
this.socket = new PartySocket({
host: 'votre-projet.username.partykit.dev', // ou localhost:1999 en développement
room: 'ma-salle-collaborative'
});
// Gestion des événements de connexion
this.socket.addEventListener('open', () => {
this.connectionStatusSignal.set('connected');
});
this.socket.addEventListener('close', () => {
this.connectionStatusSignal.set('disconnected');
});
/**
* Écoute des messages entrants
* Chaque message est typé et contient des données spécifiques
*/
this.socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'users-update':
this.usersSignal.set(data.users);
break;
// Ajoutez d'autres cas selon vos besoins
}
});
}
/**
* Envoie un message au serveur PartyKit
* @param type - Le type de message
* @param payload - Les données à envoyer
*/
sendMessage<T>(type: string, payload: T): void {
this.socket.send(JSON.stringify({
type,
payload
}));
}
/**
* Met à jour les propriétés de connexion
* Utile pour changer de salle ou de serveur
*/
updateConnection(options: { room?: string; host?: string }) {
this.socket.updateProperties(options);
this.socket.reconnect();
}
}
2. Utilisation dans un composant
Voici comment utiliser le service PartyKit dans un composant Angular :
ts
import { Component, inject } from '@angular/core';
import { PartyKitService } from '../../core/services/partykit.service';
@Component({
selector: 'app-collaborative',
standalone: true,
template: `
@if (partyKitService.connectionStatus() === 'connected') {
<div class="status-connected">Connecté à la salle collaborative</div>
} @else {
<div class="status-disconnected">Déconnecté</div>
}
<div class="users-list">
@for (user of partyKitService.users(); track user.id) {
<div class="user-card">
<span class="user-name">{{ user.name }}</span>
<span class="user-status">En ligne</span>
</div>
}
</div>
<button (click)="sendUserAction()">
Envoyer une action
</button>
`
})
export class CollaborativeComponent {
partyKitService = inject(PartyKitService);
/**
* Exemple d'envoi d'une action utilisateur
* Peut être utilisé pour envoyer des mises à jour en temps réel
*/
sendUserAction() {
this.partyKitService.sendMessage('user-action', {
timestamp: Date.now(),
action: 'click'
});
}
}
Bonnes pratiques
- Gestion des reconnexions : PartyKit gère automatiquement les reconnexions en cas de perte de connexion, mais vous pouvez personnaliser ce comportement :
ts
const socket = new PartySocket({
host: 'votre-projet.partykit.dev',
room: 'ma-salle',
query: async () => ({
// Ajoutez des paramètres dynamiques pour l'authentification
token: await getAuthToken()
})
});
- Typage des messages : Créez des interfaces pour vos messages pour une meilleure maintenabilité :
ts
interface Message<T> {
type: string;
payload: T;
}
interface UserAction {
timestamp: number;
action: string;
}
Sécurité
- Ne stockez jamais d'informations sensibles dans les messages PartyKit
- Implémentez une authentification appropriée
- Validez toujours les données reçues avant de les utiliser
Cas d'utilisation avancés
1. Curseurs collaboratifs
Pour implémenter des curseurs collaboratifs où chaque utilisateur peut voir la position des autres en temps réel :
ts
import { Component, inject, signal } from '@angular/core';
interface CursorPosition {
x: number;
y: number;
userId: string;
username: string;
}
@Component({
template: `
<div
class="collaborative-area"
(mousemove)="onMouseMove($event)"
>
@for (cursor of cursors(); track cursor.userId) {
<div
class="cursor"
[style.left.px]="cursor.x"
[style.top.px]="cursor.y"
>
{{ cursor.username }}
</div>
}
</div>
`
})
export class CollaborativeCursorsComponent {
private partyKitService = inject(PartyKitService);
cursors = signal<CursorPosition[]>([]);
onMouseMove(event: MouseEvent) {
const position: CursorPosition = {
x: event.clientX,
y: event.clientY,
userId: 'mon-id',
username: 'Mon Nom'
};
this.partyKitService.sendMessage('cursor-move', position);
}
}
2. Chat en temps réel
Pour implémenter un chat en temps réel entre les utilisateurs :
ts
import { ReactiveFormsModule } from '@angular/forms';
import { Component, inject, signal } from '@angular/core';
import { FormControl } from '@angular/forms';
interface ChatMessage {
id: string;
userId: string;
username: string;
content: string;
timestamp: number;
}
@Component({
imports: [ReactiveFormsModule],
template: `
<div class="chat-container">
<div class="messages">
@for (message of messages(); track message.id) {
<div class="message" [class.own-message]="message.userId === currentUserId">
<strong>{{ message.username }}</strong>
<p>{{ message.content }}</p>
<small>{{ message.timestamp | date:'short' }}</small>
</div>
}
</div>
<form (submit)="sendMessage()">
<input
type="text"
[formControl]="messageInput"
placeholder="Votre message..."
>
<button type="submit">Envoyer</button>
</form>
</div>
`
})
export class ChatComponent {
private partyKitService = inject(PartyKitService);
messages = signal<ChatMessage[]>([]);
messageInput = new FormControl('');
sendMessage(event: Event) {
if (!this.messageInput.value) return;
const message: ChatMessage = {
id: crypto.randomUUID(),
userId: 'mon-id',
username: 'Mon Nom',
content: this.messageInput.value,
timestamp: Date.now()
};
this.partyKitService.sendMessage('chat-message', message);
this.messageInput.reset();
}
}
crypto.randomUUID()
ts
// Préférez crypto.randomUUID()
const id = crypto.randomUUID();
// Plutôt que Math.random()
const id = Math.random().toString(36).slice(2, 9);
crypto.randomUUID()
génère un UUID v4 cryptographiquement sûr- Garantit une meilleure unicité que
Math.random()
- Évite les collisions d'ID, cruciales dans un contexte temps réel
- Supporté par tous les navigateurs modernes
Code du serveur PartyKit [Cadeau]
ts
import type * as Party from "partykit/server";
interface Message<T> {
type: string;
payload: T;
}
interface User {
id: string;
name: string;
email: string;
}
interface CursorPosition {
x: number;
y: number;
userId: string;
username: string;
}
interface ChatMessage {
id: string;
userId: string;
username: string;
content: string;
timestamp: number;
}
export default class Server implements Party.Server {
constructor(readonly room: Party.Room) {}
// Liste des utilisateurs connectés
private users = new Map<string, User>();
// Positions des curseurs
private cursors = new Map<string, CursorPosition>();
// Messages du chat
private messages: ChatMessage[] = [];
// Initialisation du serveur
async onStart() {
// Charger les messages depuis le stockage
const storedMessages = await this.room.storage.get<ChatMessage[]>("messages");
if (storedMessages) {
this.messages = storedMessages;
}
}
// Nouvelle connexion
async onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) {
// Envoyer l'état actuel au nouveau client
conn.send(JSON.stringify({
type: "users-update",
payload: Array.from(this.users.values())
}));
conn.send(JSON.stringify({
type: "cursors-update",
payload: Array.from(this.cursors.values())
}));
conn.send(JSON.stringify({
type: "chat-history",
payload: this.messages
}));
}
// Réception d'un message
async onMessage(message: string, sender: Party.Connection) {
const data = JSON.parse(message) as Message<any>;
switch (data.type) {
case "user-update":
const user = data.payload as User;
this.users.set(sender.id, user);
this.room.broadcast(JSON.stringify({
type: "users-update",
payload: Array.from(this.users.values())
}), [sender.id]);
break;
case "cursor-move":
const position = data.payload as CursorPosition;
this.cursors.set(sender.id, position);
this.room.broadcast(JSON.stringify({
type: "cursors-update",
payload: Array.from(this.cursors.values())
}), [sender.id]);
break;
case "chat-message":
const chatMessage = data.payload as ChatMessage;
this.messages.push(chatMessage);
// Garder seulement les 100 derniers messages
if (this.messages.length > 100) {
this.messages = this.messages.slice(-100);
}
// Sauvegarder dans le stockage
await this.room.storage.put("messages", this.messages);
// Diffuser à tous les clients
this.room.broadcast(JSON.stringify({
type: "chat-message",
payload: chatMessage
}));
break;
}
}
// Déconnexion
async onClose(conn: Party.Connection) {
this.users.delete(conn.id);
this.cursors.delete(conn.id);
this.room.broadcast(JSON.stringify({
type: "users-update",
payload: Array.from(this.users.values())
}));
this.room.broadcast(JSON.stringify({
type: "cursors-update",
payload: Array.from(this.cursors.values())
}));
}
}
json
{
"name": "mon-projet-collaboratif",
"main": "server.ts",
"parties": {
"main": "server.ts"
}
}
Voir https://docs.partykit.io/quickstart pour plus d'informations sur la configuration du serveur PartyKit et le déploiement.