Un client Vue MQTT permet à votre application Vue 3 d'afficher les données d'un appareil dès leur publication, sans interroger une API en boucle. Dans ce tutoriel, vous allez écrire un composable useMqtt réutilisable autour de MQTT.js, le connecter à un broker en WebSocket sécurisé et construire un widget de capteur en direct doté d'un bouton de commande. Au passage, vous éviterez les erreurs de réactivité et de cycle de vie qui rendent de nombreuses applications Vue MQTT lentes ou sujettes aux fuites.
Comment Vue se connecte à MQTT
Vue s'exécute dans le navigateur, et les navigateurs ne peuvent pas ouvrir de sockets TCP : MQTT circule donc à l'intérieur d'un WebSocket. MQTT.js s'en charge de manière transparente dès que vous lui fournissez une URL wss://. Les brokers ConnectMQ exposent le WebSocket sécurisé sur le port 8084 avec le chemin /mqtt :
wss://broker.connectmq.com:8084/mqtt
Le serveur MQTT public gratuit broker.connectmq.com accepte les connexions anonymes avec n'importe quel client ID unique et non vide, ce qui le rend idéal pour le développement et les prototypes, pas pour la production. Son espace public est lisible par tout le monde : utilisez un préfixe de topic aléatoire et uniquement des données de test. Pour en savoir plus sur ce transport, consultez MQTT over WebSocket.
Mise en place du projet
npm create vue@latest vue-mqtt-demo
cd vue-mqtt-demo
npm install
npm install mqtt
npm run dev
Acceptez les valeurs par défaut lors de la génération du projet ; TypeScript, Pinia et le router sont facultatifs pour ce tutoriel. MQTT.js v5 inclut une version navigateur que Vite résout automatiquement.
Écrire le composable useMqtt
Créez 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);
// dernier message par topic : { [topic]: { value, receivedAt } }
const messages = reactive({});
// variable simple, PAS réactive : Vue ne doit jamais créer de proxy sur le client 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,
};
}
Les choix de conception expliqués
| Choix | Pourquoi |
|---|---|
| Client conservé dans une variable simple | Envelopper le client MQTT dans ref ou reactive amène Vue à créer des proxys sur ses éléments internes, ce qui coûte en performance et peut casser la gestion des événements. Si vous devez le stocker dans l'état, utilisez shallowRef ou markRaw. |
| Dernière valeur par topic | Un tableau qui accumule tous les messages provoque une fuite mémoire dans les tableaux de bord de longue durée. |
Abonnement dans le handler connect |
Les abonnements sont rétablis automatiquement après les reconnexions. |
Cleanup dans onBeforeUnmount |
Quitter la page ferme le socket au lieu de laisser une connexion fantôme qui compte dans votre limite. |
Valeurs renvoyées en readonly |
Les composants ne peuvent pas modifier accidentellement l'état de connexion partagé. |
Vue regroupe les mises à jour du DOM à chaque tick : écrire dans messages à chaque message entrant ne pose donc aucun problème pour quelques messages par seconde. Si un appareil publie beaucoup plus vite, mettez les messages en tampon dans un objet simple et copiez-les dans messages sur un timer, comme décrit dans l'article sur le hook React MQTT.
Un widget de capteur en direct
Créez 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>
Utilisez-le dans 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>
Le client ID est créé une seule fois dans <script setup>, qui ne s'exécute qu'une fois par instance de composant : il reste donc stable d'un rendu à l'autre.
Envoyer des données de test
Publiez des mesures vers le serveur MQTT public gratuit depuis 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
Le widget se met à jour chaque seconde. Appuyez sur le bouton et observez l'arrivée des commandes avec mosquitto_sub -h broker.connectmq.com -p 1883 -i vue-sub-c42e -v -t "connectmq/demo/vue-c42e/+/cmd".
Une seule connexion pour toute l'application
Le composable ci-dessus ouvre une connexion par composant qui l'utilise. C'est acceptable pour un widget unique, mais un tableau de bord de vingt widgets ouvrirait vingt connexions. Deux patterns sont préférables :
- Singleton au niveau du module. Déplacez le client et
messageshors de la fonction pour que tous les appels les partagent, et comptez les utilisateurs pour savoir quand vous déconnecter. Simple et sans dépendance. - Store Pinia. Créez le client dans une action du store, conservez
statusetmessagesdans l'état du store et laissez les composants lire depuis celui-ci. Cette approche s'intègre bien avec Vue Devtools et les protections de rendu côté serveur.
Voici une version compacte de l'approche singleton, enregistrée sous 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 { /* conserver le texte */ }
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) };
}
Chaque widget appelle useSharedMqtt avec son propre filtre de topic. Le premier appelant ouvre la connexion, chaque topic n'est souscrit qu'une fois quel que soit le nombre de widgets qui l'affichent, et le dernier widget démonté ferme le socket. Comme messages est un unique objet réactif indexé par topic, les propriétés calculées d'un widget ne sont réévaluées que lorsque la clé précise qu'elles lisent change, ce qui garde les grands tableaux de bord réactifs.
Dans les deux cas, chaque onglet de navigateur ne dispose que d'une seule connexion MQTT, ce qui vous maintient largement sous les limites de connexions et rend la vue des clients côté broker plus lisible.
Conseils de performance pour Vue MQTT
Quelques habitudes gardent un tableau de bord Vue MQTT fluide quand il passe d'un widget à plusieurs dizaines :
- Stockez la dernière valeur, dérivez le reste. Conservez les messages bruts dans le composable et calculez les valeurs affichées, comme les nombres arrondis, les unités et les couleurs, avec
computed. Les propriétés calculées sont mises en cache et ne sont réexécutées que lorsque le message dont elles dépendent change. - Plafonnez l'historique des graphiques. Si un widget conserve une série glissante pour une sparkline, ajoutez les nouveaux points dans un tableau de longueur fixe et supprimez les plus anciens. Les tableaux non bornés sont la cause la plus fréquente des tableaux de bord qui ralentissent au bout de quelques heures.
- Utilisez les retained messages pour le premier affichage. Quand les appareils publient leur état avec le flag retain, un widget monté après une navigation affiche immédiatement la valeur actuelle au lieu d'un espace vide.
- Évitez les watchers profonds sur
messages. Unwatch(messages, ..., { deep: true })se déclenche à chaque mise à jour de chaque topic. Surveillez plutôt la seule clé qui vous intéresse. - Signalez les données périmées. Une valeur qui n'a pas changé depuis plusieurs minutes peut signifier que l'appareil est hors ligne. Afficher l'heure de mise à jour, comme le fait le widget, évite que l'on se fie à des chiffres figés.
Pièges courants avec Vue MQTT
- Le client est enveloppé dans
ref(). Les symptômes vont de performances médiocres à des erreurs étranges provenant de MQTT.js. Conservez-le dans une variable simple ou unshallowRef. - Boucle de connexions et de déconnexions. Deux composants ou onglets partagent un client ID fixe. Générez des ID uniques par connexion.
- Erreurs de contenu mixte. La page est servie en HTTPS mais l'URL utilise
ws://. Passez àwss://sur le port 8084. - Plus rien ne se met à jour après une microcoupure réseau. Les abonnements ont été effectués en dehors du handler
connectavec une clean session. - Plantages en SSR. Avec Nuxt ou une autre configuration SSR, ne vous connectez que côté client, ce que
onMountedgarantit déjà.
Configuration de production
Pour une application réelle, connectez-vous à un broker privé avec identifiants. Sur ConnectMQ, créez un broker dans le portail, puis transmettez le nom d'utilisateur et le mot de passe générés via des variables d'environnement 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"]
);
Gardez à l'esprit que tout ce qui est intégré au code frontend peut être lu par les utilisateurs. Utilisez un identifiant dédié au tableau de bord et faites passer les commandes sensibles par une API backend plutôt que directement depuis le navigateur. Avec un broker MQTT managé disposant d'un certificat Let's Encrypt valide sur le port WebSocket, les navigateurs se connectent sans avertissement ni configuration supplémentaire.
Tutoriels associés
Si votre équipe utilise aussi d'autres frameworks, comparez ce composable avec le service Angular MQTT construit sur RxJS. Pour ajouter des vérifications à la compilation sur les topics et les payloads, consultez MQTT typé avec TypeScript. Le guide des bibliothèques clientes MQTT présente les options hors JavaScript pour vos services backend, et la documentation des composables Vue explique en profondeur le pattern utilisé ici.
Démarrer avec ConnectMQ
Offrez à votre application Vue un broker privé avec WebSocket sécurisé en quelques minutes. Inscrivez-vous sur app.connectmq.com, choisissez l'offre Serverless gratuite sans carte bancaire, créez un broker et branchez les identifiants dans useMqtt. L'explorateur de topics en direct du portail ConnectMQ montre exactement quels messages vos widgets devraient recevoir.
Questions fréquentes
Quelle bibliothèque MQTT utiliser avec Vue ?
MQTT.js est le choix de référence. Elle fonctionne dans le navigateur via WebSocket, prend en charge MQTT 3.1.1 et 5.0, et s'intègre proprement dans un composable comme celui de cet article.
Cela fonctionne-t-il avec Vue 2 ?
Le composable utilise la Composition API, intégrée à Vue 2.7. Pour les versions plus anciennes de Vue 2, placez la même logique dans les hooks mounted et beforeDestroy d'un composant.
Pourquoi mon widget affiche-t-il constamment « reconnecting » ?
Le plus souvent, une autre connexion utilise le même client ID, un pare-feu bloque le port 8084 ou il manque le chemin /mqtt dans l'URL. Consultez la console du navigateur pour lire le message d'erreur de MQTT.js.
Peut-on publier depuis une application Vue ?
Oui, avec la fonction publish renvoyée par le composable. Pour les actions qui agissent sur de vrais équipements, privilégiez un passage par un backend authentifié afin que les utilisateurs du navigateur ne puissent pas publier de messages arbitraires.



