https://a.storyblok.com/f/270183/62751/f86b373273/elevate_conversationvuejs-1.png

Criação de um aplicativo de chat de voz com Vue.js e Express

Publicado em April 30, 2021

Tempo de leitura: 11 minutos

Adicionar um serviço a uma aplicação web complexa pode ser complicado de se fazer de maneira sustentável. Isso se torna ainda mais verdadeiro quando o serviço possui um componente de interface de usuário. Com a API da Nexmo, você pode criar um chat de voz no navegador que se torna a base para uma variedade de aplicativos de comunicação. Mas até mesmo organizar os elementos dessa interface de usuário básica pode ser difícil. Componentes como os usados no Vue.js facilitam isso ao fornecer um padrão para os modelos, o estilo e os scripts de interface do usuário que um componente individual da interface pode exigir. Um servidor Express conectado às ferramentas da Nexmo oferece uma solução full-stack leve que pode ser adaptada a qualquer arquitetura real que você venha a adotar, graças à separação de responsabilidades.

Existem várias maneiras de estruturar uma aplicação com o Vue. Para este tutorial, vou adaptar uma projeto do Glitch que oferece relativamente pouca estrutura inicial, mas você pode escolher um projeto inicial fornecido pelo Vue CLI ou uma biblioteca de terceiros que ofereça recursos específicos, como renderização no lado do servidor. Como seu código dependerá tanto do Vue quanto do Express, o único requisito é que sua configuração inclua ambos.

Como adicionar o Nexmo ao seu projeto

Para criar uma conversa a partir do navegador, você precisará instalar tanto o Nexmo cliente e servidor . Como o usuário enviará dados a partir do cliente, você também precisará body-parser usar o Express. No diretório raiz do seu projeto, instale esses pacotes com npm, ou, no console do Glitch, use pnpm:

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

Para usar as ferramentas da Nexmo, você também precisará fornecer suas credenciais de API no .env arquivo. O arquivo deve ter uma aparência semelhante a esta:

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

Dependendo do seu ambiente, talvez seja necessário instalar o dotenv pacote do npm. Para importar suas variáveis de ambiente de .env, basta adicionar uma única linha no início do seu server.js arquivo: require('dotenv').config();

Você pode encontrar sua chave e seu segredo da API na página “Introdução” do seu painel do Nexmo. No menu “Voice”, acesse “Criar uma aplicação” e clique em “Gerar par de chaves pública/privada” para baixar seu private.key arquivo. Em seguida, preencha os campos e clique em “Criar aplicativo” para obter seu ID do aplicativo.

Certifique-se de copiar seu private.key arquivo para o seu projeto e atualize o caminho .env para o local onde você o salvou. É possível colar o conteúdo diretamente no .env, mas a formatação pode causar problemas. Geralmente, é mais seguro mantê-lo em um arquivo separado.

Um servidor para chamadas de API

O papel do Express.js no seu projeto será fornecer um servidor simples que chama a API do Nexmo para realizar algumas tarefas administrativas. Isso exigirá algumas configurações do próprio servidor, uma instância do Nexmo e definições de rotas para os endpoints do seu servidor.

Em server.js, crie o servidor e configure-o para analisar JSON nos corpos das solicitações e servir páginas estáticas a partir do public diretório. Em seguida, crie um objeto Nexmo, passando a ele os valores de .env. Por fim, crie placeholders para suas rotas e instrua o servidor a começar a escutar eventos:

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);

Rotas do servidor

As três rotas definidas no servidor permitirão que o aplicativo liste e crie usuários que possam participar de uma conversa, além de autenticá-los. Em um aplicativo para uso no mundo real, você provavelmente conectaria isso ao seu próprio sistema de gerenciamento de usuários, em vez de uma interface web.

A rota /getJWT fornece um token que o cliente pode usar para autenticar o usuário atual. A geração do JWT é feito com uma única função, mas requer vários dados. Você precisa fornecer novamente o ID do seu aplicativo, bem como sub, que é o nome de usuário que você deseja autenticar. Você também definirá o prazo de validade e os caminhos permitidos para o token. Você pode enviar o token recém-criado ao cliente:

// 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});
});

A /getUsers código também faz uma única chamada e retorna seu resultado, mas vamos organizá-lo um pouco para uso em uma interface web. Antes de retornar a lista de todos os usuários neste aplicativo, você pode filtrar os usuários do sistema cujos IDs começam com o prefixo NAM-. Em uma aplicação real, na qual os IDs dos usuários fossem mapeados para Accounts dentro de seu aplicativo maior, você provavelmente não se preocuparia com essa etapa e poderia retornar a lista como está:

