Как да създадете напълно децентрализирано приложение за чат за по-малко от час с ‹100 реда код

Децентрализираните приложения (Dapps) са приложения, които работят в децентрализирана мрежа (от партньори) чрез (за предпочитане без доверие) протоколи peer-to-peer (p2p). Една от най-големите им силни страни е, че избягват всяка точка на провал. За разлика от традиционните приложения, няма нито един обект, който може напълно да контролира работата им. Dapps са сравнително нова концепция (така че стандартната „дефиниция“ все още е малко неуловима), но най-плодотворният набор от примери работят като „интелигентни договори“ на „блокчейн Ethereum“. Dapp стават все по-популярни, тъй като новите децентрализирани технологии като блокчейн и проекти като Междупланетната файлова система (IPFS) придобиват повече внимание и импулс.

Има много добри причини, поради които разработчиците трябва да започнат сериозно да разглеждат разработването на децентрализирани приложения, включително — но със сигурност не само — скалируемост (като цяло мрежата от партньори участва в хостването на приложението, ограничавайки натиска върху вашия собствена инфраструктура) и доверие (по дефиниция кодът на Dapp е с отворен код и често адресирано съдържание, така че вашият код може да бъде независимо проверен). И вече има много примери, от „основни приложения за гласуване“ до усъвършенствани „p2p инструменти за сътрудничество“, които могат да помогнат за рисуването на картината на силата на Dapp.

В днешната публикация ще разработим прост децентрализиран Dapp за чат, който работи на механизма за публикуване-абониране на IPFS, p2p модел за съобщения, който позволява на партньорите да комуникират в отворената, децентрализирана мрежа. Докато сме в това, ние ще разработим нашия Dapp, използвайки модела за проектиране на софтуера Model-view-viewmodel (MVVM), за да ви дадем усещане за използване на децентрализирани инструменти в сценарий за разработка в реалния свят. Ще видите, че изграждането на напълно работещи децентрализирани приложения, които се възползват от IPFS, става все по-лесно благодарение на невероятната работа на IPFS общността от разработчици. Но преди да започнем, ето кратък преглед на основния децентрализиран модел за съобщения, който ще използваме, за да направим нашия Dapp да блесне.

Pubsub

Pubsub (или публикуване-абониране) е доста „стандартен модел за съобщения“, при който издателите не знаят кой, ако някой ще се абонира за дадена тема. По принцип имаме издатели, които изпращат съобщения по дадена тема или категория, и абонати, които получават само съобщения по дадена тема, за която са абонирани. Доста лесна концепция. Ключовата характеристика тук е, че не е необходима директна връзка между издатели и абонати... което прави една доста мощна комуникационна система. Добре, тогава защо говоря за това тук? Тъй като pubsub позволява динамична комуникация между партньори, която е бърза, мащабируема и отворена… което е почти това, от което се нуждаем, за да изградим децентрализирано приложение за чат… перфектно!

В момента IPFS използва нещо, наречено floodsub, което е имплементация на pubsub, която по същество просто наводнява мрежата със съобщения и от партньорите се изисква да слушат правилните съобщения въз основа на техните абонаменти и да игнорират останалите. Това вероятно не е идеално, но е отлично първо преминаване и вече работи доста добре. Скоро IPFS ще се възползва от gossipsub, което е по-скоро като епидемичен pubsub, който е наясно с близостта, където партньорите ще комуникират с близките партньори и съобщенията ще се маршрутизират по-ефективно по този начин. Гледайте това пространство… защото това ще бъде важна част от това как IPFS мащабира и ускорява неща като IPNS в средносрочен план.

Приготвяме се да започнем

И така, за да започнем, нека клонираме Textile dapp template repo, което всъщност е просто скеле, което ни помага да ускорим процеса на разработка. Използвахме този шаблон в предишни примери (тук и тук). Чувствайте се свободни да използвате своя собствена настройка за разработка, ако предпочитате, но ще приема, че работите върху нашия шаблон до края на този урок.

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

