https://a.storyblok.com/f/270183/47741/e0990d5362/blog_slack-clone_1200x600.png

Criando um clone do Slack usando Vue – Parte 1

Publicado em April 26, 2021

Tempo de leitura: 69 minutos

Criação de uma aplicação de bate-papo em Vue.js semelhante ao Slack

Você já pensou em criar um aplicativo de bate-papo, mas ficou em dúvida sobre quais recursos incluir ou simplesmente sobre como desenvolvê-lo? Nesta postagem, você vai criar um clone do software de bate-papo favorito de todo mundo, o Slack. Usando o Vue.js, o framework favorito de todo mundo. E a Conversation API da Vonage, o serviço de conversação favorito de todo mundo.

Esta postagem é a primeira parte de uma série de tutoriais que vai nos levar de um diretório vazio até uma aplicação real, com muitos dos recursos que definem o gênero do Slack.

Aqui estão algumas das coisas que você aprenderá neste post:

Se você estiver interessado no aplicativo de demonstração completo, sem seguir o guia, confira o repositório no GitHub do meu clone do Slack em Vue.js até o momento.

Pré-requisitos

Node e NPM

Para começar, você precisará ter o Node e o NPM instalados. Este guia utiliza o Node 8 e o NPM 6. Verifique se eles estão instalados e atualizados.

node --version npm --version

Tanto o Node quanto o NPM precisam estar instalados e na versão correta. Acesse nodejs.org, baixe e instale a versão correta, caso ainda não a tenha.

Nossa CLI

Para configurar seu aplicativo, você precisará instalar nossa CLI. Instale-a usando o NPM no terminal.

npm install -g nexmo-cli@beta

Você pode verificar se está usando a versão correta com este comando. No momento em que escrevi este artigo, eu estava usando a versão 0.4.9-beta-3.

nexmo --version

Para acompanhar as etapas deste artigo, lembre-se de criar um Account gratuito na Vonage e configurar a CLI com a chave e o segredo da API encontrados no seu painel de controle.

nexmo setup

CLI do Express.js

Instalar Express Generator. Você usará essa biblioteca para gerar um servidor Express.js básico.

npm install -g express-generator

Você pode verificar se está usando a versão correta com este comando. No momento em que escrevi este artigo, eu estava usando a versão 4.16.1.

express --version

CLI do Vue.js

Instale o Vue CLI. Você usará essa biblioteca para gerar um aplicativo cliente básico em Vue.js.

npm install -g @vue/cli

Você pode verificar se está usando a versão correta com este comando. No momento em que escrevi este artigo, eu estava usando a versão 4.1.2 da @vue/cli.

vue --version

Começando do zero

Esta série vai guiá-lo desde um diretório vazio até a criação de um aplicativo de bate-papo real, utilizando o Express.js como servidor.

Criar uma pasta de projeto

Antes de mais nada, crie uma pasta para o seu trabalho.

mkdir vuejs-slack-clone

E, mude para o novo diretório.

cd vuejs-slack-clone

Gerar um servidor Express.js

Em seguida, crie um servidor básico usando o gerador do Express.js. O que eu adoro nessa CLI é que ela configura o executável do servidor e o aplicativo de forma independente um do outro. Ou seja, ela segue a filosofia do extremamente leve e bacana Express Hello World. Ele divide isso em um arquivo executável igualmente bacana para configurar o servidor e o ambiente bin/wwwe o próprio aplicativo app.js.

Como o aplicativo é, em sua essência, uma API, é melhor evitar a instalação de qualquer componente destinado ao gerenciamento de arquivos de modelo. Para isso, use a --no-view opção.

Se você pretende usar git como seu sistema de controle de versão, você deve considerar o uso --git para gerar o .gitignore arquivo.

Como você já está no diretório do projeto, especifique a --force opção e use . como diretório. Assim, a ferramenta irá gerar o aplicativo no diretório atual sem problemas.

express --git --no-view --force .

Em seguida, instale as dependências.

npm install

Executar o servidor Express.js localmente

Depois que o servidor for criado e as dependências instaladas, você pode iniciá-lo para verificar se tudo está funcionando conforme o esperado.

npm start

Você pode verificar se está funcionando na URL padrão, localhost:3000.

Screenshot of a basic Express.js server running

Rotas e controladores

O aplicativo gerado inclui o roteamento necessário. O roteamento se refere à determinação de como um aplicativo lida com uma solicitação direcionada a uma URL e um método específicos (GET, POST etc.). Os controladores, por outro lado, são responsáveis pelo fluxo de execução do aplicativo. O aplicativo gerado não cria controladores e utiliza os roteadores para retornar uma resposta.

Crie um novo diretório para os controladores.

# mkdir is a command that makes a directory mkdir controllers

Crie um novo controlador neste diretório com o nome server.js.

# touch is a command that will create an empty file touch controllers/server.js

Abrir controllers/server.js e crie o primeiro método para o servidor.

// controllers/server.js
exports.status = function(req, res, next) {
  res.json({
    status: 'ok'
  });
};

Esse controlador poderia, posteriormente, ser responsável por fornecer ao cliente uma condição, com base em várias verificações, como se o serviço de chat está em funcionamento ou se é possível se conectar aos dados. A ideia é que, caso ocorra algum problema no servidor, o cliente receba a mensagem de erro, lide com ela de maneira adequada e informe ao usuário o que aconteceu.

Para solicitar esse método do controlador, crie uma nova rota no diretório de rotas existente com o nome server.js.

touch routes/server.js

Abra routes/server.js e adicione o código mostrado abaixo.

// routes/server.js
var express = require('express');
var router = express.Router();

var serverController = require('../controllers/server');

router.get('/status', serverController.status);

module.exports = router;

Isso direciona uma rota (/status) para um método do controlador (serverController.status). A rota entrega o resultado do método do controlador ao cliente como uma resposta.