// 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});
    }
  });
});

A última rota, /createUserreceberá algumas entradas do usuário e adicionará um usuário ao aplicativo. Como a create função aceita tanto um nome de usuário quanto um nome de exibição como entrada, há a opção neste código de definir um nome de exibição separado; no entanto, não incluiremos isso na interface do usuário. Portanto, o endpoint busca apenas um name do cliente e, assim que cria um usuário com ele, retorna o ID do usuário:

// 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});
    }
  });
});

O componente de aplicativo do Vue

Todos os componentes Vue deste projeto ficarão no src diretório. O projeto que estou adaptando já inclui um main.js arquivo nesse local que cria uma instância do Vue, bem como um componente contêiner em app.vue. main.js não faz nada além de renderizar o componente App:

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

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

Isso funciona em conjunto com public/index.html, onde um div com o ID app é o único elemento da página:

<!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>

Caso queiramos acrescentar mais coisas posteriormente, vamos deixar o App componente onde está e carregaremos um Nexmo componente dentro dele, em vez de substituir App por Nexmo. Se você já tiver um app.vue arquivo, você pode substituir seu conteúdo por um modelo simples e um script que apenas carregue o Nexmo componente:

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

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

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

O Componente Nexmo

O Nexmo componente é onde as coisas começam a ficar interessantes. Você pode criá-lo em nexmo.vue e adicionar um modelo no início do arquivo que irá renderizar User e Conversation componentes. Para User, um gancho de atualização chamará uma função getJWT no script que você adicionará a seguir. Você também pode adicionar uma referência ao componente para acessá-lo mais tarde:

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

Abaixo do modelo, você adicionará uma tag de script contendo a lógica do componente. Após importar os dois subcomponentes e o Nexmo Client SDK, você exportará um componente Vue chamado Nexmo. Ele conterá algumas data que farão parte de seu estado, bem como seus subcomponentes e alguns métodos que você definirá a seguir:

<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>

A methods propriedade definirá duas funções: uma para obter um JWT do servidor e outra para lidar com o login. A getJWT função é chamada pelo hook `update` do seu User componente; portanto, ela deve primeiro verificar se esse componente contém uma username propriedade. Se contiver, ela pode chamar o endpoint do lado do servidor /getJWT usando fetch. Ela passa o valor convertido em string username e, se tudo correr bem, recebe um JWT em troca. Ele armazena o JWT como uma propriedade da instância e chama a login função.

A login função é onde você instanciará um cliente Nexmo propriamente dito. Você fará o login do usuário com seu JWT e, em seguida, definirá um sinalizador caso a operação seja bem-sucedida e salvará uma referência ao aplicativo Nexmo. Depois de obter o aplicativo, você poderá acessar as conversas para as quais o usuário atual foi convidado:

    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());
          });
        });
      }
    }

O Componente do Usuário

O User componente em src/user.vue é o primeiro lugar em que você terá um modelo que faz algo além de renderizar um subcomponente. Essa é outra parte que você poderia pular em um aplicativo de produção, mas, neste caso, a interface de login do usuário faz parte do seu aplicativo Nexmo. O modelo mostrará o usuário conectado, se houver. Caso contrário, ele exibe um formulário com duas opções. A primeira permite que o usuário selecione um usuário existente em um menu suspenso. Se uma seleção for feita, o usuário é imediatamente atualizado pela setExistingUser função.

Um usuário também pode inserir um novo nome de usuário e clicar no botão “Enviar”. Isso chama a createUser função:

<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 de componente do usuário

O script componente tem algumas coisas diferentes acontecendo, mas nenhuma lógica complexa. A maior parte do que ele faz é carregar e salvar propriedades. As coisas complexas acontecem dentro do próprio framework Vue, em funcionalidades como o hook de atualização no seu Nexmo componente.

Não há nada para importar, então você pode exportar imediatamente um User componente. O único data o que ele precisará são propriedades para o ID e o nome do usuário, além de uma lista dos usuários atuais no aplicativo.

O componente possui quatro métodos para dar suporte ao formulário no modelo. As getUsers chamadas de função /getUsers no servidor para buscar a lista de usuários. Você deve se lembrar de que já tratou toda a lógica de filtragem necessária no lado do servidor; portanto, se não houver erro, basta definir essa propriedade no componente.

