Un cliente Vue MQTT permite que tu app Vue 3 muestre los datos de los dispositivos en el momento en que se publican, sin hacer polling a una API. En este tutorial escribirás un composable useMqtt reutilizable sobre MQTT.js, lo conectarás a un broker mediante WebSocket seguro y construirás un widget de sensor en vivo con un botón de comando. Por el camino evitarás los errores de reactividad y de ciclo de vida que hacen que muchas apps Vue con MQTT sean lentas o pierdan recursos.
Cómo se conecta Vue a MQTT
Vue se ejecuta en el navegador, y los navegadores no pueden abrir sockets TCP, así que MQTT viaja dentro de un WebSocket. MQTT.js lo gestiona de forma transparente cuando le das una URL wss://. Los brokers de ConnectMQ exponen WebSocket seguro en el puerto 8084 con la ruta /mqtt:
wss://broker.connectmq.com:8084/mqtt
El servidor MQTT público gratuito broker.connectmq.com acepta conexiones anónimas con cualquier client ID único y no vacío, lo que lo hace ideal para desarrollo. Su área pública la puede leer cualquiera, así que usa un prefijo de topic aleatorio y solo datos de prueba, nunca datos de producción. Para entender el transporte, consulta MQTT sobre WebSocket.
Configuración del proyecto
npm create vue@latest vue-mqtt-demo
cd vue-mqtt-demo
npm install
npm install mqtt
npm run dev
Acepta los valores por defecto en las preguntas del asistente; TypeScript, Pinia y el router son opcionales para este tutorial. MQTT.js v5 incluye una versión para navegador que Vite resuelve automáticamente.
Escribir el composable useMqtt
Crea src/composables/useMqtt.js:
import { ref, reactive, readonly, onMounted, onBeforeUnmount } from "vue";
import mqtt from "mqtt";
export function useMqtt(url, options = {}, topics = []) {
const status = ref("idle");
const error = ref(null);
// último mensaje por topic: { [topic]: { value, receivedAt } }
const messages = reactive({});
// variable normal, NO reactiva: Vue nunca debe envolver en un proxy el cliente MQTT
let client = null;
function parse(payload) {
const text = payload.toString();
try {
return JSON.parse(text);
} catch {
return text;
}
}
function connect() {
status.value = "connecting";
client = mqtt.connect(url, {
protocolVersion: 5,
reconnectPeriod: 3000,
connectTimeout: 10000,
...options,
});
client.on("connect", () => {
status.value = "connected";
error.value = null;
if (topics.length) client.subscribe(topics, { qos: 0 });
});
client.on("reconnect", () => (status.value = "reconnecting"));
client.on("offline", () => (status.value = "offline"));
client.on("error", (err) => (error.value = err.message));
client.on("message", (topic, payload) => {
messages[topic] = { value: parse(payload), receivedAt: Date.now() };
});
}
function publish(topic, message, publishOptions = { qos: 0 }) {
if (!client || !client.connected) return false;
const body = typeof message === "string" ? message : JSON.stringify(message);
client.publish(topic, body, publishOptions);
return true;
}
function disconnect() {
if (client) {
client.end(true);
client = null;
}
status.value = "disconnected";
}
onMounted(connect);
onBeforeUnmount(disconnect);
return {
status: readonly(status),
error: readonly(error),
messages: readonly(messages),
publish,
disconnect,
};
}
Decisiones de diseño explicadas
| Decisión | Por qué |
|---|---|
| Cliente guardado en una variable normal | Envolver el cliente MQTT en ref o reactive hace que Vue cree proxies de sus internos, lo que cuesta rendimiento y puede romper la gestión de eventos. Si tienes que guardarlo en el estado, usa shallowRef o markRaw. |
| Último valor por topic | Un array creciente con todos los mensajes provoca fugas de memoria en dashboards de larga duración. |
Suscripción en el handler connect |
Las suscripciones se restauran automáticamente tras las reconexiones. |
Limpieza en onBeforeUnmount |
Al navegar a otra vista se cierra el socket en lugar de dejar una conexión fantasma que cuenta para tu límite. |
Valores devueltos con readonly |
Los componentes no pueden modificar por accidente el estado compartido de la conexión. |
Vue agrupa las actualizaciones del DOM por tick, así que escribir en messages con cada mensaje entrante está bien para unos pocos mensajes por segundo. Si un dispositivo publica mucho más rápido, almacena los mensajes en un objeto normal y cópialos a messages con un temporizador, como se describe en el artículo del hook de React para MQTT.
Un widget de sensor en vivo
Crea src/components/SensorWidget.vue:
<script setup>
import { computed } from "vue";
import { useMqtt } from "../composables/useMqtt";
const props = defineProps({
prefix: { type: String, required: true },
device: { type: String, required: true },
});
const clientId = `vue-${Math.random().toString(16).slice(2, 10)}`;
const telemetryTopic = `${props.prefix}/${props.device}/telemetry`;
const { status, messages, publish } = useMqtt(
"wss://broker.connectmq.com:8084/mqtt",
{ clientId },
[telemetryTopic]
);
const reading = computed(() => messages[telemetryTopic]?.value ?? {});
const updatedAt = computed(() => {
const ts = messages[telemetryTopic]?.receivedAt;
return ts ? new Date(ts).toLocaleTimeString() : "never";
});
function toggleFan() {
publish(`${props.prefix}/${props.device}/cmd`, { action: "toggle-fan" }, { qos: 1 });
}
</script>
<template>
<div class="widget">
<header>
<h3>{{ device }}</h3>
<span :class="['status', status]">{{ status }}</span>
</header>
<p class="big">{{ reading.temperature ?? "--" }} °C</p>
<p>Humidity: {{ reading.humidity ?? "--" }} %</p>
<small>Updated: {{ updatedAt }}</small>
<button :disabled="status !== 'connected'" @click="toggleFan">Toggle fan</button>
</div>
</template>
<style scoped>
.widget { border: 1px solid #ccc; border-radius: 8px; padding: 1rem; max-width: 280px; }
.big { font-size: 2rem; margin: 0.5rem 0; }
.status.connected { color: green; }
.status.reconnecting, .status.offline { color: orange; }
</style>
Úsalo en src/App.vue:
<script setup>
import SensorWidget from "./components/SensorWidget.vue";
const prefix = "connectmq/demo/vue-c42e";
</script>
<template>
<main>
<h1>Vue MQTT Demo</h1>
<SensorWidget :prefix="prefix" device="greenhouse1" />
</main>
</template>
El client ID se crea una vez en <script setup>, que se ejecuta una sola vez por instancia del componente, así que se mantiene estable entre renders.
Envía datos de prueba
Publica lecturas en el servidor público desde un terminal:
for i in $(seq 1 30); do
mosquitto_pub -h broker.connectmq.com -p 1883 -i vue-pub-c42e \
-t connectmq/demo/vue-c42e/greenhouse1/telemetry \
-m "{\"temperature\": $((18 + RANDOM % 8)), \"humidity\": $((55 + RANDOM % 15))}"
sleep 1
done
El widget se actualiza cada segundo. Pulsa el botón y observa cómo llegan los comandos con mosquitto_sub -h broker.connectmq.com -p 1883 -i vue-sub-c42e -v -t "connectmq/demo/vue-c42e/+/cmd".
Una conexión para toda la app
El composable anterior abre una conexión por cada componente que lo usa. Está bien para un solo widget, pero un dashboard con veinte widgets abriría veinte conexiones. Dos patrones mejores:
- Singleton a nivel de módulo. Saca el cliente y
messagesfuera de la función para que todas las llamadas los compartan, y cuenta los usuarios para saber cuándo desconectar. Sencillo y sin dependencias. - Store de Pinia. Crea el cliente en una acción del store, guarda
statusymessagescomo estado del store y deja que los componentes lean de él. Encaja bien con Vue Devtools y con las protecciones para el renderizado en servidor.
Esta es una versión compacta del enfoque singleton, guardada como src/composables/useSharedMqtt.js:
import { ref, reactive, readonly, onBeforeUnmount } from "vue";
import mqtt from "mqtt";
const status = ref("idle");
const messages = reactive({});
const topicUsers = new Map();
let client = null;
let users = 0;
function ensureClient(url, options) {
if (client) return;
client = mqtt.connect(url, { protocolVersion: 5, reconnectPeriod: 3000, ...options });
client.on("connect", () => {
status.value = "connected";
const filters = [...topicUsers.keys()];
if (filters.length) client.subscribe(filters);
});
client.on("reconnect", () => (status.value = "reconnecting"));
client.on("offline", () => (status.value = "offline"));
client.on("message", (topic, payload) => {
let value = payload.toString();
try { value = JSON.parse(value); } catch { /* conservar el texto */ }
messages[topic] = { value, receivedAt: Date.now() };
});
}
export function useSharedMqtt(url, options, topicFilter) {
ensureClient(url, options);
users++;
const count = topicUsers.get(topicFilter) ?? 0;
topicUsers.set(topicFilter, count + 1);
if (count === 0 && client.connected) client.subscribe(topicFilter);
onBeforeUnmount(() => {
const left = topicUsers.get(topicFilter) - 1;
if (left === 0) {
topicUsers.delete(topicFilter);
client?.unsubscribe(topicFilter);
} else {
topicUsers.set(topicFilter, left);
}
if (--users === 0) {
client.end(true);
client = null;
status.value = "disconnected";
}
});
return { status: readonly(status), messages: readonly(messages) };
}
Cada widget llama a useSharedMqtt con su propio filtro de topic. El primero que llama abre la conexión, cada topic se suscribe una sola vez sin importar cuántos widgets lo muestren, y el último widget en desmontarse cierra el socket. Como messages es un único objeto reactivo indexado por topic, las propiedades computed de un widget solo se reevalúan cuando cambia la clave concreta que leen, lo que mantiene ágiles los dashboards grandes.
En ambos casos, cada pestaña del navegador acaba con una única conexión MQTT, lo que te mantiene holgadamente dentro de los límites de conexiones y hace más fácil de entender la vista de clientes del broker.
Consejos de rendimiento para Vue con MQTT
Unos pocos hábitos mantienen fluido un dashboard Vue MQTT a medida que crece de un widget a decenas:
- Guarda el último valor y deriva el resto. Mantén los mensajes en bruto en el composable y calcula los valores de presentación, como números redondeados, unidades y colores, con
computed. Las propiedades computed se cachean, así que solo se vuelven a ejecutar cuando cambia el mensaje del que dependen. - Limita el histórico de los gráficos. Si un widget mantiene una serie móvil para un sparkline, añade los puntos nuevos a un array de longitud fija y descarta los más antiguos. Los arrays sin límite son la causa más común de dashboards que se ralentizan tras unas horas.
- Usa retained messages para el primer pintado. Cuando los dispositivos publican su estado con el flag retain, un widget montado tras una navegación muestra el valor actual de inmediato en lugar de un marcador vacío.
- Evita los watchers profundos sobre
messages. Unwatch(messages, ..., { deep: true })se dispara con cada actualización de cada topic. Observa solo la clave que te interesa. - Muestra la antigüedad. Un valor que lleva minutos sin cambiar puede significar que el dispositivo está offline. Mostrar la hora de actualización, como hace el widget, evita que la gente confíe en números congelados.
Errores comunes de Vue con MQTT
- El cliente está envuelto en
ref(). Los síntomas incluyen un rendimiento lento o errores extraños dentro de MQTT.js. Guárdalo en una variable normal o enshallowRef. - Bucle de conexión y desconexión. Dos componentes o pestañas comparten un client ID fijo. Genera IDs únicos por conexión.
- Errores de contenido mixto. La página se sirve por HTTPS pero la URL usa
ws://. Cambia awss://en el puerto 8084. - Nada se actualiza tras un microcorte de red. Las suscripciones se hicieron fuera del handler
connectcon una sesión limpia. - Fallos en SSR. En Nuxt u otra configuración SSR, conéctate solo en el cliente, algo que
onMountedya garantiza.
Configuración para producción
Para una aplicación real, conéctate a un broker privado con credenciales. En ConnectMQ, crea un broker en el portal y pasa el usuario y la contraseña generados mediante variables de entorno de Vite:
const { status, messages } = useMqtt(
"wss://YOUR_BROKER_HOST:8084/mqtt",
{
clientId,
username: import.meta.env.VITE_MQTT_USERNAME,
password: import.meta.env.VITE_MQTT_PASSWORD,
},
["factory1/+/telemetry"]
);
Recuerda que todo lo que se incluye en el código del frontend lo pueden leer los usuarios. Usa una credencial dedicada para el dashboard y envía los comandos sensibles a través de una API de backend en lugar de hacerlo directamente desde el navegador. Un broker MQTT gestionado con un certificado válido de Let's Encrypt en el puerto WebSocket hace que los navegadores se conecten sin avisos ni configuración adicional.
Tutoriales relacionados
Si tu equipo usa también otros frameworks, compara este composable con el servicio MQTT de Angular construido sobre RxJS. Para añadir comprobaciones en tiempo de compilación de topics y payloads, consulta MQTT con tipos seguros en TypeScript. La guía de librerías cliente MQTT cubre opciones fuera de JavaScript para tus servicios de backend, y la documentación de composables de Vue explica en profundidad el patrón usado aquí.
Empieza con ConnectMQ
Dale a tu app Vue un broker privado con WebSocket seguro en pocos minutos. Regístrate en app.connectmq.com, elige el plan Serverless gratuito sin tarjeta de crédito, crea un broker y conecta las credenciales a useMqtt. El explorador de topics en vivo del portal de ConnectMQ muestra exactamente qué mensajes deberían estar recibiendo tus widgets.
Preguntas frecuentes
¿Qué librería MQTT debo usar con Vue?
MQTT.js es la opción estándar. Funciona en el navegador sobre WebSocket, admite MQTT 3.1.1 y 5.0 y se integra limpiamente en un composable como el de este artículo.
¿Funciona con Vue 2?
El composable usa la Composition API, que viene integrada en Vue 2.7. Para versiones anteriores de Vue 2, pon la misma lógica en los hooks mounted y beforeDestroy de un componente.
¿Por qué mi widget muestra "reconnecting" constantemente?
Normalmente otra conexión está usando el mismo client ID, un firewall bloquea el puerto 8084 o a la URL le falta la ruta /mqtt. Revisa la consola del navegador para ver el mensaje de error de MQTT.js.
¿Puedo publicar desde una app Vue?
Sí, con la función publish que devuelve el composable. Para acciones que afectan a equipos reales, es preferible pasarlas por un backend autenticado para que los usuarios del navegador no puedan publicar mensajes arbitrarios.