Para adicionar essa rota ao aplicativo, você precisa editar app.js e fazer essas alterações.

// app.js
- var indexRouter = require('./routes/index');
- var usersRouter = require('./routes/users');

...

- app.use('/', indexRouter);
- app.use('/users', usersRouter);
+ app.use('/api/server', require('./routes/server'));

Então você pode prosseguir e excluir o routes/index.js e routes/users.js arquivos.

Reinicie o aplicativo com npm start; em seguida, você poderá acessar a nova rota em localhost:3000/api/server/status.

Screenshot of a basic server status API endpoint

Criação de um cliente

Use o Vue CLI para criar um novo aplicativo cliente.

Gerar um cliente Vue.js

Execute o comando `create` com o Vue CLI. Essa ferramenta gera um aplicativo Vue simples que servirá de base para o nosso cliente de bate-papo. Ela exibe algumas opções, e você pode selecionar as predefinidas.

vue create client

O cliente é gerado no client diretório especificado no comando. Ele também é executado npm install automaticamente.

Agora, vá para o client diretório.

cd client

Para executar o cliente, use este comando. Observe que é diferente da forma como você executa o servidor.

npm run serve

Então, você pode acessar seu cliente em localhost:8080. Você vai perceber que ele usa uma porta diferente por padrão e, no ambiente de desenvolvimento, isso nos ajuda, como você verá a seguir, quando executarmos o servidor e o cliente simultaneamente.

Screenshot of a basic Vue.js client running

Recarregamento dinâmico dos arquivos do servidor Express.js

Normalmente, no processo de desenvolvimento, a maioria das pessoas prefere que o aplicativo atualize automaticamente os arquivos à medida que são editados. Para isso, vamos configurar o servidor para usar o o nodemon para servir os arquivos.

Instalar o Nodemon

Se você ainda estiver no client diretório anterior, você pode voltar ao diretório principal do projeto subindo um nível com este comando, .. que indica um diretório pai.

cd ..

Agora, instale o Nodemon como uma dependência de desenvolvimento. Para instalar uma dependência de desenvolvimento, basta adicionar --save-dev como uma opção do comando.

npm install nodemon --save-dev

Após a instalação, você pode editar o package.json arquivo e modificar o start script conforme mostrado aqui.

+     "dev:server": "nodemon ./bin/www",
      "start": "node ./bin/www"

Quando você executar o aplicativo com npm run dev:server, ele utilizará o Nodemon. O Nodemon monitora os arquivos do aplicativo e reinicia o serviço automaticamente sempre que algum arquivo for alterado.

Observação: Isso também inclui metadados do arquivo, como permissões e data de modificação.

Execute o servidor e o cliente simultaneamente

À medida que avançarmos neste guia, você precisará executar o cliente e o Express.js simultaneamente. Há um pacote Concurrently para isso, que facilita muito a integração de Applications distintas entre si.

Instalar simultaneamente

Instale o Concurrently, também como dependência de desenvolvimento.

npm install concurrently --save-dev

Inicie os dois ambientes de desenvolvimento

Modifique o package.json arquivo do servidor, conforme mostrado aqui. Na última seção, adicionamos um dev:server script que executava o servidor usando o Nodemon. Agora, vamos adicionar um dev:client script na raiz do projeto para executar o cliente a partir daqui também.

      "dev:server": "nodemon ./bin/www",
+     "dev:client": "cd client && npm run serve",
      "start": "node ./bin/www"

Agora, adicione esta linha para combinar os dois usando o Concurrently. Você notará a opção --kill-others-on-fail que significa que o Concurrently interromperá todos os serviços caso seja detectado um erro grave. Sem isso, se o Node ou o Webpack (que atende ao cliente) encontrasse um erro, você precisaria reiniciar o Concurrently para que tanto o cliente quanto o servidor voltassem a funcionar.

      "dev:server": "nodemon ./bin/www",
      "dev:client": "cd client && npm run serve",
+     "dev": "concurrently --kill-others-on-fail 'npm run dev:server' 'npm run dev:client'",
      "start": "node ./bin/www"

Ao executar o aplicativo com npm run dev, ele iniciará o servidor e o cliente simultaneamente em localhost:3000 e localhost:8080 , respectivamente.

Screenshot of a Express.js and Vue.js running concurrently

Solicitações de API por proxy ao servidor Express.js

Para enviar solicitações do cliente ao servidor no ambiente de desenvolvimento, você precisará configurar um proxy. É possível configurar o Vue.js para redirecionar por meio de um proxy quaisquer solicitações que comecem com uma rota específica.

Configurar o proxy

Para isso, crie um novo arquivo dentro do client diretório chamado vue.config.js. Portanto, acesse o diretório do cliente.

cd client

Crie um arquivo de configuração vazio.

# touch is a command that will create an empty file touch vue.config.js

Cole o código a seguir.

// vue.config.js

module.exports = {
  devServer: {
    proxy: {
      "/api": {
        target: "http://localhost:3000",
        secure: false
      }
    }
  }
};

Esse código informa ao Vue.js que, ao ser executado, devServer que qualquer rota que corresponda a /api devem ser redirecionadas para http://localhost:3000. Essa é a URL do servidor ao executar o dev script ou o dev:server script diretamente.

Criar um serviço consumidor de API

Para enviar solicitações do Vue.js ao nosso servidor a partir do cliente, instale o Axios, que é uma cliente HTTP , para uso em código do lado do navegador.

npm install axios

Agora que você já instalou o Axios e pode encaminhar solicitações entre o servidor e o cliente, é hora de fazer essas solicitações. No diretório src/ , crie uma nova pasta chamada services para conter todos os arquivos do serviço de API.

mkdir src/services

Crie um serviço de API abstrato, que definirá o caminho para os serviços de API subsequentes. Lembre-se de que, no ambiente de desenvolvimento, /api vai atuar como proxy para o servidor.

touch src/services/Api.js

