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
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
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
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).
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
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...
}
}
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!
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"></script>
</body>
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):
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:
-
<input id="name" type="text" data-bind="value: name" />
: liga a propriedade doname
do nossoviewModel
aovalue
do input deste elemento. -
<input id="text" type="text" data-bind="value: message, enable: subscribed" />
: vincula a propriedade demessage
do nosso viewModel aovalue
aoinput
do elemento , e somenteenable
o elemento sesubscribed
for verdadeiro. -
<div class="output" data-bind="foreach": { data: messages, as: msg' }">
: para cada item do array demessages
, rotular item comomsg
, e… -
<a data-bind="texto: msg.name, css: { local: msg.from ==== $root.id() }, attr: { href:
ipfs.io/ipns/${msg.from}}">
: ligue otexto
do elemento hyperlink à propriedadename
domsg
, defina o atributo href para string (template literal) contendo a propriedadefrom
demsg
, 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)
})
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)
})
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())
})
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.
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
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!
Latest comments (0)