En esta sección verás cómo crear un cliente que use la API WebSocket del navegador para conectarse, enviar y recibir mensajes. Luego construiremos una versión con interfaz mínima, reconexión y mensajes en JSON.
Pruébalo con un servidor en ws://localhost:8080 (el de tu punto anterior o cualquiera que tengas).
<!doctype html>
<html lang="es">
<body>
<script>
const ws = new WebSocket("ws://localhost:8080");
ws.onopen = () => {
console.log("Conexión abierta");
ws.send("Hola servidor desde el navegador!");
};
ws.onmessage = (e) => console.log("Servidor:", e.data);
ws.onclose = () => console.log("Conexión cerrada");
ws.onerror = (err) => console.error("Error WS:", err);
</script>
</body>
</html>
Qué incluye:
room).{type, payload}).Guárdalo como cliente.html y ábrelo en el navegador. Asegúrate de que tu servidor acepte los tipos join, message, typing (como el que ya hicimos).
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"/>
<title>WS Cliente Básico</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 720px; margin: 2rem auto; }
.fila { display: flex; gap: .5rem; margin-bottom: .5rem; }
input, button { padding: .5rem .7rem; }
#estado { font-size: .9rem; margin-bottom: .5rem; }
#mensajes { border: 1px solid #ddd; padding: .75rem; min-height: 240px; border-radius: .5rem; }
.sistema { color:#666; font-style: italic; }
.yo { color:#0a7; }
.otro { color:#06c; }
</style>
</head>
<body>
<h1>Cliente WebSocket</h1>
<div id="estado">🔴 Desconectado</div>
<div class="fila">
<input id="user" placeholder="usuario" value="ana">
<input id="room" placeholder="sala" value="general">
<button id="btn-join">Unirme</button>
</div>
<div id="mensajes" aria-live="polite"></div>
<div class="fila">
<input id="texto" placeholder="Escribe un mensaje y presiona Enter" style="flex:1">
<button id="btn-send">Enviar</button>
</div>
<div id="typing" class="sistema"></div>
<script>
let ws;
let conectado = false;
let usuarioActual = "";
let salaActual = "";
let backoff = 500; // reconexión exponencial (ms), tope suave 5s
let typingTimer;
let puedeAvisarTyping = true;
const $estado = document.getElementById("estado");
const $mensajes = document.getElementById("mensajes");
const $texto = document.getElementById("texto");
const $typing = document.getElementById("typing");
function setEstado(ok, detalle="") {
conectado = ok;
$estado.textContent = (ok ? "🟢 Conectado" : "🔴 Desconectado") + (detalle ? " " + detalle : "");
}
function addLinea(html, clase="") {
const p = document.createElement("p");
if (clase) p.className = clase;
p.innerHTML = html;
$mensajes.appendChild(p);
$mensajes.scrollTop = $mensajes.scrollHeight;
}
function safeSend(obj) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(obj));
} else {
addLinea("No se puede enviar: socket no abierto", "sistema");
}
}
function conectar() {
ws = new WebSocket("ws://localhost:8080");
ws.onopen = () => {
setEstado(true, "socket abierto");
backoff = 500;
addLinea("Conexión establecida", "sistema");
// Si ya teníamos usuario/sala, reingresar
if (usuarioActual && salaActual) {
safeSend({ type: "join", payload: { user: usuarioActual, room: salaActual } });
}
};
ws.onmessage = (e) => {
let msg;
try { msg = JSON.parse(e.data); }
catch { return addLinea("Servidor (texto): " + e.data, "otro"); }
switch (msg.type) {
case "welcome":
addLinea("Bienvenido: id=" + (msg.payload?.id ?? "?"), "sistema");
break;
case "ack":
// ACK de join/message u otras operaciones
// addLinea("ACK: " + JSON.stringify(msg.payload), "sistema");
break;
case "system":
addLinea(`[SISTEMA] ${msg.payload?.text ?? ""}`, "sistema");
break;
case "message": {
const { user, text } = msg.payload || {};
if (user === usuarioActual) addLinea(`Tú: ${text}`, "yo");
else addLinea(`${user ?? "?"}: ${text}`, "otro");
break;
}
case "typing":
if (msg.payload?.user && msg.payload.user !== usuarioActual) {
$typing.textContent = `${msg.payload.user} está escribiendo`;
clearTimeout(typingTimer);
typingTimer = setTimeout(() => $typing.textContent = "", 1000);
}
break;
case "error":
addLinea("ERROR: " + (msg.payload?.code ?? "desconocido"), "sistema");
break;
default:
// Mensajes personalizados de tu servidor
// addLinea("Evento " + msg.type + ": " + JSON.stringify(msg.payload));
break;
}
};
ws.onclose = () => {
setEstado(false, "cerrado");
addLinea("Conexión cerrada. Reintentando", "sistema");
setTimeout(conectar, backoff);
backoff = Math.min(backoff * 2, 5000);
};
ws.onerror = (err) => {
console.error("WS error:", err);
};
}
conectar();
// UI
document.getElementById("btn-join").onclick = () => {
const user = document.getElementById("user").value.trim();
const room = document.getElementById("room").value.trim();
if (!user || !room) return addLinea("Completá usuario y sala", "sistema");
usuarioActual = user;
salaActual = room;
safeSend({ type: "join", payload: { user, room } });
};
document.getElementById("btn-send").onclick = enviar;
$texto.addEventListener("keydown", (ev) => {
if (ev.key === "Enter") enviar();
});
$texto.addEventListener("input", () => {
if (!puedeAvisarTyping) return;
puedeAvisarTyping = false;
setTimeout(() => puedeAvisarTyping = true, 500); // throttle 500ms
safeSend({ type: "typing", payload: {} });
});
function enviar() {
const t = $texto.value;
if (!t) return;
safeSend({ type: "message", payload: { text: t } });
$texto.value = "";
}
</script>
</body>
</html>
{type, payload}.join/message/typing (muy típico en chats).open, message, close, error.JSON.parse dentro de try/catch. Tu servidor puede enviar texto o JSON.safeSend: evita InvalidStateError si el socket no está OPEN.wss:// (TLS). Cambia la URL según tu dominio/puerto.{type:"ping"} cada X segundos; el servidor responde {type:"pong"}).type/payload) te permite crecer ordenado.