Use o código a seguir para criar um serviço de API abstrato que retorne uma instância do Axios.

// src/services/Api.js

import axios from 'axios'

export default() => {
  return axios.create({
    baseURL: `/api`,
    headers: {'Cache-Control': 'no-cache, no-store, no-transform'}
  })
}

Você já criou um server/status ponto de extremidade no servidor, que, quando o servidor estava em execução, você podia acessar por meio de localhost:3000/api/server/status.

Para acessar esse endpoint a partir do aplicativo cliente, crie um arquivo para o serviço.

touch src/services/Server.js

E adicione este código para criar um fetchStatus método no novo Server serviço.

// src/services/Server.js

import Api from '@/services/Api'

export default {
  fetchStatus () {
    return Api().get('server/status')
  }
}

Solicitar o status do servidor no cliente

Agora que você criou um serviço para fazer solicitações ao servidor, importe o serviço para o seu App.vue componente.

Abrir App.vue e adicione as linhas conforme mostrado aqui.

  <template>
    <div id="app">
      <img alt="Vue logo" src="./assets/logo.png">
-     <HelloWorld msg="Welcome to Your Vue.js App"/>
+     <HelloWorld v-if="!!server.status && server.status === 'ok'" msg="Welcome to Your Vue.js App"/>
+     <template v-else>
+       <HelloWorld msg="Connecting..."/>
+     </template>
    </div>
  </template>

  <script>
  import HelloWorld from './components/HelloWorld.vue'
+ import ServerService from '@/services/Server'

  export default {
    name: 'App',
    components: {
      HelloWorld
+   },
+   data () {
+     return {
+       server: {},
+     }
+   },
+   mounted () {
+     this.getServerStatus()
+   },
+   methods: {
+     getServerStatus () {
+       ServerService.fetchStatus()
+         .then((response) => {
+           this.server = response.data
+         })
+     }
    }
  }
  </script>

# ...

Aqui, ele reutiliza o HelloWorld para exibir o status da solicitação ao usuário.

Observação: Lembre-se de que, provavelmente, você ainda está no diretório do cliente nesta etapa. Para iniciar (ou reiniciar) o ambiente de desenvolvimento novamente, é necessário npm run dev deve ser feito no diretório do servidor (cd .. para passar do cliente para o servidor).

Assim que estiver em execução, você poderá acessar o cliente em localhost:8080. Se você for rápido o suficiente, poderá ver a mensagem “Conectando...”.

Screenshot of the Vue.js client connecting to the Express.js server

Telas de carregamento com Tailwind e FontAwesome

Ao se conectar ao servidor na última seção, você terá reutilizado o HelloWorld . Agora, usando o Tailwind CSS e o FontAwesome, crie uma tela de carregamento para o cliente.

Se você quiser praticar isso fora deste aplicativo, escrevi sobre Como usar o Tailwind CSS com o Vue.js em um guia separado, feito especialmente para você.

Instalar o Tailwind CSS

Para usar o Tailwind CSS no cliente, precisamos instalá-lo como uma dependência e configurar o cliente para utilizá-lo.

Observação: Esta instalação é para o cliente, não para o servidor. Portanto, certifique-se de estar no client diretório.

npm install tailwindcss

Configurar o cliente Vue.js para o Tailwind CSS

Quando o aplicativo cliente é compilado, ele procura por um postcss.config.js arquivo de configuração que o Vue.js usa para saber como processar o CSS. As instruções de instalação do Tailwind CSS indicam que você deve adicioná-lo como um plug-in na sua cadeia de compilação.

O aplicativo de demonstração gerado pelo Vue não cria um postcss.config.js arquivo. Faça isso agora.

touch postcss.config.js

E configure-o usando este código.

// postcss.config.js

const autoprefixer = require('autoprefixer');
const tailwindcss = require('tailwindcss');

module.exports = {
  plugins: [
    tailwindcss,
    autoprefixer,
  ],
};

Adicionar o Tailwind como um recurso CSS

O aplicativo de demonstração também não cria nenhum recurso CSS. Em vez disso, ele usa CSS dentro dos componentes do Vue.js, como mostram muitos guias. Portanto, para incluir o Tailwind, crie um arquivo CSS básico dentro do diretório `assets` usando esses comandos ou seu editor.

mkdir -p src/assets/styles/ touch src/assets/styles/index.css

Use este código para incluir a base, os componentes e os utilitários do Tailwind CSS na sua compilação de CSS. Copie e cole-o no seu novo index.css arquivo.

/* src/assets/styles/index.css */

@tailwind base;
@tailwind components;
@tailwind utilities;

Incluir o Tailwind CSS

Agora edite seu main.js arquivo para importar index.css para o cliente.

  // src/main.js
  import Vue from 'vue';
  import App from './App.vue';

+ import './assets/styles/index.css';

  Vue.config.productionTip = false;

  new Vue({
    render: h => h(App),
  }).$mount(`#app`);

Screenshot of the Vue.js client styles after Tailwind CSS preflight enabled

Observação: O Tailwind CSS usa o preflight (baseado no normalize.css) para redefinir todos os estilos em diferentes navegadores para o mesmo padrão. Você vai perceber que isso alterou alguns estilos padrão. Não se preocupe; você vai substituir tudo isso em breve.

Instalar o FontAwesome

A criação de um indicador de carregamento será feita com um círculo com entalhes do Font Awesome. Instale-o no cliente com este comando.

npm install @fortawesome/fontawesome-svg-core \ @fortawesome/free-solid-svg-icons \ @fortawesome/vue-fontawesome \ @fortawesome/free-regular-svg-icons \

Incluir o FontAwesome

Editar main.js novamente e adicione este código.

  // src/main.js
  import Vue from 'vue';
  import App from './App.vue';
+ import { library } from '@fortawesome/fontawesome-svg-core'
+ import { fas } from '@fortawesome/free-solid-svg-icons'
+ import { far } from '@fortawesome/free-regular-svg-icons'
+ import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'

  import './assets/styles/index.css';

