https://d226lax1qjow5r.cloudfront.net/blog/blogposts/voice-chat-with-vue-and-express-dr/Elevate_ConversationVueJS-1.png

Créer une application de Voice Chat avec Vue.js et Express

Publié le April 30, 2021

Temps de lecture : 12 minutes

L'ajout d'un service à une application web complexe peut s'avérer difficile à maintenir. C'est encore plus vrai lorsque le service a un composant d'interface utilisateur. Avec l'API de Nexmo, vous pouvez créer un chat vocal dans le navigateur qui devient la base d'une variété d'applications de communication. Mais même l'organisation des éléments de cette interface utilisateur de base peut s'avérer difficile. Les composants tels que ceux utilisés dans Vue.js facilitent cette tâche en fournissant un modèle pour les modèles, le style et les scripts d'interface utilisateur qu'un composant d'interface utilisateur individuel peut nécessiter. Un serveur Express se connectant aux outils Nexmo vous offre une solution complète et légère qui peut être adaptée à n'importe quelle architecture réelle, grâce à la séparation des préoccupations.

Il existe de nombreuses façons de structurer une application avec Vue. Pour ce tutoriel, je vais remixer un projet de projet Glitch qui fournit relativement peu d'échafaudage, mais vous pouvez choisir un projet de démarrage fourni par l'outil Vue CLI ou une bibliothèque tierce offrant des fonctionnalités spécifiques comme le rendu côté serveur. Comme votre code s'appuiera à la fois sur Vue et Express, la seule exigence est que votre installation comprenne les deux.

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

Ajouter Nexmo à votre projet

Pour créer une conversation à partir du navigateur, vous devez installer le client Nexmo Nexmo et le serveur de Nexmo. Comme l'utilisateur enverra des données à partir du client, vous aurez également besoin de body-parser pour l'utiliser dans Express. Dans le répertoire racine de votre projet, installez ces paquets avec npmou depuis la console de Glitch avec pnpm:

pnpm install nexmo@beta nexmo-client body-parser -s

Pour utiliser les outils Nexmo, vous devrez également fournir vos identifiants API dans le fichier .env dans le fichier Le fichier doit ressembler à ceci :

API_KEY="12ab3456" API_SECRET="123AbcdefghIJklM" APP_ID="a0b23456-c789-012d-3456-e789012f34a5" PRIVATE_KEY="/.data/private.key"

En fonction de votre environnement, il se peut que vous deviez également installer le paquetage dotenv à partir de npm. Pour importer vos variables d'environnement depuis .envil vous suffit d'ajouter une ligne au début de votre fichier server.js au début de votre fichier require('dotenv').config();

Vous pouvez trouver votre clé API et votre secret sur le site Web de la Démarrage de votre tableau de bord Nexmo. Dans le menu Voice, cliquez sur Créer une application et cliquez sur "Générer une paire de clés publiques/privées" pour télécharger votre fichier. private.key fichier. Remplissez ensuite les champs et cliquez sur "Créer une application" pour obtenir votre identifiant d'application.

Veillez à copier votre fichier private.key dans votre projet et de mettre à jour le chemin dans .env à l'emplacement où vous l'avez enregistré. Il est possible de coller le contenu directement dans le fichier .envmais le formatage peut poser des problèmes. Il est généralement plus robuste de le conserver dans un fichier séparé.

Un serveur pour les appels API

Le rôle de Express.js dans votre projet sera de fournir un serveur simple qui appelle l'API Nexmo pour effectuer quelques tâches administratives. Cela nécessitera une certaine configuration du serveur lui-même, une instance Nexmo, et des définitions de routes pour les points d'extrémité de votre serveur.

En server.jscréez le serveur et demandez-lui d'analyser JSON dans les corps de requête et de servir des pages statiques à partir du répertoire public et de servir des pages statiques à partir du répertoire Ensuite, créez un objet Nexmo, en lui passant les valeurs de .env. Enfin, créez des espaces réservés pour vos itinéraires et demandez au serveur de commencer à écouter les événements :

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(express.static('public'));

// create a Nexmo client
const Nexmo = require('nexmo');
const nexmo = new Nexmo({
  apiKey: process.env.API_KEY,
  apiSecret: process.env.API_SECRET,
  applicationId: process.env.APP_ID,
  privateKey: __dirname + process.env.PRIVATE_KEY 
}, {debug: true});

// the client calls this endpoint to request a JWT, passing it a username
app.post('/getJWT', function(req, res) {});

