WEB3DEV

Cover image for Construa uma aplicativo de chat descentralizado com Knockout e IPFS
Dimitris Carvalho Calixto
Dimitris Carvalho Calixto

Posted on

Construa uma aplicativo de chat descentralizado com Knockout e IPFS

Esse artigo foi escrito por: Carson Farmer e traduzido por Dimitris Calixto, artigo original disponível aqui

Como construir uma aplicativo de chat completo e descentralizado em menos de uma hora com <100 linhas de código

Image

As aplicações descentralizadas (Dapps) são aplicações que funcionam numa rede descentralizada (de pares) através de protocolos (de preferência trust-less) ponto-a-ponto (p2p). Um dos seus maiores pontos fortes é que evitam qualquer ponto de falha. Ao contrário das aplicações tradicionais, não há uma única entidade que possa controlar completamente o seu funcionamento. Os Dapps são um conceito relativamente novo (por isso uma definição padrão é ainda um pouco elusiva), mas o conjunto de exemplos mais produtivos estão funcionando como Contratos Inteligentes na blockchain Ethereum. Os Dapps tornaram-se cada vez mais populares à medida que novas tecnologias descentralizadas como as blockchains e projetos como o Sistema de Arquivo Interplanetário (IPFS) ganharam mais atenção e dinamismo

Há muitas boas razões pelas quais os programadores devem começar a olhar seriamente para o desenvolvimento de aplicações descentralizadas, incluindo - mas, certamente não limitadas a - escalabilidade (em geral, a rede de pares participa no alojamento da aplicação, limitando a pressão sobre a sua própria infra-estrutura) e confiança (por definição, o código Dapp é código aberto e muitas vezes o conteúdo é abordado, para que o seu código possa ser verificado independentemente). E há agora muitos exemplos por aí, desde aplicações básicas de votação até ferramentas avançadas de colaboração p2p, que podem ajudar a pintar uma imagem do poder do Dapp.

No post de hoje, vamos desenvolver um simples Dapp de chat descentralizado que funciona no mecanismo de publicação de subscrições do IPFS, um padrão de mensagens p2p que permite aos pares comunicar na web aberta e descentralizada. Enquanto estamos nisso, vamos desenvolver o nosso Dapp utilizando o modelo de design de software Model-view-viewmodel (MVVM), para lhe dar uma sensação de utilização de ferramentas descentralizadas num cenário de desenvolvimento do mundo real. Você verá que a construção de aplicações descentralizadas totalmente funcionais que tiram proveito do IPFS está se tornando cada vez mais fácil, graças ao trabalho espantoso da comunidade de programadores IPFS. Mas antes de começarmos, aqui está uma rápida visão geral do padrão de mensagens descentralizado primário que vamos utilizar para fazer nosso Dapp brilhar.

Pubsub

Pubsub (ou publicar-inscrever) é um bonito padrão de mensagens em que os editores não sabem quem, se alguém" subscreverá um determinado tópico. Basicamente, temos editores que enviam mensagens sobre um determinado tópico ou categoria, e inscritos que recebem apenas mensagens sobre um determinado tópico em que estão inscritos. Um conceito bastante fácil. A característica primária aqui é que não é necessária qualquer ligação direta entre editores e inscritos... o que torna um sistema de comunicação bastante poderoso. Ok, então porque estou falando sobre isso aqui? Porque o pubsub permite uma comunicação dinâmica entre pares que é rápida, escalável, e aberta... que é praticamente o que precisamos para construir uma aplicação de chat descentralizada... perfeito!

Neste momento, o IPFS utiliza algo chamado floodsub, que é uma implementação de pubsub que apenas inunda a rede com mensagens, e os pares são obrigados a ouvir as mensagens certas com base nas suas inscrições, e ignorar o resto. Isto provavelmente não é o ideal, mas é um primeiro passo excelente, e já funciona muito bem. Em breve, o IPFS irá tirar partido do gossipsub, que se assemelha mais a um pubsub de proximidade, onde os pares irão se comunicar com os pares próximos, e as mensagens serão encaminhadas de forma mais eficiente desta forma. Observe este espaço... porque isso vai ser uma parte importante de como o IPFS vai escalar e acelerar coisas como o IPNS a médio prazo.

Começando

Então, para começar, vamos clonar o repositório modelo do Textile dapp, que na realidade é apenas uma simples armação para nos ajudar a acelerar o nosso processo de desenvolvimento. Já utilizamos este modelo em exemplos anteriores (aqui e aqui). Sinta-se à vontade para usar a sua própria configuração de desenvolvimento se preferir, mas vou assumir que você está trabalhando fora do nosso modelo para o resto deste tutorial.