+ library.add(fas, far)

+ Vue.component('font-awesome-icon', FontAwesomeIcon)
+ Vue.component('font-awesome-layers', FontAwesomeLayers)

  Vue.config.productionTip = false;

  new Vue({
    render: h => h(App),
  }).$mount(`#app`);

Criar a tela de carregamento

Para criar um novo componente Vue.js para usar como tela de carregamento, adicione um novo arquivo de componente com este comando ou no seu editor.

touch source/components/Loading.vue

Agora, usando esse código, adicione o spinner a uma sobreposição translúcida em tela cheia.

<template>
  <div class="w-screen h-screen fixed block top-0 left-0 bg-white opacity-75 z-50 flex">
    <span class="text-green-500 opacity-75 top-1/2 m-auto text-center">
      <font-awesome-icon icon="circle-notch" class="fa-spin fa-5x mb-2"/>
      <p class="text-base">
        {{ message }}
      </p>
    </span>
  </div>
</template>

<script>
export default {
  name: 'Loading',
  props: {
    message: String
  }
}
</script>

E, adicione a tela de carregamento editando App.vue e substituindo a reutilização de HelloWorld.vue pelo novo componente.

  <template>
    <div id="app">
      <img alt="Vue logo" src="./assets/logo.png">
      <HelloWorld v-if="!!server.status && server.status === 'ok'" msg="Welcome to Your Vue.js App"/>
      <template v-else>
-       <HelloWorld msg="Connecting..."/>
+       <Loading message="Connecting..." />
      </template>
    </div>
  </template>

  <script>
  import HelloWorld from './components/HelloWorld.vue'
+ import Loading from '@/components/Loading.vue'
  import ServerService from '@/services/Server'

  export default {
    name: 'App',
    components: {
-     HelloWorld
+     HelloWorld,
+     Loading
    },
    data () {
      return {
        server: {},
      }
    },
    mounted () {
      this.getServerStatus()
    },
    methods: {
      getServerStatus () {
        ServerService.fetchStatus()
          .then((response) => {
            this.server = response.data
          })
      }
    }
  }
  </script>

  ...

Screenshot of the Vue.js client loading screen with spinner

Observação: Para testar isso, você pode modificar a resposta status no diretório controllers/server.js para retornar algo diferente de ok.

Como lidar com erros do servidor no cliente

É hora de adicionar o tratamento de erros ao cliente.

Erros na captura de solicitações

Editar App.vue e adicione o código a seguir.

...

  <script>
  import HelloWorld from './components/HelloWorld.vue'
  import Loading from '@/components/Loading.vue'
  import ServerService from '@/services/Server'

  export default {
    name: 'App',
    components: {
      HelloWorld,
      Loading
    },
    data () {
      return {
        server: {},
+       error: null
      }
    },
    mounted () {
      this.getServerStatus()
    },
    methods: {
      getServerStatus () {
        ServerService.fetchStatus()
          .then((response) => {
            this.server = response.data
          })
+         .catch((err) => {
+           this.error = { title: 'Couldn\'t connect to Server', message: 'There may be a problem with your connection. Please check and try again.', reason: err.reason }
+         })
      }
    }
  }
  </script>

  ...

Agora, se houver um erro proveniente do servidor, ele será detectado pelo cliente e adicionado aos dados do componente.

Criar um componente de erro

Para exibir um erro, crie um componente Error.vue componente vazio usando este comando ou seu editor.

touch source/components/Error.vue

Adicione este código, que também utiliza ícones (e camadas) do FontAwesome para gerar um gráfico adequado.

<template>
  <div class="flex h-screen">
    <div class="m-auto text-center w-2/3">
      <font-awesome-layers class="fa-10x mb-10">
        <font-awesome-icon icon="globe-americas" transform="grow-4" class="text-gray-500"/>
        <font-awesome-icon :icon="['far', 'circle']" transform="grow-5" class="outline text-white"/>
        <font-awesome-icon icon="times" class="cross text-red-500" transform="shrink-8 right-5 up-5"/>
      </font-awesome-layers>
      <h1 class="text-3xl mb-3 text-gray-800">{{ error.title }}</h1>
      <p class="text-base text-gray-800">{{ error.message }}</p>
      <p class="invisible">{{ error.reason }}</p>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Error',
  props: {
    error: Object
  }
}
</script>

<style scoped>
.outline path {
  stroke: white;
  stroke-width: 20px;
}
.cross path {
  stroke: white;
  stroke-width: 20px;
}
</style>

Exibir um erro do servidor no cliente

Mais uma vez, ao editar App.vue, adicione o código conforme mostrado aqui. Remova a imagem ao mesmo tempo.

  <template>
    <div id="app">
-     <img alt="Vue logo" src="./assets/logo.png">
      <HelloWorld v-if="!!server.status && server.status === 'ok'" msg="Welcome to Your Vue.js App"/>
      <template v-else>
-       <Loading message="Connecting..." />
+       <Loading v-if="!error" message="Connecting..." />
+       <Error v-else :error="error" />
      </template>
    </div>
  </template>

  <script>
  import HelloWorld from './components/HelloWorld.vue'
+ import Error from '@/components/Error.vue'
  import Loading from '@/components/Loading.vue'
  import ServerService from '@/services/Server'

  export default {
    name: 'App',
    components: {
      HelloWorld,
+     Error,
      Loading
    },
    data () {
      return {
        server: {},
        error: null
      }
    },
    mounted () {
      this.getServerStatus()
    },
    methods: {
      getServerStatus () {
        ServerService.fetchStatus()
          .then((response) => {
            this.server = response.data
          })
          .catch((err) => {
            this.error = { title: 'Couldn\'t connect to Server', message: 'There may be a problem with your connection. Please check and try again.', reason: err.reason }
          })
      }
    }
  }
  </script>

  ...