// the client calls this endpoint to get a list of all users in the Nexmo application
app.get('/getUsers', function(req, res) {});

// the client calls this endpoint to create a new user in the Nexmo application,
// passing it a username and optional display name
app.post('/createUser', function(req, res) {});

app.listen(process.env.PORT);

Routes du serveur

Les trois routes définies sur le serveur permettront à l'application de répertorier et de créer des utilisateurs pouvant participer à une conversation, et de les authentifier. Dans une application destinée à être utilisée dans le monde réel, vous connecteriez probablement cela à votre propre gestion des utilisateurs plutôt qu'à une interface web.

La route /getJWT fournit un jeton que le client peut utiliser pour authentifier l'utilisateur actuel. La production de la clé JWT se fait à l'aide d'une seule fonction, mais elle nécessite plusieurs données. Vous devez à nouveau fournir l'identifiant de votre application, ainsi que subqui est le nom d'utilisateur que vous voulez authentifier. Vous devez également définir l'expiration et les chemins autorisés pour le jeton. Vous pouvez envoyer le jeton nouvellement créé au client :

// the client calls this endpoint to request a JWT, passing it a username
app.post('/getJWT', function(req, res) {
  const jwt = nexmo.generateJwt({
    application_id: process.env.APP_ID,
    sub: req.body.name,
    exp: Math.round(new Date().getTime()/1000)+3600,
    acl: {
      "paths": {
        "/v1/users/**":{},
        "/v1/conversations/**":{},
        "/v1/sessions/**":{},
        "/v1/devices/**":{},
        "/v1/image/**":{},
        "/v3/media/**":{},
        "/v1/push/**":{},
        "/v1/knocking/**":{}
      }
    }
  });
  res.send({jwt: jwt});
});

Le chemin /getUsers effectue également un appel unique et renvoie le résultat, mais nous allons le nettoyer un peu pour l'utiliser dans une interface web. Avant de renvoyer la liste de tous les utilisateurs de cette application, vous pouvez filtrer les utilisateurs du système dont l'ID commence par le préfixe NAM-. Dans une application réelle où les identifiants des utilisateurs sont associés à des Account dans votre application principale, vous n'auriez probablement pas à vous soucier de cette étape et pourriez retourner la liste telle quelle :

// the client calls this endpoint to get a list of all users in the Nexmo application
app.get('/getUsers', function(req, res) {
  const users = nexmo.users.get({}, (err, response) => {
    if (err) {
      res.sendStatus(500);
    } else {
      let realUsers = response.filter(user => user.name.substring(0,4) !== 'NAM-');
      res.send({users: realUsers});
    }
  });
});

La dernière route, /createUserprend en compte les données de l'utilisateur et ajoute un utilisateur à l'application. Comme la fonction create prend en entrée à la fois un nom d'utilisateur et un nom d'affichage, il est possible dans ce code de définir un nom d'affichage séparé, mais nous ne l'inclurons pas dans l'interface utilisateur. Par conséquent, le point d'accès ne recherche qu'un name de la part du client, et une fois qu'il a créé un utilisateur avec, il renvoie son ID :

// the client calls this endpoint to create a new user in the Nexmo application,
// passing it a username and optional display name
app.post('/createUser', function(req, res) {
  nexmo.users.create({
    name: req.body.name,
    display_name: req.body.display_name || req.body.name
  },(err, response) => {
    if (err) {
      res.sendStatus(500);
    } else {
      res.send({id: response.id});
    }
  });
});

Le composant Vue App

Tous les composants Vue de ce projet se trouvent dans le répertoire src dans le répertoire Le projet que je remixe comprend déjà un fichier main.js qui crée une instance Vue, ainsi qu'un composant conteneur dans le répertoire app.vue. main.js ne fait rien d'autre que de rendre le composant App :

var Vue = require('vue');
var App = require('./app.vue');

var vm = new Vue({
  el: '#app',
  render: createElement => {
    return createElement(App)
  }
});

Cela fonctionne de concert avec public/index.htmloù une div avec l'ID app est le seul élément de la page :

<!DOCTYPE html>
<html>
<head>
  <title>VueJS + Express Template</title>
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
  <div id="app"></div>
  <script src="build.js"></script>
</body>
</html>

Au cas où nous voudrions en ajouter plus tard, nous laisserons le composant App en place et charger un composant Nexmo en son sein, plutôt que de remplacer App par Nexmo. Si vous avez déjà un fichier app.vue vous pouvez remplacer son contenu par un modèle et un script simples qui ne font que charger le composant Nexmo composant :