git clone https://github.com/textileio/dapp-template.git chat-dapp

cd chat-dapp

yarn remove queue window.ipfs-fallback

yarn add ipfs ipfs-pubsub-room knockout query-string

Enter fullscreen mode Exit fullscreen mode

Se quiser seguir em frente, mas a partir de uma versão de trabalho totalmente completa, em vez das linhas 3 e 4 acima…


git checkout build/profile-chat

yarn install

Enter fullscreen mode Exit fullscreen mode

Ok, então uma coisa de cada vez. O que é que esses pacotes acima nos trazem? Vamos começar com [ipfs](https://github.com/ipfs/js-ipfs) e [ipfs-pubsub-room](https://github.com/ipfs-shipyard/ipfs-pubsub-room). Obviamente, vamos usar ipfs para interagir com a rede IPFS... mas e o ipfs-pubsub-room? Este é um pacote muito bom do IPFS shipyard (um repositório no GitHub para projetos incubados pela comunidade IPFS) que simplifica a interação com as instalações do IPFS pubsub-room. Basicamente, permite aos desenvolvedores criar facilmente uma sala baseada num canal IPFS pubsub, e depois emite eventos de membros, escuta mensagens, transmissões e mensagens diretas aos seus pares. Legal.

Model–view–viewmodel

A seguir, temos os pacotes de [knockout](http://knockoutjs.com/) e o pacote [query-string](https://www.npmjs.com/package/query-string). O último destes dois é apenas um simples pacote para analisar uma cadeia de consulta url, e realmente simplifica um pouco a nossa vida ao desenvolver dapps com parâmetros url (o que faremos aqui) - nada de extravagante aqui. Mas o pacote knockout é na verdade bastante extravagante, e vamos usá-lo para desenvolver a nossa aplicação usando um verdadeiro padrão arquitetônico de software: Model-view-viewmodel (MVVM).

Image

O MVVM facilita uma separação do desenvolvimento da interface gráfica do usuário - seja através de uma linguagem de marcação ou código GUI - do desenvolvimento da lógica empresarial ou da lógica de back-end (o modelo de dados). Para os não leigos este padrão introduz o conceito de "modelo de visualização" no meio da sua aplicação, que é responsável pela exposição (conversão) de objetos de dados do seu modelo subjacente, de modo a ser facilmente gerido e apresentado. Essencialmente, o modelo de visualização da MVVM é um conversor de valor, o que significa que o modelo de visualização é responsável pela exposição (conversão) dos objetos de dados do modelo, de modo que os objetos sejam facilmente geridos e apresentados. O novo papel da sua visão é então simplesmente 'vincular' os dados do modelo expostos pelo Viewmodel à sua view. A este respeito, o modelo de vista é mais modelo do que vista, e lida com a maioria, se não toda, a lógica de visualização da vista.

Ok, então o que é que isto nos "dá"? Bem, isso nos permite desenvolver aplicações dinâmicas utilizando um estilo de programação declarativo mais simples, obtemos atualizações automáticas do modelo do UI/dados e seguimento de dependência entre elementos do UI 'de graça', além de facilitar uma separação mais clara de interesses. Mas, mais do que tudo, é uma forma de explorar um padrão de design comum com componentes de software descentralizados. E porquê o Knockout? Porque é realmente fácil começar a construir aplicações de página única com um mínimo de marcação/código, os seus tutoriais interativos são super úteis, e fornecem documentações úteis que abrangem vários conceitos e características MVVM.

Próximos passos

Se você quiser ver para onde nos dirigimos, pode verificar essa diff da nossa branch de build/profile-chat branch com a branch padrão dapp-template (master). Entretanto, vamos configurar as nossas novas importações. Comece por editar o seu arquivo src/main.js usando o seu editor de texto favorito/IDE, e substitua a linha 2 (import getIpfs from 'window.ipfs-fallback') por:

import Room from 'ipfs-pubsub-room'

import IPFS from 'ipfs'

import ko from 'knockout'

import queryString from 'query-string'

// Referências globais para fins de demonstração

let ipfs

let viewModel

Enter fullscreen mode Exit fullscreen mode

Agora que ajustamos as nossas importações (e acrescentamos algumas variáveis globais para mais tarde), podemos executar o yarn watch a partir do terminal para por o nosso servidor de construção local a funcionar. Nós teremos que fazer algumas alterações antes do nosso código iniciar corretamente, mas é útil ter o nosso código 'browserfied' para nós enquanto trabalhamos.

Pares IPFS

A seguir, vamos criar um novo objeto IPFS para utilizar para interagir com a web descentralizada. Ao contrário dos tutoriais anteriores, vamos criar um objeto IPFS diretamente, em vez de confiar em algo como window.ipfs-fallback. Isto é principalmente porque queremos mais controle sobre como configuramos o nosso par IPFS. Em particular, queremos ser capazes de permitir algumas características experimentais (i.e., pubsub), e controlar em que endereços de swarm anunciamos (ver este post para detalhes). Assim, nossa função async setup agora se torna:

const setup = async () => {

  try {

    ipfs = new IPFS({

      // Precisamos habilitar o pubsub...

      EXPERIMENTAL: {

        pubsub: true

      },

      config: {

        Addresses: {

          // ...E fornece endereço de swarm para anunciar em

          Swarm: [

            '/dns4/ws-star.discovery.libp2p.io/tcp/443/wss/p2p-websocket-star'

          ]

        }

      }

    })

  } catch(err) {

    console.error('Failed to initialize peer', err)

    viewModel.error(err) // Log error...

  }

}

Enter fullscreen mode Exit fullscreen mode

View Model

Agora que temos as nossas importações em ordem, e o nosso par IPFS inicializando da forma que queremos, vamos começar a editar/criar o nosso view model. Você pode fazer isto dentro da nossa função setup modificada, para que as primeiras linhas dessa função se pareçam agora com isto:

const setup = async () => {  

  // Criar o viewmodel com propriedades para controlar o chat

  função ViewModel() {

    let self = isto

    // Armazena o nome do usuário

    self.name = ko.observable('')

    // Armazena a mensagem atual

    self.message = ko.observable('')

    // Armazena um array de mensagens

    self.messages = ko.observableArray([]))

    // Armazena o id local dos pares

    self.id = ko.observablel(null)

    // Armazena se nós nos inscrevemos na sala com sucesso

    self.subscribed = ko.observable(false)

    // Registra o último erro (no caso de querermos)

    self.error = ko.observable(null)

    // Calculamos a ligação ipns no ar a partir do id do par

    self.url = ko.pureComputed(() => {

      return `https://ipfs.io/ipns/${self.id()}``

    })

  }

  // Cria o viewmodel padrão utilizado para ligar elementos ui, etc.

  viewModel = new ViewModel()

  // Aplicar bindings padrões

  ko.applyBindings(viewModel)

  window.viewModel = viewModel // Só para efeitos de demonstração mais tarde!

Enter fullscreen mode Exit fullscreen mode

Basicamente, estamos criando um objeto Javascript relativamente simples com propriedades que controlam o nome de usuário (name), a mensagem atual (message), o IPFS local Peer Id (id), bem como informação do estado da aplicação, tal como um conjunto de mensagens passadas (messages), e se inscrevemos com sucesso o tópico de chat dado (subscribed). Temos também uma propriedade computadorizada de conveniência para representar uma ligação IPNS do usuário, apenas por diversão. Você notará que para cada uma dessas propriedades, estamos utilizando os objetos observáveis do knockout. A partir da documentação do Knockout:

[...] uma das principais vantagens do KO é que atualiza automaticamente a sua UI quando o viewmodel muda. Como pode a KO saber quando partes do seu viewmodel mudam? Resposta: precisa declarar as propriedades do seu modelo como observáveis, porque estes são objetos especiais em JavaScript que podem notificar os inscritos sobre as alterações, e podem detectar automaticamente as dependências.

Portanto, para poder reagir às mudanças em uma determinada propriedade, precisamos torná-la uma propriedade observável. Este é o ponto principal dos observáveis em Knockout: outros códigos podem ser notificados sobre mudanças. Como veremos em breve, isto significa que podemos "vincular" propriedades de elementos HTML às propriedades do nosso viewmodel, de modo que, por exemplo, se tivermos um elemento <div> com um atributo data-bind="text: name", a vinculação do texto se registrará para ser notificada quando a propriedade name do nosso viewmodel mudar. Como sempre, estes conceitos são muito mais fáceis de entender quando você tem algum código para brincar, então vamos começar a modificar nosso src/index.html para tirar proveito das funcionalidades de bindings observáveis do Knockouts.

Propriedade binding

Para tirar proveito do viewmodel que acabamos de criar, precisaremos especificar como nossos vários elementos HTML 'ligam-se' às propriedades do nosso viewmodel. Fazemos isso usando a propriedade Knockout's data-bind. Não há muitas mudanças a serem feitas aqui, mas seu <body> div deve agora se parecer com isto (revisaremos os vários componentes um por um para ter certeza de que estamos todos na mesma página):

<body>

 <div id="main">

    <div class="controls">

      <input id="name" type="text" data-bind="value: name"

             placeholder="Pick a name (or remain anonymous)"/>

    </div>

    <div class="output"

         data-bind="foreach: { data: messages, as: 'msg' }">

     <div>

        <a data-bind="text: msg.name,

                      css: { local: msg.from === $root.id() },

                      attr: { href: `ipfs.io/ipns/${msg.from}` }">

       </a>

        <div data-bind="text: msg.text"></div>

      </div>

    </div>

    <div class="input">

      <input id="text" type="text" placeholder="Type a message"

             data-bind="value: message, enable: subscribed" />

    </div>

  </div>

  <script src="bundle.js">&lt;/script>

</body>
Enter fullscreen mode Exit fullscreen mode

Já que uma imagem vale por 1000 palavras, eis aqui como deve ser sua web-app agora se você atualizar o localhost:8000/ (talvez você também queira copiar o CSS mínimo daqui, para que pareça um pouco mais agradável):

Image

Como você pode ver (adicionei um fundo azul a nossa saída da div para referência visual), temos três elementos principais: i) um elemento 'name' input para controlar nosso nome de usuário, ii) um div output para exibir nosso histórico de chat (isto conterá séries de divs de mensagens com nome de usuário, links IPNS, e a mensagem), e iii) um input de mensagem 'text' para digitar nossas mensagens. A única sintaxe 'nova' com a qual você provavelmente não estará familiarizado são os atributos de data-bind, por isso, vamos analisar esses um de cada vez:

  1. <input id="name" type="text" data-bind="value: name" />: liga a propriedade do name do nosso viewModel ao value do input deste elemento.
  2. <input id="text" type="text" data-bind="value: message, enable: subscribed" />: vincula a propriedade de message do nosso viewModel ao value ao input do elemento , e somente enable o elemento se subscribed for verdadeiro.
  3. <div class="output" data-bind="foreach": { data: messages, as: msg' }">: para cada item do array de messages, rotular item como msg, e…
  4. <a data-bind="texto: msg.name, css: { local: msg.from ==== $root.id() }, attr: { href:ipfs.io/ipns/${msg.from}}">: ligue o texto do elemento hyperlink à propriedade name do msg, defina o atributo href para string (template literal) contendo a propriedade from de msg, e finalmente, defina a classe CSS do elemento para 'local' se a propriedade do msg for igual ao id do viewModel raiz (ou seja, se esta foi sua própria mensagem).

Uau, foram muitas ideias novas! Mas a marcação é na verdade bastante simples, e simplificou muito nosso código geral, porque a Knockout trata de todas as interações entre o estado do aplicativo e os elementos de visualização. E só para que estejamos todos na mesma página antes de avançarmos, aqui está o estado atual da web-app como o temos agora…

Interações pubsub

Muito bem, agora é hora de realmente adicionar algumas capacidades de bate-papo. Para isso, vamos contar com a fantástica biblioteca ipfs-pubsub-room. Vamos começar modificando nosso arquivo src/main.js novamente, desta vez, criando um novo bloco de try/catch contendo todos os callbacks de interação que vamos precisar. Vamos analisar cada seção deste código separadamente, mas você também pode acompanhar o arquivo diff ou o estado secundário neste resumo.

try {

  ipfs.on('ready', async () => {

    const id = await ipfs.id()

    // Atualiza o view model

    viewModel.id(id.id)

    // Também pode usar query string para especificar, veja o exemplo do github

    const roomID = "test-room-" + Math.random()

    // Cria uma sala básica para determinada id de sala

    const room = Room(ipfs, roomID)

    // Uma vez que o par tenha se inscrito na sala, nós permitimos o bate-papo,

    // que está vinculada à inscrição do viewmodel

    room.on('subscribed', () => {

      // Atualiza o viewmodel

      viewModel.subscribed(true)

    })
Enter fullscreen mode Exit fullscreen mode

Uma vez que o nosso ponto de acesso IPFS esteja pronto, nós await o ponto id , atualizamos a propriedade de identificação do viewModel, configuramos a room do pubsub (aqui usamos umum id fixo da sala, mas na prática é provável que você queira usar uma cadeia de consulta para especificar isto... veja o exemplo no GitHub), e então inscrevemos o evento subscribed na room e o ligamos à propriedade subscribed do viewModel. Isto habilitará automaticamente a caixa de entrada do chat uma vez que nós nos inscrevemos com sucesso na sala. Até agora, tudo bem.

// Quando recebemos uma mensagem...

    room.on('message', (msg) => {

      const data = JSON.parse(msg.data) // Parse data

      // Atualiza o nome de msg (padrão para anônimos)

      msg.name = data.name ? data.name : "anonymous"

      // Atualizada o texto de msg (só para simplificar mais tarde)

      msg.text = data.text

      // Adiciona isso para _front_ do array para manter na parte de baixo

      viewModel.messages.unshift(msg)

    })
Enter fullscreen mode Exit fullscreen mode

Agora nós inscrevemos o evento de message da room, onde especificamos uma chamada um callback que analisa os dados msg (como JSON), atualiza o nome do usuário (ou usa 'anonymous'), atualiza o texto de msg, e então adiciona o objeto msg ao nosso array viewModel's message . Mais uma vez, muito simples.

viewModel.message.subscribe(async (text) => {

      // Se não estiver de fato inscrito ou não tiver texto, pule para fora

      if (!viewModel.subscribed() || !text) return

      try {

        // Obter o nome atual

        const name = viewModel.name()

        // Receba a mensagem atual (uma que iniciou esta atualização)

        const msg = viewModel.message()

        // Transmissão de mensagem para toda a sala como um string JSON

        room.broadcast(Buffer.from(JSON.stringify({ name, text })))

      } catch(err) {

        console.error('Failed to publish message', err)

        viewModel.error(err)

      }

      // Mensagem vazia no view model

      viewModel.message('')

    })

    // Deixar a sala quando descarregarmos

    window.addEventListener('unload',

                            async () => await room.leave())

  })
Enter fullscreen mode Exit fullscreen mode

Finalmente, subscrevemos as mudanças de message em nosso view model (provavelmente como resultado da interação do usuário), e especificamos um callback que receberá a mensagem atual (msg), o nome de usuário (name), e transmitirá msg para toda a room como uma string codificada JSON. Isto torna possível digitar na caixa de texto de entrada e enviar a mensagem quando o usuário enviar o texto. O resto do código é limpeza e tratamento de erros.

Teste

Se você prosseguir e atualizar seu aplicativo em seu navegador, e abrir outra janela no navegador, você deverá agora ser capaz de se comunicar entre janelas, de forma semelhante à sessão aqui descrita.

Image

Você pode adicionar ainda mais janelas (usuários) à sessão de bate-papo (quantas quiser), e adicionar recursos adicionais como anunciar quando alguém entra ou sai da sala, etc., que eu deixarei como um exercício para o leitor. Entretanto, aproveite a glória de saber que você acabou de criar um aplicativo de bate-papo totalmente funcional usando um pouco de Javascript, alguma marcação de HTML mínima, um pouquinho de CSS, e uma nova apreciação da web descentralizada. A melhor parte de tudo isso é que não há pontos de controle centralizados envolvidos. É claro que não adicionamos nenhuma medida de segurança, criptografia ou controles de privacidade, então pense duas vezes antes de usar isso para realizar conversões da vida real sobre a web descentralizada.

Implemente

E por falar em facilidade, porque este dapp em particular não depende de nenhum código externo ou de colegas que rodam localmente, podemos muito facilmente implementá-lo sobre IPFS. É tão fácil quanto construir o código, adicionando a pasta dist/ ao IPFS e abrindo-o em um navegador através de um gateway público:

yarn build

hash=$(ipfs add -rq dist/ | tail -n 1)

open https://ipfs.io/ipfs/$hash
Enter fullscreen mode Exit fullscreen mode

Isso é tudo.

E aí está! Neste tutorial, conseguimos construir um aplicativo de bate-papo totalmente descentralizado com o mínimo de código e esforço. Mantivemos as coisas bem simples, mas conseguimos aproveitar os padrões de programação do mundo real que fazem do desenvolvimento de aplicativos uma brisa. Tudo em um esforço para demonstrar como é surpreendentemente fácil desenvolver aplicações do mundo real em cima do IPFS e suas bibliotecas subjacentes. Mas antes de descentralizar todas as coisas, certifique-se de avaliar todas as desvantagens possíveis também. Se você gostou do que leu aqui, por que não conferir algumas de nossas outras histórias e tutoriais, ou se inscrever em nossa lista de espera do Textile Photos para ver o que estamos construindo com o IPFS. Enquanto você está nisso, deixe-nos um comentário e nos diga quais são os melhores projetos distribuídos na web em que você está trabalhando!

Image

Top comments (0)