Agora, o cliente exibe os erros enviados pelo servidor.

Screenshot of the Vue.js client catching a server error

Observação: Para ver isso em funcionamento, você pode modificar o aplicativo do seu servidor controllers/server.js, substituindo res.json por res.sendStatus(500) para fornecer ao cliente um código de erro 500.

Usar arquivos de ambiente Dotenv

Você não deve armazenar chaves e credenciais de forma estática no seu servidor, mas, principalmente, não no seu cliente.

Instalar o Dotenv

Instale dotenv para que você possa definir variáveis de ambiente e lê-las em seu aplicativo.

Observação: Você pode estar no diretório do cliente nesta etapa. Use cd .. para passar do diretório do cliente para o do servidor.

npm install dotenv

Criar um arquivo de ambiente

Crie um arquivo de configuração vazio para o servidor usando este comando ou seu editor.

touch .env

Configurar o ambiente

Agora, edite .env e adicione esta configuração de exemplo ao arquivo. O token e o ID não são reais.

# server config PORT=3000 # user config VONAGE_USER=username VONAGE_USER_TOKEN=eyJhbGciOiJ.SUzI1NiIsInR.5cCI6IkpXVCJ9 # app config VONAGE_DEFAULT_CONVERSATION_ID=CON-1255bc-1c-4db7-bc48-15a46

Observação: .env os arquivos são ignorados pelo Git devido ao .gitignore adição de arquivos .env por padrão. Fazer o commit do seu .env arquivo é tão seguro quanto codificar suas credenciais diretamente no código. Isso também ressalta que essas credenciais são específicas para este ambiente, que está sendo executado localmente. Se você for fazer a implantação, será necessário gerenciar o ambiente no servidor de uma maneira diferente. O Heroku, por exemplo, oferece um painel de controle para configurar o ambiente.

Carregar o ambiente

Agora, edite o arquivo principal do servidor para incluir o ambiente no momento em que o aplicativo for iniciado. Edite bin/www (ele não tem extensão de arquivo) conforme mostrado aqui.

#!/usr/bin/env node

+ require('dotenv').config();

  /**
  * Module dependencies.
  */

  ...

Passar valores do ambiente do servidor para o cliente

A primeira variável de ambiente a ser compartilhada com o cliente é VONAGE_DEFAULT_CONVERSATION_ID, o ID padrão da “sala” para o chat! Você voltará aqui mais tarde para editar o valor dessa variável de ambiente.

Editar controllers/server.js e adicione o código mostrado aqui.

// controllers/server.js
  exports.status = function(req, res, next) {
    res.json({
+     defaultConversationId: process.env.VONAGE_DEFAULT_CONVERSATION_ID,
      status: 'ok'
    });
  };

Terminais de usuário para autenticação de clientes

Nas próximas partes desta série, um provedor de identidade irá gerenciar os dados do usuário enviados pelo servidor. Enquanto isso, simule essas informações também e volte para editá-las quando tiver os dados reais.

Criar um endpoint de usuário

Crie um endpoint de usuário criando primeiro um user.js controlador usando seu editor ou este comando.

touch controllers/user.js

Inserindo esse código.

// controllers/user.js
exports.session = function(req, res, next) {
  res.json({
    user: process.env.VONAGE_USER,
    token: process.env.VONAGE_USER_TOKEN
  });
};

Agora, crie uma rota para acessar os endpoints do controlador de usuário usando seu editor ou este comando.

touch routes/user.js

E insira este código.

// routes/user.js
const express = require('express');
const router = express.Router();

const userController = require('../controllers/user');

router.get('/session', userController.session);

module.exports = router;

Por fim, edite seu app.js arquivo e adicione a nova rota conforme mostrado aqui.

// app.js
  var express = require('express');
  var path = require('path');
  var cookieParser = require('cookie-parser');
  var logger = require('morgan');

  var app = express();

  app.use(logger('dev'));
  app.use(express.json());
  app.use(express.urlencoded({ extended: false }));
  app.use(cookieParser());
  app.use(express.static(path.join(__dirname, 'public')));

+ app.use('/api/user', require('./routes/user'));
  app.use('/api/server', require('./routes/server'));

  module.exports = app;

Reinicie o aplicativo com npm start; em seguida, você poderá acessar a nova rota em localhost:3000/api/user/session.

Screenshot of a user session API endpoint

Conectar-se à Conversation API da Vonage

Nesta seção, o que se segue são as etapas habituais, caso você já tenha lido algum dos meus tutoriais sobre o lado do cliente. Caso contrário, estes são comandos simples para criar nossa conversa no Vonage, à qual os usuários poderão participar.

Configure com nossa CLI

Para se conectar à Conversation API como usuário, primeiro é preciso criar um aplicativo, uma conversa e um usuário.

Criar um aplicativo

Crie uma aplicação com recursos de RTC (comunicação em tempo real). A URL de eventos recebe um registro em tempo real dos eventos que ocorrem no serviço, como usuários entrando ou saindo e enviando mensagens. Por enquanto, trata-se de uma URL de exemplo, mas você poderá capturar e reagir a eventos nas próximas partes da nossa série.

nexmo app:create "Vue.js Slack Chat" --capabilities=rtc --rtc-event-url=http://example.com --keyfile=private.key # Application created: 4556dbae-bf...f6e33350d8 # Credentials written to .nexmo-app # Private Key saved to: private.key

Criar uma conversa

Em segundo lugar, crie uma conversa, que funcione como uma sala de bate-papo. Ou seja, um espaço para mensagens e eventos.

nexmo conversation:create display_name="general" # Conversation created: CON-a57b0...11e57f56d

Crie sua conta

Agora, crie uma conta para você mesmo.

Observação: Nesta demonstração, não haverá bate-papo entre dois usuários. Outros guias mostram como criar conversas entre vários usuários. Este guia se concentra em definir o estilo da interface de usuário das mensagens de maneira simples, mas atraente.