template>
  <div class="app">
    <Nexmo/>
  </div>
</template>

<script>
import Nexmo from './nexmo.vue';

export default {
  name: 'App',
  components: {
    Nexmo
  }
}
</script>

Le composant Nexmo

Le composant Nexmo c'est là que les choses commencent à devenir intéressantes. Vous pouvez le créer à nexmo.vue et ajouter un modèle au début du fichier qui rendra User et Conversation composants. Par exemple Userun crochet de mise à jour appellera une fonction getJWT dans le script que vous ajouterez ensuite. Vous pouvez également ajouter une référence au composant pour y accéder ultérieurement :

<template>
  <div class="nexmo">
    <User @hook:updated="userUpdated" ref="user" />
    <Conversation/>
  </div>
</template>

Sous le modèle, vous ajouterez une balise de script contenant la logique du composant. Après avoir importé les deux sous-composants et le Client SDK Nexmo, vous exporterez un composant Vue nommé Nexmo. Il contiendra quelques propriétés data qui feront partie de son état, ainsi que ses sous-composants et quelques méthodes que vous définirez ensuite :

<script>
  import User from './user.vue';
  import Conversation from './conversation.vue';
  import nexmoClient from 'nexmo-client';
  
  export default {
    name: 'Nexmo',
    data: () => ({
      app: null,
      token: null,
      invites: [],
      loggedIn: false
    }),
    components: {
      User,
      Conversation
    },
    methods: {}
  };
</script>

La propriété methods définira deux fonctions, l'une pour obtenir un JWT du serveur et l'autre pour gérer la connexion. La fonction getJWT est appelée par le crochet de mise à jour de votre composant User et doit donc d'abord vérifier si ce composant contient une propriété username . Si c'est le cas, il peut appeler la fonction de mise à jour. Si c'est le cas, il peut appeler le point d'accès côté serveur /getJWT côté serveur en utilisant fetch. Il transmet la valeur username et, si tout se passe bien, obtient un JWT en retour. Il stocke le JWT en tant que propriété de l'instance et appelle la fonction login fonction.

La fonction login est l'endroit où vous instancierez un client Nexmo réel. Vous allez connecter votre utilisateur avec son JWT, puis définir un drapeau si cela réussit et enregistrer une référence à l'application Nexmo. Une fois que vous avez l'application, vous pouvez obtenir les conversations auxquelles l'utilisateur actuel est invité :

    methods: {
      getJWT: function() {
        var username = this.$refs.user.username;
        if (!username) {
          return;
        }
        var vm = this;
        fetch('/getJWT', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            name: username
          })
        })
        .then(results => results.json())
        .then(data => {
          vm.token = data.jwt;
          vm.login();
        });
      },
      login: function() {
        let nexmo = new nexmoClient();
        nexmo.login(this.token).then(app => {
          this.loggedIn = true;
          this.app = app;
          app.getConversations().then(convos => {
            this.invites = Array.from(convos.entries());
          });
        });
      }
    }

Le composant utilisateur

La composante User à src/user.vue est le premier endroit où vous aurez un modèle qui fait autre chose que de rendre un sous-composant. C'est une autre partie que vous pourriez sauter dans une application de production, mais dans ce cas, l'interface de connexion de l'utilisateur fait partie de votre application Nexmo. Le modèle affichera l'utilisateur connecté s'il existe. Si ce n'est pas le cas, il affiche un formulaire avec deux chemins. Le premier permet de sélectionner un utilisateur existant dans une liste déroulante. Si une sélection est faite, l'utilisateur est immédiatement mis à jour par la fonction setExistingUser fonction.

Un utilisateur peut également fournir un nouveau nom d'utilisateur et cliquer sur le bouton "Soumettre". Cela appelle la fonction createUser fonction :

<template>
  <div v-if="userId" class="userinfo userconnected">
    Connected as <span class="username">{{username}}</span>
  </div>
  <div v-else class="userinfo">
    <label>User name: 
      <select v-on:change="setExistingUser">
        <option value=""></option>
        <option v-for="item in currentUsers" v-bind:value="item.id">
          {{item.name}}
        </option>
      </select>
    </label>
    <input type="text" v-on:change="setUsername" />
    <button v-on:click="createUser">Create user</button>
  </div>
</template>

Script du composant utilisateur

Le script pour le composant a plusieurs choses à faire, mais pas de logique complexe. L'essentiel de ce qu'il fait est de charger et d'enregistrer des propriétés. Les choses complexes se produisent dans le cadre de Vue lui-même, dans des fonctionnalités telles que le crochet de mise à jour dans votre Nexmo composant.