Ако искате да следвате, но от напълно изпечена работеща версия, тогава вместо редове 3 и 4 по-горе...

git checkout build/profile-chat
yarn install

Добре, така че всичко първо. Какво ни носят тези пакети по-горе? Да започнем с ipfs и ipfs-pubsub-room. Очевидно ще използваме ipfs за взаимодействие с IPFS мрежата... но какво да кажем за ipfs-pubsub-room? Това е наистина хубав пакет от корабостроителницата на IPFS (репо GitHub за инкубирани проекти от IPFS общността), който опростява взаимодействието с IPFS pubsub съоръженията. По принцип той позволява на разработчиците лесно да създадат стая, базирана на IPFS pubsub канал, и след това излъчва събития за членство, слуша съобщения, предавания и директни съобщения до партньори. хубаво.

Модел–изглед–изгледмодел

След това имаме пакетите knockout и query-string. Последният от тези два е просто прост пакет за анализиране на низ на заявка за url и наистина просто опростява живота ни малко, когато разработваме dapps с url параметри (което ще направим тук) – нищо особено тук. Но пакетът knockout всъщност едоста фантастичен и ние ще го използваме, за да разработим нашето приложение, използвайки реален софтуерен архитектурен модел: Модел–изглед–модел на изглед (MVVM).

MVVM улеснява отделянето на разработката на графичния потребителски интерфейс — било то чрез език за маркиране или GUI код — от разработката на бизнес логиката или бек-енд логиката (модела на данните). За непосветените този модел въвежда концепцията за „модел на изглед“ в средата на вашето приложение, което е отговорно за излагането (конвертирането) на обекти с данни от основния ви модел, така че да се управлява и представя лесно. По същество моделът на изглед на MVVM е конвертор на стойност, което означава, че моделът на изглед е отговорен за излагането (конвертирането) на обектите с данни от модела по такъв начин, че обектите да се управляват и представят лесно. След това новата роля на вашия изглед е просто да „свърже“ данните от модела, изложени от модела на изгледа, към вашия изглед. В това отношение моделът на изглед е повече модел, отколкото изглед, и обработва повечето, ако не и цялата логика на изгледа на изгледа.

Добре, какво ни носи това? Е, това ни позволява да разработваме динамични приложения, използвайки по-прост декларативен стил на програмиране, получаваме автоматични актуализации на потребителския интерфейс/модела на данни и проследяване на зависимостите между елементите на потребителския интерфейс „безплатно“, плюс това улеснява по-ясното разделяне на проблемите. Но повече от всичко, това е начин за изследване на общ модел на проектиране с децентрализирани софтуерни компоненти. И защо "Нокаут"? Тъй като е наистина лесно да започнете да създавате приложения от една страница с минимално маркиране/код, техните интерактивни уроци са супер полезни и предоставят полезни документи, обхващащи различни MVVM концепции и функции.

Следващи стъпки

Ако искате да видите накъде сме се запътили, можете да разгледате този разход на нашия целеви клон build/profile-chat с клона по подразбиране dapp-template (master). Междувременно, нека настроим нашите нови импортирания. Започнете с редактиране на вашия src/main.js файл с помощта на любимия ви текстов редактор/IDE и заменете ред 2 (import getIpfs from 'window.ipfs-fallback') с:

import Room from 'ipfs-pubsub-room'
import IPFS from 'ipfs'
import ko from 'knockout'
import queryString from 'query-string'
// Global references for demo purposes
let ipfs
let viewModel

Сега, след като коригирахме нашите импортирания (и добавихме някои глобални променливи за по-късно), можете да стартирате yarn watch от терминала, за да стартирате нашия локален сървър за изграждане. Ще трябва да направим някои промени, преди нашият код да работи правилно, но е полезно нашият код да бъде „преглеждан“ за нас, докато работим.