nexmo user:create name=USER_NAME display_name=DISPLAY_NAME # User created: USR-6eaa4...e36b8a47f

Adicionar o usuário a uma conversa

Em seguida, adicione seu novo usuário à conversa. Um usuário pode ser membro de uma Application, mas ainda assim precisa participar da conversa.

nexmo member:add CONVERSATION_ID action=join channel='{"type":"app"}' user_id=USER_ID # Member added: MEM-df772...1ad7fa06

Gerar um token de usuário

Por fim, gere um token para o seu novo usuário. Esse token representa o usuário ao acessar o aplicativo. Esse token de acesso identifica o usuário; portanto, qualquer pessoa que o utilizar será considerada o usuário correto.

Na prática, você configurará o aplicativo com esse token. Em ambiente de produção, esses tokens devem ser protegidos, mantidos em sigilo e expostos com muito cuidado ao aplicativo cliente, se for o caso.

O token só pode ser usado por 24 horas. Depois disso, você precisará executar novamente este nexmo jwt:generate comando para conceder acesso ao seu usuário cliente novamente.

nexmo jwt:generate ./private.key sub=USER_NAME exp=$(($(date +%s)+86400)) acl='{ "paths": { "/*/users/**": {}, "/*/conversations/**": {}, "/*/sessions/**": {}, "/*/devices/**": {}, "/*/image/**": {}, "/*/media/**": {}, "/*/push/**": {}, "/*/knocking/**": {} } }' application_id=APPLICATION_ID # eyJhbGciOi...XVCJ9.eyJpYXQiOjE1NzM5M...In0.qn7J6...efWBpemaCDC7HtqA

Armazene as credenciais no ambiente

Agora, edite .env e adicione as credenciais que você acabou de gerar.

# server config PORT=3000 # user config VONAGE_USER=username # USER_NAME from the above commands VONAGE_USER_TOKEN=eyJhbGciOi...XVCJ9.eyJpYXQiOjE1NzM5M...In0.qn7J6...efWBpemaCDC7HtqA # as generated from `nexmo jwt:generate` # app config VONAGE_DEFAULT_CONVERSATION_ID=CON-a57b0...11e57f56d # as generated from `nexmo conversation:create`

Criar um serviço para a sessão do usuário

Crie um User.js serviço para acessar o endpoint da sessão do usuário a partir do aplicativo cliente.

# back in the client directory cd client

Crie o arquivo usando este comando ou seu editor.

touch src/services/User.js

E adicione este código para criar um fetchSession método no novo User serviço.

// src/services/User.js

import Api from '@/services/Api'

export default {
  fetchSession () {
    return Api().get('user/session')
  }
}

Conecte o cliente à Conversation API

Para conectar o cliente à Conversation API, é necessário instalar a versão mais recente do nexmo-client.

npm install nexmo-client

Crie um novo Vonage.vue componente usando seu editor ou o comando abaixo, que terá a função de se conectar à Conversation API usando a nexmo-client biblioteca.

touch src/components/Vonage.vue

Semelhante ao App.vue componente, o Vonage.vue componente solicita informações da sessão do usuário ao servidor, utilizando o Loading.vue e Error.vue da mesma forma.

<template>
  <div>
    <HelloWorld v-if="!!app && !!conversation" msg="Welcome to Your Vue.js App"/>
    <template v-else>
      <Loading v-if="!error" message="Logging you in..." />
      <Error v-else :error="error" />
    </template>
  </div>
</template>

<script>
import HelloWorld from '@/components/HelloWorld.vue'
import Loading from '@/components/Loading.vue'
import Error from '@/components/Error.vue'
import UserService from '@/services/User'
import Client from 'nexmo-client'

export default {
  name: 'Vonage',
  props: {
    server: Object
  },
  components: {
    ChatWindow,
    Error,
    Loading
  },
  data () {
    return {
      app: null,
      conversation: null,
      error: null
    }
  },
  mounted () {
    this.fetchSession()
  },
  methods: {
    _errorHandler (err) {
      this.error = { title: 'Chat Service Error', message: err.reason }
    },
    fetchSession () {
      UserService.fetchSession()
        .then((response) => {
          const { token } = response.data

          new Client()
            .createSession(token)
            .then(app => {
              this.app = app

              return app.getConversation(this.$props.server.defaultConversationId)
            })
            .then((conversation) => {
              this.conversation = conversation
            })
            .catch(this._errorHandler)
        })
        .catch(this._errorHandler)
    }
  }
}
</script>

Agora, substitua o uso do HelloWorld.vue pelo novo Vonage.vue componente dentro App.vue fazendo essas alterações.

  <template>
    <div id="app">
-     <HelloWorld v-if="!!server.status && server.status === 'ok'" msg="Welcome to Your Vue.js App"/>
+     <Vonage v-if="!!server.status && server.status === 'ok'" :server="server" />
      <template v-else>
        <Loading v-if="!error" message="Connecting..." />
        <Error v-else :error="error" />
      </template>
    </div>
  </template>

  <script>
- import HelloWorld from './components/HelloWorld.vue'
+ import Vonage from '@/components/Vonage.vue'
  import Error from '@/components/Error.vue'
  import Loading from '@/components/Loading.vue'
  import ServerService from '@/services/Server'

  export default {
    name: 'App',
    components: {
-     HelloWorld,
+     Vonage,
      Error,
      Loading
    },
    data () {
      return {
        server: {},
        error: null
      }
    },
    mounted () {
      this.getServerStatus()
    },
    methods: {
      getServerStatus () {
        ServerService.fetchStatus()
          .then((response) => {
            this.server = response.data
          })
          .catch((err) => {
            this.error = { title: 'Couldn\'t connect to Server', message: 'There may be a problem with your connection. Please check and try again.', reason: err.reason }
          })
      }
    }
  }
  </script>