Il n'y a rien à importer, vous pouvez donc immédiatement exporter un User immédiatement. Les seules data dont il aura besoin sont des propriétés pour l'ID et le nom de l'utilisateur, ainsi qu'une liste des utilisateurs actuels de l'application.

Le composant dispose de quatre méthodes pour prendre en charge le formulaire dans le modèle. La fonction getUsers appelle /getUsers au serveur pour récupérer la liste des utilisateurs. Vous vous souviendrez que vous avez géré toute logique de filtrage nécessaire côté serveur, de sorte que s'il n'y a pas d'erreur, vous pouvez simplement définir cette propriété sur le composant.

setExistingUser est appelé par un événement onchange sur la liste déroulante des utilisateurs. Il enregistre le nom d'utilisateur et l'ID utilisateur de la sélection effectuée. Pour les nouveaux utilisateurs, setUsername est également appelé par un événement onchangecette fois sur le champ de texte. La mise à jour du nouveau nom d'utilisateur sur le composant à chaque fois qu'il change vous évite d'avoir à obtenir une référence à l'élément du champ de texte. Si un utilisateur clique sur le bouton "Créer un utilisateur", createUser est appelée, envoyant le nom d'utilisateur en état au serveur et sauvegardant l'identifiant de l'utilisateur qui est renvoyé.

Après methodsce composant appelle également beforeMount pour s'assurer que la liste des utilisateurs est chargée lors de la première initialisation :

<template>
  ...
</template>

<script>  
export default {
  name: 'User',
  data: () => ({
    userId: undefined,
    username: null,
    currentUsers: []
  }),
  methods: {
    getUsers: function() {
      var vm = this;
      fetch('/getUsers', {
        method: 'GET'
      }).then(results => results.json())
      .then(data => {
        vm.currentUsers = data.users;
      });
    },

    setExistingUser: function(evt) {
      this.username = evt.target[evt.target.selectedIndex].text;
      this.userId = evt.target.value;
    },

    setUsername: function(evt) {
      this.username = evt.target.value;
    },

    createUser: function() {
      var vm = this;
      fetch('/createUser', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          name: vm.username
        })
      }).then(results => results.json())
        .then(data => { 
          vm.userId = data.id;
        });
    }
  },
  beforeMount() {
    this.getUsers()
  }
};
</script>

Le volet "conversation

Jusqu'à présent, le code que vous avez écrit a servi à créer votre application Nexmo, à définir un utilisateur et à vous connecter. Il devrait être suffisamment séparé pour que vous puissiez apporter des modifications afin de répondre aux besoins de votre projet individuel tout en exposant les éléments d'information essentiels à votre application Vue plus large. Vous pouvez maintenant utiliser ces éléments pour participer à une conversation. La conversation est le point de départ d'une variété de types de communication que vous pourriez souhaiter faire en utilisant l'API de Nexmo sur le client.

Le composant serait plus facile à gérer si quelques sous-composants étaient séparés (par exemple, les contrôles audio). Cependant, pour rendre les choses plus évidentes pour l'instant, vous pouvez rassembler tout le code dans le fichier conversation.vue.

Le modèle

Au niveau supérieur du modèle se trouve une condition permettant de déterminer s'il existe une conversation en cours. Si c'est le cas, vous créerez un élément audio pour fournir le son, ainsi que deux boutons pour activer et désactiver l'audio. Lorsqu'ils seront cliqués, ils appelleront enableAudio et disableAudiorespectivement.

S'il n'y a pas de conversation en cours, l'utilisateur devra en rejoindre une ou en commencer une. Si l'utilisateur a été invité à une conversation ou en a déjà entamé une, celle-ci apparaîtra dans le tableau invites du composant parent Nexmo du composant parent. Les valeurs dans invites alimenteront une liste déroulante de conversations, et la sélection de l'une d'entre elles appellera la fonction joinConversation fonction. Que l'utilisateur ait ou non des conversations existantes, il verra un bouton lui permettant d'entamer une nouvelle conversation. invitesil verra un bouton lui permettant de démarrer une nouvelle conversation :

