WebSockets Integration
WebSockets keep a persistent connection open so your Aurelia components can react to server-side events instantly—no polling necessary. The Vite-powered Aurelia 2 starter already ships with everything you need to consume WebSockets from the browser; you only need a small client service and (optionally) a Node-based development server to broadcast messages while you iterate.
1. Optional: spin up a tiny WebSocket server
If you don’t already have a backend, you can scaffold a development server with the popular ws package:
npm install ws --save-dev// scripts/dev-websocket-server.mjs
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (socket) => {
console.log('Client connected');
socket.send(JSON.stringify({ type: 'server-ready', timestamp: Date.now() }));
socket.on('message', (data) => {
// echo message to everyone (simple chat)
wss.clients.forEach(client => {
if (client.readyState === 1 /* WebSocket.OPEN */) {
client.send(JSON.stringify({
type: 'message',
text: data.toString(),
timestamp: Date.now(),
}));
}
});
});
});
console.log('WebSocket server listening on ws://localhost:8080');Run it alongside npm run dev so both the Vite dev server and the WebSocket server stay active.
2. Configure an environment variable for the WebSocket URL
Keep the WebSocket endpoint in .env.local so you can point the frontend at different servers per environment:
# .env.local
VITE_WS_URL=ws://localhost:8080Vite exposes variables prefixed with VITE_ via import.meta.env, letting you inject the URL without hard-coding it in source control.
3. Create a WebSocket client service
// src/services/websocket-client.ts
import { DI, Registration } from '@aurelia/kernel';
import { IDisposable } from '@aurelia/kernel';
export interface ChatMessage {
type: string;
text?: string;
timestamp: number;
}
export const IWebSocketClient = DI.createInterface<IWebSocketClient>('IWebSocketClient');
export interface IWebSocketClient extends IDisposable {
connect(url: string, listener: (msg: ChatMessage) => void): void;
send(payload: Record<string, unknown>): void;
readyState(): number;
}
export class WebSocketClient implements IWebSocketClient {
private socket: WebSocket | null = null;
private listener: ((msg: ChatMessage) => void) | null = null;
connect(url: string, listener: (msg: ChatMessage) => void): void {
this.dispose();
this.listener = listener;
const socket = new WebSocket(url);
socket.onopen = () => console.info('WebSocket connected');
socket.onclose = (event) => {
console.info('WebSocket closed', event.reason);
this.socket = null;
};
socket.onerror = (err) => console.error('WebSocket error', err);
socket.onmessage = (event: MessageEvent<string>) => {
try {
const data = JSON.parse(event.data) as ChatMessage;
this.listener?.(data);
} catch (error) {
console.warn('Unparseable message', error, event.data);
}
};
this.socket = socket;
}
send(payload: Record<string, unknown>): void {
if (this.socket?.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(payload));
} else {
console.warn('WebSocket not connected yet');
}
}
readyState(): number {
return this.socket?.readyState ?? WebSocket.CLOSED;
}
dispose(): void {
this.socket?.close();
this.socket = null;
this.listener = null;
}
}
export const WebSocketClientRegistration = Registration.singleton(IWebSocketClient, WebSocketClient);This service encapsulates connection lifecycle, JSON parsing, and basic logging. Because it’s registered as a singleton, every component that resolves IWebSocketClient shares the same connection.
4. Register the service when bootstrapping Aurelia
// src/main.ts
import { Aurelia, StandardConfiguration } from '@aurelia/runtime-html';
import { WebSocketClientRegistration } from './services/websocket-client';
import { MyApp } from './my-app';
await new Aurelia()
.register(StandardConfiguration, WebSocketClientRegistration)
.app({ host: document.querySelector('my-app')!, component: MyApp })
.start();5. Build a chat component on top of the service
// src/components/chat.ts
import { customElement } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';
import { IWebSocketClient, ChatMessage } from '../services/websocket-client';
@customElement({
name: 'chat',
template: `
<div class="chat">
<ul>
<li repeat.for="message of messages">
<strong>${message.type}:</strong> ${message.text}
<span class="timestamp">\${new Date(message.timestamp).toLocaleTimeString()}</span>
</li>
</ul>
<form submit.trigger="send()">
<input value.bind="draft" autocomplete="off" placeholder="Type a message…" />
<button type="submit" disabled.bind="!draft.trim()">Send</button>
</form>
</div>
`,
})
export class Chat {
draft = '';
messages: ChatMessage[] = [];
private readonly ws = resolve(IWebSocketClient);
private readonly url = import.meta.env.VITE_WS_URL as string;
binding() {
this.ws.connect(this.url, (message) => {
this.messages = [...this.messages, message];
});
}
detaching() {
this.ws.dispose(); // or keep alive if other components rely on it
}
send() {
if (!this.draft.trim()) return;
this.ws.send({ type: 'message', text: this.draft.trim(), timestamp: Date.now() });
this.draft = '';
}
}The single chat element now reflects server updates in real time and can safely send messages through the shared connection.
6. Tips & extensions
Reconnect logic: wrap the
WebSocketinstantiation in a helper that backs off and retries oncloseevents whenevent.wasCleanisfalse.Binary data: switch to
socket.binaryType = 'arraybuffer'and send typed arrays when transferring files or images.Authentication: include JWTs or session tokens in the initial connection URL (e.g.,
ws://host?token=...) or upgrade request headers depending on your backend.State integration: dispatch incoming messages into
@aurelia/stateor another store so multiple components can react without each subscribing to the socket.
With the connection wrapped in a DI-friendly service and the endpoint stored in environment variables, swapping between local, staging, and production WebSocket servers becomes trivial—and you avoid the legacy webpack-specific loader configuration entirely.
Last updated
Was this helpful?