Agora, após a tela de carregamento “Conectando...”, você verá uma tela de carregamento “Fazendo seu login...” antes que o HelloWorld.vue componente.

Screenshot of client logging into the Conversation API

Observação: Você só verá o “Hello World” se seu aplicativo tiver se conectado com sucesso ao servidor, recebido o status “OK”, solicitado a sessão do usuário e, em seguida, usado o token do usuário para se conectar à Conversation API por meio da nexmo-client biblioteca.

Criar os componentes do chat

Agora que você está conectado à Conversation API, pode começar a criar sua interface de mensagens. Primeiro, comece pela estrutura básica do seu aplicativo: a janela de bate-papo.

Janela de bate-papo

Para isso, crie os componentes ChatWindow.vue, ChatWindowHeader.vue, ChatWindowEvents.vuee ChatWindowFooter.vue usando o comando ou seu editor.

touch src/components/{ChatWindow,ChatWindowHeader,ChatWindowEvents,ChatWindowFooter}.vue

Edição ChatWindow.vue, insira o seguinte código.

<template>
  <div class="flex flex-col min-h-screen max-h-screen bg-white overflow-hidden">
    <ChatWindowHeader :channelName="'#' + conversation.display_name"/>
    <ChatWindowEvents :conversation="conversation" :user="user" :members="members" />
    <ChatWindowFooter :conversation="conversation" />
  </div>
</template>

<script>
import ChatWindowHeader from '@/components/ChatWindowHeader.vue'
import ChatWindowEvents from '@/components/ChatWindowEvents.vue'
import ChatWindowFooter from '@/components/ChatWindowFooter.vue'

export default {
  name: 'ChatWindow',
  props: {
    app: Object,
    conversation: Object
  },
  components: {
    ChatWindowHeader,
    ChatWindowEvents,
    ChatWindowFooter
  },
  data () {
    return {
      user: {},
      members: new Map(),
    }
  },
  mounted () {
    this.user = this.$props.app.me
    this.fetchMembers()
  },
  methods: {
    fetchMembers () {
      this.members = this.$props.conversation.members
    }
  }
}
</script>

O ChatWindow.vue componente é responsável por estruturar o layout do chat. Cabeçalho na parte superior, mensagens no meio e rodapé na parte inferior. Ele passa o nome do canal, precedido por um hash, como o channelName prop para o cabeçalho. Ele também repassa a conversa, o usuário e os participantes para o componente de eventos. Em seguida, repassa a conversa para o rodapé.

Em seguida, edite ChatWindowHeader.vue e insira este código.

<template>
  <div class="border-b flex px-6 py-2 items-center">
    <div class="flex flex-col">
      <h4 class="text-grey-darkest mb-1 font-extrabold">{{ channelName }}</h4>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ChatWindowHeader',
  props: {
    channelName: String,
    members: Number
  }
}
</script>

O ChatWindowHeader.vue componente, por enquanto, apenas exibe o nome do canal.

Agora, edite ChatWindowEvents.vue e insira este código.

<template>
  <div class="py-4 flex-auto overflow-y-auto" ref="chatWindow">
    <template v-if="!!events.length">
      <div class="px-6 hover:bg-gray-100" v-for="event in events" v-bind:key="'event' + event.id">
        <div v-if="event.type === 'text'">
          <strong>{{ members.get(event.from).display_name }}</strong> on <strong>{{ event.timestamp.split("T")[0] }}</strong> at <strong>{{ event.timestamp.split("T")[1].split(".")[0] }}</strong> says {{ event.body.text }}
        </div>
        <div v-else-if="event.type === 'member:joined'">
          <strong>{{ event.body.user.display_name }}</strong> has joined <strong>#{{ event.conversation.display_name }}</strong>.
        </div>
      </div>
    </template>
    <Loading v-else message="Loading messages..." />
    <Error v-else :error="error" />
  </div>
</template>

<script>
import Loading from '@/components/Loading.vue'
import Error from '@/components/Error.vue'

export default {
  name: 'ChatWindowEvents',
  components: {
    Loading,
    Error
  },
  props: {
    user: Object,
    conversation: Object,
    members: Map,
  },
  data () {
    return {
      events: [],
      error: null
    }
  },
  mounted () {
    this.getEventHistory()
    this.registerListeners()
  },
  methods: {
    registerListeners () {
      const { conversation } = this.$props

      conversation.on('text', (user, event) => {
        this.events.push(event)
      })

      conversation.on("member:joined", (user, event) => {
        this.events.push(event)
      })
    },
    getEventHistory () {
      this.$props.conversation
        .getEvents({ page_size: 40, order: 'desc' })
        .then(eventsPage => {
          eventsPage.items.forEach(event => {
            this.events.unshift(event)
          })
        })
        .catch(err => {
          this.error = { title: 'Chat Service Error', message: err.message }
        })
    },
  },
}
</script>

O ChatWindowEvents.vue componente é responsável por listar todos os eventos da conversa. Ele faz isso de cima para baixo, com os eventos mais antigos aparecendo na parte superior da janela. Role a tela para baixo para ver as mensagens mais recentes. Ele carrega um total de 40 mensagens. Mais adiante nesta série, você verá como carregar mensagens mais antigas.

Por fim, edite ChatWindowFooter.vue e insira este código.

<template>
  <div class="px-4">
    <textarea
      v-bind:class="{ 
        'disabled:opacity-75': isSending,
        'bg-gray-300': isSending,
        'border-gray-400': isSending,
        'border-gray-400': !isSending
      }"
      v-bind:disabled="isSending"
      v-bind:value="inputMessage"
      v-on:input="inputMessage = $event.target.value"
      v-on:keydown.enter.exact.prevent
      v-on:keyup.enter.exact="sendMessage"
      v-on:keyup="typingEvents"
      type="text"
      :placeholder="'Message ' + conversation.display_name"
      class="w-full rounded border text-sm border-gray-700 overflow-hidden py-2 px-4 resize-none"
      rows="1"
      ref="inputBox"
    >
    </textarea>
    <div class="grid grid-cols-10 h-6 text-xs">
    </div>
  </div>