<template>
  <div v-if="current_conv" class="conversation">
    <audio ref="audio">
      <source/>
    </audio>
    <button v-on:click="enableAudio" v-bind:disabled="audioOn">
      Enable audio
    </button>
    <button v-on:click="disableAudio" v-bind:disabled="!audioOn">
      Disable audio
    </button>
  </div>
  <div v-else class="conversation">
    <label v-if="$parent.invites.length">Choose an active conversation: 
      <select v-on:change="joinConversation">
        <option value="0">-</option>
        <option v-for="invite in $parent.invites" v-bind:value="invite[0]">
          {{invite[1].name}}
        </option>
      </select> or
    </label>
    <button v-on:click="createConversation" :disabled="!$parent.loggedIn">
      Start conversation
    </button>
  </div>
</template>

Le scénario

Dans ce composant également, la seule chose qui se passe au niveau supérieur de la balise script est l'exportation d'un composant Conversation composant. Ses seuls data sont la conversation en cours et un indicateur permettant de savoir si l'audio est activé.

Les methods que contient le composant sont tous assez simples. createConversation appelle la fonction newConversation de l'application stockée dans le composant parent Nexmo puis stocke la conversation créée. joinConversation fait la même chose, sauf qu'il appelle la fonction de l'application, en lui transmettant l'ID de la conversation sélectionnée dans le menu déroulant. getConversation de l'application, en lui passant l'ID de la conversation sélectionnée dans le menu déroulant.

En enableAudiovous devez d'abord activer les médias dans la conversation en cours. Vous obtiendrez ainsi un flux que vous pourrez définir comme l'élément srcObject ou src de l'élément audio dans votre modèle. Une fois les métadonnées chargées, vous pouvez lire le flux et définir l'indicateur audioOn du composant à true. La fonction disableAudio appelée lorsque vous cliquez sur le bouton "Désactiver l'audio" est plus simple. Il suffit de désactiver les médias dans current_conv puis remettre l'indicateur audioOn à la valeur false:

<template>
  ...
</template>

<script>
  export default {
    name: 'Conversation',
    data: () => ({
      current_conv: undefined,
      audioOn: false
    }),
    methods: {
      createConversation: function() {
        var vm = this;
        this.$parent.app.newConversation().then(conv => {
          conv.join();
          vm.current_conv = conv;
        });
      },
  
      joinConversation: function(evt) {
        var vm = this;
        this.$parent.app.getConversation(evt.target.value).then(conv => {
          conv.join();
          vm.current_conv = conv;
        });
      },

      enableAudio: function() {
        var vm = this;
        this.current_conv.media.enable().then(stream => {
          // Older browsers may not have srcObject
          if ('srcObject' in vm.$refs.audio) {
            vm.$refs.audio.srcObject = stream;
          } else {
            // Avoid using this in new browsers, as it is going away.
            vm.$refs.audio.src = window.URL.createObjectURL(stream);
          }
          vm.$refs.audio.onloadedmetadata = () => {
            vm.$refs.audio.play();
            vm.audioOn = true;
          }
        });
      },

      disableAudio: function() {
        var vm = this;
        this.current_conv.media.disable().then(() => {
          vm.audioOn = false;
        });
      }
    }
  };
</script>

Le reste

Il y a quelques éléments que nous n'avons pas couverts, qui sont, espérons-le, fournis par le boilerplate Vue que vous avez choisi ou qui ne doivent pas nécessairement affecter la logique de votre application. Par exemple, dans mon propre code, je m'appuie sur Browserify et sur Vueifyainsi qu'un peu de CSS qui faisait partie du projet que j'ai remixé. L'étape de construction qui fait fonctionner le côté Vue de l'application est définie dans "scripts" dans package.json:

"compile": "browserify -t vueify -e src/main.js -o public/build.js"

Prochaines étapes

Le code que vous avez écrit est en grande partie un point de départ pour votre travail dans le monde réel. Comme nous l'avons mentionné, vous voudrez probablement remplacer le système de gestion des utilisateurs de test par quelque chose qui lie les membres de la conversation à vos propres utilisateurs authentifiés. Une fois la conversation créée, vous pouvez envoyer et recevoir des messages, recevoir des appels et mettre en place des conférences audio.

En savoir plus sur le Client SDK Nexmo pour découvrir ce que vous pouvez faire ensuite :

Partager:

https://a.storyblok.com/f/270183/250x250/f231d97f1b/garann-means.png
Garann MeansDéveloppeur Éducateur

Je suis développeur JavaScript et éducateur de développeurs chez Vonage. Au fil des ans, j'ai été très intéressé par les modèles, Node.js, les applications Web progressives et les stratégies offline-first, mais ce que j'ai toujours aimé, c'est une API utile et bien documentée. Mon objectif est de faire en sorte que votre expérience de l'utilisation de nos API soit la meilleure possible.