setExistingUser é chamado por um onchange evento no menu suspenso de usuários. Ele salva o nome de usuário e o ID do usuário da seleção feita. Para novos usuários, setUsername também é chamado por um onchange, desta vez no campo de texto. Atualizar o novo nome de usuário no componente sempre que ele mudar evita que você precise obter uma referência ao elemento do campo de texto. Se um usuário clicar no botão “Criar usuário”, createUser é chamado, enviando o nome de usuário armazenado no estado para o servidor e salvando o ID do usuário que é retornado.

Depois de methods, esse componente também chama beforeMount para garantir que a lista de usuários seja carregada na primeira inicialização:

<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>

O Componente de Conversação

Até agora, o código que você escreveu serviu para criar seu aplicativo Nexmo, definir um usuário e fazer o login. Ele deve estar suficientemente separado para que você possa fazer alterações para atender às necessidades específicas do seu projeto, ao mesmo tempo em que continua expondo as informações essenciais ao seu aplicativo Vue como um todo. Agora você pode usar esses elementos para participar de uma conversa. A conversa é o ponto de partida para diversos tipos de comunicação que você pode querer realizar usando a API do Nexmo no cliente.

O componente ficaria mais fácil de gerenciar se alguns subcomponentes fossem separados (por exemplo, os controles de áudio). No entanto, para deixar as coisas mais claras por enquanto, você pode colocar todo o código junto em conversation.vue.

O modelo

No nível superior do modelo, há uma condição para determinar se existe uma conversa em andamento. Se houver, você exibirá um elemento de áudio para reproduzir o som e dois botões para ativar e desativar o áudio. Ao serem clicados, eles chamarão enableAudio e disableAudio, respectivamente.

Se não houver nenhuma conversa em andamento, o usuário precisará participar de uma ou iniciar uma. Se o usuário tiver sido convidado para uma conversa ou já tiver iniciado uma anteriormente, ela aparecerá na invites array no componente pai Nexmo . Os valores em invites preencherão uma lista suspensa de conversas, e a seleção de uma delas chamará a joinConversation função. Independentemente de o usuário já ter invites, ele verá um botão para iniciar uma nova conversa:

<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>

O Roteiro

Mais uma vez, neste componente, a única coisa que acontece no nível superior da script é exportar um Conversation componente. Seu único data são a conversa atual e um sinalizador que indica se o áudio está ativado.

Os methods componentes contidos nele são todos bastante simples. createConversation chama a newConversation função do aplicativo armazenada no componente pai Nexmo e, em seguida, armazena a conversa criada. joinConversation faz a mesma coisa, exceto que chama a getConversation função do aplicativo, passando a ela o ID da conversa selecionada no menu suspenso.

Para enableAudio, primeiro você precisa habilitar a mídia na conversa atual. Isso lhe dará um fluxo que você pode definir como o srcObject ou src do audio no seu modelo. Assim que os metadados forem carregados, você poderá reproduzir o fluxo e definir o audioOn sinalizador para true. A disableAudio função chamada quando o botão “Desativar áudio” é clicado é mais simples. Ela só precisa desativar a mídia em current_conv e, em seguida, definir o audioOn sinalizador de volta para 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>

O Resto

Há alguns aspectos que ainda não abordamos, mas que, esperamos, sejam fornecidos pelo modelo padrão do Vue que você escolheu ou que não necessariamente afetem a lógica do seu aplicativo. Por exemplo, no meu próprio código, estou utilizando no Browserify e Vueify, além de um pouco de CSS que fazia parte do projeto que eu adaptei. A etapa de compilação que faz a parte do Vue da aplicação funcionar está definida em “scripts” no arquivo package.json:

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

Próximos passos

O código que você escreveu é, sem dúvida, um ponto de partida para o seu trabalho na prática. Como mencionado, provavelmente você vai querer substituir o sistema de gerenciamento de usuários de teste por algo que vincule os participantes da conversa aos seus próprios usuários autenticados. Com a conversa criada, você pode enviar e receber mensagens, receber chamadas e configurar conferências de áudio.

Leia mais sobre o Client SDK do Nexmo para descobrir o que você pode fazer a seguir:

Compartilhar:

https://a.storyblok.com/f/270183/250x250/f231d97f1b/garann-means.png
Garann MeansFormador de Desenvolvedores

Sou desenvolvedor de JavaScript e instrutor de desenvolvimento na Vonage. Ao longo dos anos, tenho me interessado muito por modelos, Node.js, aplicativos web progressivos e estratégias “offline-first”, mas o que sempre adorei de verdade é uma API útil e bem documentada. Meu objetivo é tornar a sua experiência com nossas APIs a melhor possível.