IPFS партньор

След това ще създадем нов IPFS обект, който да използваме за взаимодействие с децентрализираната мрежа. За разлика от предишните уроци, ние ще създадем IPFS обект директно, вместо да разчитаме на нещо като window.ipfs-fallback. Това е основно защото искаме повече контрол върху това как настройваме нашия IPFS партньор. По-конкретно, искаме да можем да активираме някои експериментални функции (т.е. pubsub) и да контролираме на кои адреси на рояк обявяваме (вижте тази публикация за подробности). Така нашата async setup функция сега става:

const setup = async () => {
  try {
    ipfs = new IPFS({
      // We need to enable pubsub...
      EXPERIMENTAL: {
        pubsub: true
      },
      config: {
        Addresses: {
          // ...And supply swarm address to announce on
          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...
  }
}

Вижте модела

Сега, когато импортираме в ред и IPFS партньорът ни инициализира желания от нас начин, нека започнем да редактираме/създаваме нашия модел на изглед. Можете да направите това в нашата модифицирана функция setup, така че първите няколко реда на тази функция сега да изглеждат по следния начин:

const setup = async () => {  
  // Create view model with properties to control chat
  function ViewModel() {
    let self = this
    // Stores username
    self.name = ko.observable('')
    // Stores current message
    self.message = ko.observable('')
    // Stores array of messages
    self.messages = ko.observableArray([])
    // Stores local peer id
    self.id = ko.observable(null)
    // Stores whether we've successfully subscribed to the room
    self.subscribed = ko.observable(false)
    // Logs latest error (just there in case we want it)
    self.error = ko.observable(null)
    // We compute the ipns link on the fly from the peer id
    self.url = ko.pureComputed(() => {
      return `https://ipfs.io/ipns/${self.id()}`
    })
  }
  // Create default view model used for binding ui elements etc.
  viewModel = new ViewModel()
  // Apply default bindings
  ko.applyBindings(viewModel)
  window.viewModel = viewModel // Just for demo purposes later!
  ...

По принцип създаваме сравнително прост Javascript обект със свойства, които контролират потребителското име (name), текущото съобщение (message), локалния IPFS Peer Id (id), както и информация за състоянието на приложението, като масив от минали съобщения ( messages) и дали сме се абонирали успешно за дадената тема за чат (subscribed). Имаме и удобно изчислено свойство за представяне на IPNS връзката на потребителя, просто за забавление. Ще забележите, че за всяко от тези свойства използваме „наблюдаеми“ обекти на нокаут. От документите на Knockout:

[…] едно от ключовите предимства на KO е, че той актуализира вашия потребителски интерфейс автоматично, когато моделът на изглед се промени. Как може KO да разбере кога части от вашия модел на изглед се променят? Отговор: трябва да декларирате свойствата на вашия модел като наблюдавани, защото това са специални JavaScript обекти, които могат да уведомяват абонатите за промени и могат автоматично да откриват зависимости.

Така че, за да можем да реагираме на промени в дадено свойство, трябва да го направим наблюдаемо свойство. Това е нещо като целия смисъл на наблюдаемите в Knockout: друг код може да бъде уведомен за промени. Както ще видим скоро, това означава, че можем да „свържем“ свойствата на HTML елемент към свойствата на нашия изглед модел, така че, например, ако имаме <div> елемент с data-bind="text: name" атрибут, текстовото обвързване ще се регистрира, за да бъде уведомено, когато свойството name на нашия модел на изглед се променя. Както винаги, тези концепции са много по-лесни за разбиране, когато имате някакъв код, с който да си поиграете, така че нека започнем да модифицираме нашия src/index.html, за да се възползваме от функциите за наблюдавано свързване на Knockouts.

Свързващи свойства

За да се възползваме от модела на изглед, който току-що настроихме, ще трябва да посочим как нашите различни HTML елементи се „свързват“ към свойствата на нашия модел на изглед. Правим това, използвайки свойството data-bind на Knockout. Тук няма много промени, но вашият <body> div вече трябва да изглежда по следния начин (ще прегледаме различните компоненти един по един, за да сме сигурни, че всички сме на една страница):

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

Тъй като едно изображение струва 1000 думи, ето как сега трябва да изглежда вашето уеб приложение, ако опресните localhost:8000/ (може също да искате да копирате минималния CSS от тук, така че да изглежда малко по-добре):

Както можете да видите (добавих син фон към нашия изходен div за визуална справка), имаме три основни елемента: i) 'name' input елемент за контролиране на нашето потребителско име, ii) 'output' div за показване на нашата история на чат (това ще съдържа серия от съобщения div с потребителско име, IPNS връзки и съобщението) и iii) 'text' съобщение input за въвеждане на нашите съобщения. Единственият „нов“ синтаксис, с който вероятно няма да сте запознати, са атрибутите data-bind, така че ще ги прегледаме един по един:

  1. <input id="name" type="text" data-bind="value: name" />: свържете свойството name на нашия viewModel към value на този input елемент.
  2. <input id="text" type="text" data-bind="value: message, enable: subscribed" />: свържете свойството message на нашия viewModel към value на този input елемент и само enable елемента, ако subscribed е true.
  3. <div class="output" data-bind="foreach: { data: messages, as: 'msg' }">: за всеки елемент в масива messages, маркирайте елемента като msg и...
  4. <a data-bind="text: msg.name, css: { local: msg.from === $root.id() }, attr: { href: `ipfs.io/ipns/${msg.from}` }">: обвържете text на елемента хипервръзка със свойството name на msg, задайте атрибута href на низ (шаблонен литерал), съдържащ свойството from на msg, и накрая задайте CSS класа на елемента на 'local', ако from свойството на msg е равно на id на корена viewModel (т.е. ако това е вашето собствено съобщение).

Уау това бяха много нови идеи! Но маркирането всъщност е доста просто и значително опрости цялостния ни код, защото Knockout обработва всички взаимодействия между състоянието на приложението и елементите на изгледа. И точно за да сме всички на една и съща страница, преди да продължим напред, ето текущото състояние на уеб приложението, както го имаме сега...

Pubsub взаимодействия

Добре, сега е време всъщност да добавите някои възможности за чат. За това ще разчитаме на много страхотната библиотека ipfs-pubsub-room. Ще започнем, като модифицираме отново нашия src/main.js файл, този път, като създадем нов try/catch блок, съдържащ всички обратни извиквания за взаимодействие, от които ще се нуждаем. Ще прегледаме всеки раздел от този код поотделно, но вие също можете да продължите, като „проверите файла diff“ или „вторичното състояние в тази същност“.

try {
  ipfs.on('ready', async () => {
    const id = await ipfs.id()
    // Update view model
    viewModel.id(id.id)
    // Can also use query string to specify, see github example
    const roomID = "test-room-" + Math.random()
    // Create basic room for given room id
    const room = Room(ipfs, roomID)
    // Once the peer has subscribed to the room, we enable chat,
    // which is bound to the view model's subscribe
    room.on('subscribed', () => {
      // Update view model
      viewModel.subscribed(true)
    })
...

След като нашият IPFS партньор е готов, ние await партньора id, актуализираме свойството id на нашия viewModel, настройваме pubsub Room (тук използваме фиксиран идентификатор на стая, но на практика вероятно ще искате да използвате низ на заявка, за да посочите това... вижте примерът в GitHub), а след това се абонирайте за събитието subscribed на room и го свържете към собствеността subscribed на нашия viewModel. Това автоматично ще активира полето за въвеждане на чат, след като успешно се абонираме за стаята. Дотук добре.

...
    // When we receive a message...
    room.on('message', (msg) => {
      const data = JSON.parse(msg.data) // Parse data
      // Update msg name (default to anonymous)
      msg.name = data.name ? data.name : "anonymous"
      // Update msg text (just for simplicity later)
      msg.text = data.text
      // Add this to _front_ of array to keep at bottom
      viewModel.messages.unshift(msg)
    })
...

Сега се абонираме за събитието message на Room, където посочваме обратно извикване, което анализира msg данните (като JSON), актуализира потребителското име (или използва 'anonymous'), актуализира msg текста и след това добавя msg обекта към messages видимия масив на viewModel . Отново, доста ясно.

...
    viewModel.message.subscribe(async (text) => {
      // If not actually subscribed or no text, skip out
      if (!viewModel.subscribed() || !text) return
      try {
        // Get current name
        const name = viewModel.name()
        // Get current message (one that initiated this update)
        const msg = viewModel.message()
        // Broadcast message to entire room as JSON string
        room.broadcast(Buffer.from(JSON.stringify({ name, text })))
      } catch(err) {
        console.error('Failed to publish message', err)
        viewModel.error(err)
      }
      // Empty message in view model
      viewModel.message('')
    })
    // Leave the room when we unload
    window.addEventListener('unload',
                            async () => await room.leave())
  })
...

И накрая, ние се абонираме за message промени в нашия модел на изглед (вероятно в резултат на взаимодействие с потребителя) и посочваме обратно извикване, което ще получи текущото съобщение (msg), потребителското име (name) и broadcast msg до целия room като JSON-кодиран низ. Прави възможно въвеждането в текстовото поле за въвеждане и изпращане на съобщението, когато потребителят изпрати текста. Останалата част от кода е почистване и обработка на грешки...

Тествайте го

Ако продължите и опресните приложението си в браузъра си и отворите друг прозорец или браузър, сега трябва да можете да комуникирате между прозорците, подобно на сесията, изобразена тук.

Можете да добавите още повече прозорци (потребители) към чат сесията (колкото желаете) и да добавите допълнителни функции като съобщаване, когато някой се присъедини или напусне стаята и т.н., което ще оставя като упражнение за читателя. Междувременно се насладете на славата да знаете, че току-що сте създали напълно работещо приложение за чат, използвайки малко Javascript, малко минимално HTML маркиране, малко CSS и изцяло нова оценка за децентрализираната мрежа. Най-добрата част от всичко това е, че няма включени централизирани контролни точки. Разбира се, не сме добавили никакви мерки за сигурност, криптиране или контроли за поверителност, така че помислете два пъти, преди да използвате това за провеждане на реализации в реалния живот през децентрализираната мрежа.

Разположете го

И като говорим за лесно, тъй като това конкретно dapp не разчита на никакъв външен код или локално работещи партньори, можем доста лесно да го внедрим през IPFS. Толкова е лесно, колкото да създадете кода, да добавите папката dist/ към IPFS и да я отворите в браузър през публичен шлюз:

yarn build
hash=$(ipfs add -rq dist/ | tail -n 1)
open https://ipfs.io/ipfs/$hash

Това е всичко

И ето го! В този урок успяхме да изградим напълно работещо децентрализирано приложение за чат с минимален код и усилия. Поддържахме нещата доста прости, но успяхме да се възползваме от моделите за програмиране в реалния свят, които правят разработката на приложения лесна. Всичко в опит да се демонстрира колко изненадващо лесно е да се разработват приложения от реалния свят върху IPFS и неговите основни библиотеки. Но преди да започнете да децентрализирате всички неща, уверете се, че сте „оценили“ и всички „възможни недостатъци“. Ако ви харесва това, което сте прочели тук, защо не разгледате някои от другите ни истории и уроци или се запишете в нашия списък за чакане за снимки на текстил, за да видите какво ние изграждаме с IPFS. Докато го правите, пишете ни и ни кажете върху какви готини разпределени уеб проекти работите— ще се радваме да чуем за тях!