Um cliente de MQTT com Vue permite que seu app Vue 3 mostre dados de dispositivos no momento em que são publicados, sem fazer polling em uma API. Neste tutorial você vai escrever um composable useMqtt reutilizável em torno do MQTT.js, conectá-lo a um broker via WebSocket seguro e construir um widget de sensor ao vivo com um botão de comando. No caminho, você vai evitar os erros de reatividade e de ciclo de vida que deixam muitos apps de MQTT com Vue lentos ou com vazamentos.
Como o Vue se Conecta ao MQTT
O Vue roda no navegador, e navegadores não conseguem abrir sockets TCP, então o MQTT trafega dentro de um WebSocket. O MQTT.js cuida disso de forma transparente quando você passa uma URL wss://. Os brokers do ConnectMQ expõem WebSocket seguro na porta 8084 com o caminho /mqtt:
wss://broker.connectmq.com:8084/mqtt
O servidor MQTT público (gratuito) em broker.connectmq.com aceita conexões anônimas com qualquer client ID não vazio e único, o que o torna ideal para desenvolvimento. Ele pode ser lido por todos e serve apenas para testes, então use um prefixo de topic aleatório e apenas dados de teste. Para entender melhor o transporte, veja MQTT sobre WebSocket.
Configuração do Projeto
npm create vue@latest vue-mqtt-demo
cd vue-mqtt-demo
npm install
npm install mqtt
npm run dev
Aceite os padrões nas perguntas do scaffolding; TypeScript, Pinia e o router são opcionais para este tutorial. O MQTT.js v5 inclui um build para navegador que o Vite resolve automaticamente.
Escrevendo o Composable useMqtt
Crie 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);
// última mensagem por topic: { [topic]: { value, receivedAt } }
const messages = reactive({});
// variável simples, NÃO reativa: o Vue nunca deve criar um proxy do 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,
};
}
As Escolhas de Design Explicadas
| Escolha | Por quê |
|---|---|
| Cliente mantido em uma variável simples | Envolver o cliente MQTT em ref ou reactive faz o Vue criar proxies dos seus internos, o que custa desempenho e pode quebrar o tratamento de eventos. Se precisar guardá-lo no estado, use shallowRef ou markRaw. |
| Último valor por topic | Um array crescente com todas as mensagens vaza memória em dashboards de longa duração. |
Assinar no handler connect |
As assinaturas são restauradas automaticamente após as reconexões. |
Cleanup no onBeforeUnmount |
Sair da página fecha o socket, em vez de deixar uma conexão fantasma que conta para o seu limite. |
Valores de retorno readonly |
Os componentes não conseguem alterar por acidente o estado compartilhado da conexão. |
O Vue agrupa as atualizações do DOM por tick, então gravar em messages a cada mensagem recebida é aceitável para algumas mensagens por segundo. Se um dispositivo publica muito mais rápido, faça buffer das mensagens em um objeto simples e copie-as para messages com um timer, como descrito no artigo sobre o hook de MQTT com React.
Um Widget de Sensor ao Vivo
Crie 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>
Use-o em 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>
O client ID é criado uma única vez em <script setup>, que roda uma só vez por instância do componente, então ele permanece estável entre os re-renders.
Envie Dados de Teste
Publique leituras no servidor MQTT público a partir de um 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
O widget se atualiza a cada segundo. Pressione o botão e acompanhe a chegada dos comandos com mosquitto_sub -h broker.connectmq.com -p 1883 -i vue-sub-c42e -v -t "connectmq/demo/vue-c42e/+/cmd".
Uma Conexão para o App Inteiro
O composable acima abre uma conexão por componente que o usa. Isso é aceitável para um único widget, mas um dashboard com vinte widgets abriria vinte conexões. Dois padrões melhores:
- Singleton no nível do módulo. Mova o cliente e
messagespara fora da função, para que todas as chamadas os compartilhem, e conte os usuários para saber quando desconectar. Simples e sem dependências. - Store do Pinia. Crie o cliente em uma action do store, mantenha
statusemessagescomo estado do store e deixe os componentes lerem dele. Isso funciona bem com o Vue Devtools e com proteções para renderização no servidor.
Aqui está uma versão compacta da abordagem singleton, salva 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 { /* mantém o 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 chama useSharedMqtt com seu próprio filtro de topic. O primeiro a chamar abre a conexão, cada topic é assinado uma única vez, não importa quantos widgets o exibam, e o último widget a ser desmontado fecha o socket. Como messages é um único objeto reativo indexado por topic, as computed properties de um widget só são reavaliadas quando a chave específica que elas leem muda, o que mantém dashboards grandes responsivos.
De qualquer forma, cada aba do navegador acaba com uma única conexão MQTT, o que mantém você bem dentro dos limites de conexão e torna mais fácil entender a visão que o broker tem dos clientes.
Dicas de Desempenho para MQTT com Vue
Alguns hábitos mantêm um dashboard de MQTT com Vue fluido à medida que ele cresce de um widget para dezenas:
- Armazene o último valor e derive o resto. Mantenha as mensagens brutas no composable e calcule os valores de exibição, como números arredondados, unidades e cores, com
computed. As computed properties ficam em cache, então só são recalculadas quando a mensagem da qual dependem muda. - Limite o histórico dos gráficos. Se um widget mantém uma série móvel para um sparkline, coloque os novos pontos em um array de tamanho fixo e descarte os mais antigos. Arrays sem limite são a causa mais comum de dashboards que ficam lentos depois de algumas horas.
- Use retained messages na primeira renderização. Quando os dispositivos publicam o estado com a flag retain, um widget montado após uma navegação mostra o valor atual imediatamente, em vez de um placeholder vazio.
- Evite watchers profundos em
messages. Umwatch(messages, ..., { deep: true })dispara a cada atualização de qualquer topic. Observe apenas a chave que interessa. - Mostre quando os dados estão desatualizados. Um valor que não muda há minutos pode significar que o dispositivo está offline. Exibir o horário da atualização, como o widget faz, evita que as pessoas confiem em números congelados.
Armadilhas Comuns de MQTT com Vue
- O cliente está envolvido em
ref(). Os sintomas incluem desempenho lento ou erros estranhos vindos de dentro do MQTT.js. Mantenha-o em uma variável simples ou emshallowRef. - Loop de conexão e desconexão. Dois componentes ou abas compartilham um client ID fixo. Gere IDs únicos por conexão.
- Erros de conteúdo misto. A página é servida por HTTPS, mas a URL usa
ws://. Mude parawss://na porta 8084. - Nada se atualiza após uma oscilação de rede. As assinaturas foram feitas fora do handler
connectcom uma clean session. - Falhas no SSR. No Nuxt ou em outra configuração com SSR, conecte apenas no cliente, o que o
onMountedjá garante.
Configuração para Produção
Em uma aplicação real, conecte-se a um broker privado com credenciais. No ConnectMQ, um broker MQTT gerenciado, crie um broker no portal e passe o nome de usuário e a senha gerados por variáveis de ambiente do 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"]
);
Lembre-se de que qualquer coisa incluída no código do frontend pode ser lida pelos usuários. Use uma credencial dedicada para o dashboard e envie comandos sensíveis por uma API de backend, e não diretamente do navegador. Um broker MQTT gerenciado com um certificado Let's Encrypt válido na porta WebSocket significa que os navegadores se conectam sem avisos nem configuração extra.
Tutoriais Relacionados
Se sua equipe também usa outros frameworks, compare este composable com o serviço de MQTT com Angular construído sobre RxJS. Para adicionar verificações em tempo de compilação para topics e payloads, veja MQTT com tipagem segura em TypeScript. O guia de bibliotecas cliente MQTT cobre opções fora do JavaScript para seus serviços de backend, e a documentação de composables do Vue explica em profundidade o padrão usado aqui.
Comece a Usar o ConnectMQ
Dê ao seu app Vue um broker privado com WebSocket seguro em poucos minutos. Cadastre-se em app.connectmq.com, escolha o plano Serverless gratuito, sem cartão de crédito, crie um broker e conecte as credenciais ao useMqtt. O explorador de topics ao vivo do portal do ConnectMQ mostra exatamente quais mensagens seus widgets deveriam estar recebendo.
Perguntas frequentes
Qual biblioteca MQTT devo usar com o Vue?
O MQTT.js é a escolha padrão. Ele funciona no navegador via WebSocket, suporta MQTT 3.1.1 e 5.0 e se encaixa muito bem em um composable como o deste artigo.
Isso funciona com o Vue 2?
O composable usa a Composition API, que já vem embutida no Vue 2.7. Para versões mais antigas do Vue 2, coloque a mesma lógica nos hooks mounted e beforeDestroy de um componente.
Por que meu widget mostra "reconnecting" o tempo todo?
Normalmente outra conexão está usando o mesmo client ID, um firewall bloqueia a porta 8084 ou falta o caminho /mqtt na URL. Verifique a mensagem de erro do MQTT.js no console do navegador.
Posso publicar a partir de um app Vue?
Sim, com a função publish retornada pelo composable. Para ações que afetam equipamentos reais, prefira encaminhá-las por um backend autenticado, para que os usuários do navegador não possam publicar mensagens arbitrárias.