</template>

<script>
export default {
  name: 'ChatWindowFooter',
  props: {
    conversation: Object,
  },
  data () {
    return {
      inputMessage: '',
      isSending: false
    }
  },
  methods: {
    typingEvents () {
      this.resizeInput()
    },
    resizeInput () {
      const inputRows = this.inputMessage.split(/\r?\n/).length
      this.$refs.inputBox.rows = inputRows
    },
    sendMessage () {
      if (this.inputMessage.replace(/\s/g,'').length > 0) {
        this.isSending = true

        this.$props.conversation
          .sendText(this.inputMessage.trim())
          .then(() => {
            this.isSending = false
            this.$nextTick(() => {
              this.$refs.inputBox.focus()
              this.inputMessage = ''
              this.resizeInput()
            });
          })
          .catch(err => {
            console.error(err) // eslint-disable-line no-console
          })
      }
    }
  }
}
</script>

<style scoped>
textarea:focus{
  outline: none;
}
</style>

Depois de criar seus componentes, edite Vonage.vue e substitua HelloWorld.vue pelo seu novo ChatWindow.vue componente.

  <template>
    <div>
-     <HelloWorld v-if="!!app && !!conversation" msg="Welcome to Your Vue.js App" />
+     <ChatWindow v-if="!!app && !!conversation" :app="app" :conversation="conversation" />
      <template v-else>
        <Loading v-if="!error" message="Logging you in..." />
        <Error v-else :error="error" />
      </template>
    </div>
  </template>

  <script>
- import HelloWorld from '@/components/HelloWorld.vue'
+ import ChatWindow from '@/components/ChatWindow.vue'
  import Loading from '@/components/Loading.vue'
  import Error from '@/components/Error.vue'
  import UserService from '@/services/User'
  import VonageClient from 'nexmo-client'

  export default {
    name: 'Vonage',
    props: {
      server: Object
    },
    components: {
-     HelloWorld,
+     ChatWindow,
      Error,
      Loading
    },
    data () {
      return {
        app: null,
        conversation: null,
        error: null
      }
    },
    mounted () {
      this.fetchSession()
    },
    methods: {
      ...
    }
  }
  </script>

Tem bastante coisa para copiar e colar aqui. Depois de executar, veja como fica.

Screenshot of the chat client working

Observe a margem, que ficou como resquício do aplicativo de demonstração! Por fim, remova esse estilo editando src/App.vue desta forma.

  <template>
    <div id="app">
      <Vonage v-if="!!server.status && server.status === 'ok'" :server="server" />
      <template v-else>
        <Loading v-if="!error" message="Connecting..." />
        <Error v-else :error="error" />
      </template>
    </div>
  </template>

  <script>
  ...
  </script>
-
- <style>
- #app {
-   font-family: Avenir, Helvetica, Arial, sans-serif;
-   -webkit-font-smoothing: antialiased;
-   -moz-osx-font-smoothing: grayscale;
-   text-align: center;
-   color: #2c3e50;
-   margin-top: 60px;
- }
- </style>

Já que está nisso, apague HelloWorld.vue. Finalmente.

rm src/components/HelloWorld.vue

Screenshot of the chat client working beautifully

O chat já está funcionando!

Parte 1, concluída! Você criou um cliente de bate-papo que está começando a se parecer com o Slack. Aqui está uma lista do que você fez até agora:

  • Criei um aplicativo em Express.js para usar como API

  • Criei um aplicativo em Vue.js para usar como cliente

  • Criei pontos de extremidade da API no Express.js

  • Endpoints da API utilizados no Vue.js

  • Adicionada a recarga dinâmica de arquivos do Express.js

  • Adicionado simultaneamente ao Express.js e ao Vue.js com um único comando

  • Solicitações de API encaminhadas pelo Vue.js para o Express.js

  • Estilizando o Vue.js com o Tailwind CSS

  • Ícones animados com o FontAwesome

  • Criei um componente de carregamento em tela cheia

  • Conectado à Conversation API da Vonage

  • Criei uma interface de usuário para mensagens

Se você estiver interessado no aplicativo de demonstração completo, confira o repositório no GitHub do meu clone do Slack em Vue.js até o momento.

Fique ligado na parte 2, onde abordaremos os seguintes aspectos essenciais da experiência do usuário.

  • Histórico de rolagem infinita

  • Posições fixas ao percorrer o histórico

  • Ping para a parte inferior ao enviar mensagens

  • Notificações de mensagens não lidas

  • Botão “Marcar como lido”

  • Número de membros do canal

  • Exclusão de mensagens

  • Notificação de eventos de digitação do usuário (várias pessoas estão digitando)

  • Mensagens com várias linhas

  • Markdown no estilo do Slack

Ao final da Parte 2, você terá algo mais parecido com isto!

Screenshot of the sneak peek of chat from Part 2

Leitura complementar

Aqui estão mais alguns artigos que podem ser úteis na sua jornada para criar um aplicativo de bate-papo na web.

E não se esqueça: se tiver alguma dúvida, sugestão ou ideia que gostaria de compartilhar com a comunidade, sinta-se à vontade para participar do nosso espaço de trabalho da Comunidade no Slack 👇

Compartilhar:

https://a.storyblok.com/f/270183/250x250/451101b4f0/lukeoliff.png
Luke OliffEx-funcionários da Vonage

Educador de tecnologia com um jeito amigável, pai de família, defensor da diversidade, provavelmente discuto um pouco demais. Ex-engenheiro de backend. Fale comigo sobre JavaScript (front-end ou back-end), o incrível Vue.js, DevOps, DevSecOps e tudo relacionado ao JamStack. Escritor no DEV.to