专栏名称: IPFS星鉴网
星鉴网是全球首家专注于ipfs生态的垂直媒体,集信息传播、技术推广、应用孵化于一身,内容包含项目资讯、技术研讨、行业分析、人物专访、项目展示等维度,为数十万IPFS爱好者提供最具参考价值的媒体服务,成为ipfs爱好者的每日必读媒体。
目录
相关文章推荐
冷丫  ·  开心一笑:妹儿,去把单买了吧! ·  昨天  
冷丫  ·  男朋友不停要,我根本给不了那么多 ·  2 天前  
中国舞台美术学会  ·  资讯丨2025年春节假期国内出游5.01亿人次 ·  2 天前  
51好读  ›  专栏  ›  IPFS星鉴网

使用Knockout和IPFS来构建一个分布式的聊天应用程序

IPFS星鉴网  · 公众号  ·  · 2019-01-17 20:46

正文


文的作者是IPFS方得社区的合作伙伴Textil项目的数据科学家、科罗拉多大学博尔德分校教授Carson Farmer。

本文取得原作者授权进行翻译。

作者简介:

Textile项目数据科学家

科罗拉多大学博尔德分校教授

加拿大不列颠哥伦比亚省维多利亚大学地理系学士、硕士

梅努斯大学国家地理计算中心博士

苏格兰圣安德鲁斯大学博士后

曾担任纽约城市大学亨特学院空间信息高级研究中心(CARSI)副主任和GIScience助理教授

开源软件和开放数据的坚定拥护者

爱好骑自行车、单板滑雪和收集机器人




使用Knockout和IPFS来构建一个分布式的聊天应用程序


如何在一小时内使用不到100行的代码来建立一个完全分布式的聊天应用程序

本次教程的最终产品


分布式应用程序(DApps)是通过peer-to-peer(p2p)协议在去中心化网络上运行的应用程序。他们最大的优点之一是避免任何一点失败,不像传统的APP一样,没有一个实体能够完全控制他们的运作。DApp是一个相对较新的概念(所以仍然难以给出标准定义),但是最广泛的一组例子就是在以太坊上运行的智能合约。随着新的分布式技术,例如区块链和诸如星际文件系统(IPFS)之类的项目受到越来越多关注,势头越来越强劲,DApps也随之越来越受欢迎。


有很多很棒的理由促使开发人员应该开始认真研究、开发分布式应用,包括——当然不限于——可扩展性(一般来说是网络中的节点在控制好自身设备压力的情况下参与应用程序的托管)和可信任(根据定义,DApp代码是开源的,并且通常是内容寻址的,所以代码能够独立验证)。并且,现在有很多例子,从基础的投票应用程序到先进的P2P协作工具,这有助于展现DApp的力量。


今天,我们将开发一个简单的分布式聊天DApp,它运行在IPFS的Pub-Sub机制上,是一个允许节点在开放的、分布式的网络上通信的P2P消息传递模式。在这个环境下,我们将使用Model–view–viewmodel(MVVM)软件设计模式来开发我们的DApp,为您提供在现实的开发场景中使用分布式工具的感受。您将看到,由于IPFS社区开发人员的惊人工作,利用IPFS构建完全分布式的应用程序变得越来越容易。在我们开始动手之前,这儿有一个主要分布式消息模式的快速概述,我们将用来让我们的DApp闪耀。



Pubsub



Pubsub(或发布—订阅)是一种相当标准的消息传递模式,如果有人订阅了主题,他并不知道发布者是谁。基本上,有发布者发送关于特定主题或类别相关的消息,而订阅者只接收关于他们订阅的特定主题的消息。这概念相当简单。这里的主要特点是发布者和订阅者之间没有直接联系,这形成了一个非常强大的通讯系统。那么,我为什么在这里谈论这个呢?因为Pubsub允许节点在之间进行快速、可扩展的和开放的动态通讯……这正是构建分布式聊天应用所需要的……完美!


现在,IPFS使用一种叫做floodsub的东西,这是一个Pubsub的工具,它本质上只是让消息传遍网络,并且要求节点们根据它的确认来收听正确的消息,并忽略其余消息。这可能不是理想的,但这是一个不错的第一步,并且已经能够很好地运行了。也许很快,IPFS将使用gossipsub,这更像一个潮流化的Pubsub,其中节点将与近端节点通讯,并且通过这种方式将更快速地传递消息。注意这个差别,因为这将是IPFS在中期内如何扩展和推动像IPNS这样的东西的一个重要部分。


开始


那么开始吧,让我们克隆Textile dapp模板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 shipyard(一个IPFS社区的孵化项目的GitHub 仓库)的一个非常好的安装包,它简化了与IPFS Pubsub工具的交互。基本上,它允许开发人员基于IPFS Pubsub途径轻松地创建空间,然后发出会员资格事件、监听消息、广播以及将消息直接发送到节点。漂亮。



Model–view–viewmodel


接下来,我们有 knockout和 query-string安装包。这两者的背后只是一个解析URL查询字符串的简单的包,用URL参数开发DAPP时简化了我们的工作——没有什么特别的。不过knockout安装包实际上非常奇特,我们将使用它来开发我们的应用程序,使用真正的软件体系结构模式:Model–view–viewmodel (MVVM)。


视图的新作用是简单地将视图模型暴露的模型数据绑定到视图上


MVVM有助于通过标记语言或GUI代码将图形用户界面的开发从业务逻辑或后端逻辑(数据模型)中开发分离。对于非初学者来说,这种模式在应用程序中引入了“视图模型”的概念,它负责从基础模型中显示(转换)数据对象,使其易于管理和呈现。本质上,MVVM的视图模型是一个值转换器,这意味着视图模型负责以易于管理和呈现对象的方式显示(转换)来自模型的数据对象。视图的新作用是简单地将视图模型显示公布的模型数据“绑定”到您的视图中。在这方面,视图模型更多的是模型,而不是视图,它处理了大多数视图(如果不是全部的话)的显示逻辑。


好的,这会给我们带来哪些思考?它允许我们使用更简单的声明式编程风格来开发动态应用程序,我们可以“免费”获得UI/数据模型的自动更新和UI元素之间的依赖跟踪,而且它有助于更清楚地区分关注点。此外,它是一种探索分布式软件组件的通用设计模式的方法。为什么使用Knockout?因为开始以最小的标记符号/代码来构建单页应用程序是很容易的,他们的交互式教程非常有用,它们提供了有用的文档,涵盖了各种MVVMC概念和特性。


下一步


如果您想要知道我们的接下来的方向,您可以核对一下我们目标中的这个 build/profile-chat分支和这个默认的dapp-template(master) 分支的差异。同时,让我们来建立起新的输入:用您最喜欢的文本编辑器/IDE编辑您的 src/main.js 文件为起点,同时用以下替换line 2:


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对象,用于与分布式web进行交互。与以前的教程不同,我们将直接创建一个IPFS对象,而不是依赖于像window.IPFS-fallback 这样的东西。这主要是因为我们希望我们设置IPFS节点拥有更多的控制权。特别是,我们希望能够使用一些有实验性的特性(比如:Pubsub)并控制我们发布的swarm地址。所以现在我们的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节点按照我们想要的方式初始化,让我们开始我们视图模型的编辑/创建吧。您可以在modified 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节点ID(id),同时还有应用的状态信息,比如一组过往的信息(messages),以及我们是否已经成功地确认了给定的聊天主题(subscribed)。我们也有一个方便的计算属性来表示用户的IPNS链接,这很有趣。您会注意到对于这些属性中的每一个,我们使用的是knockout的可观察对象。以下是knockout的文件:


[…]KO的一个关键好处是它可以在视图模型改变时自动更新您的UI。KO是如何知道部分的视图模式发生了改变的?原因是:您需要将模型属性设置为可观察的,因为这些是特殊的JavaScript对象,可以通知确认者进行更改,并且可以自动检测依赖性。


因此,为了能够对给定属性中的更改做出反应,我们需要使其成为可观察的属性。这是Knockout中可观察的关键点:其他代码可以感知变化。正如我们稍后将看到的,这意味着我们可以将HTML元素属性“绑定”到我们的视图模型的属性中,所以,打个比方:如果我们有一个具有data-bind="text: name"属性的

元素,当我们的视图模型的name属性发生改变时,绑定文本将登记其自身从而得到通知。与往常一样,当您有一些代码可以使用时,这些概念更容易理解,所以让我们利用Knockouts可观察的绑定功能来开始修改我们的src/index.html。


绑定属性


为了利用我们刚刚设置的视图模型,我们需要指定各种HTML元素如何“绑定”到视图模型属性上。我们使用Knockout的data-bind 特性来做到这一点。这里没有很多变化,但您的div现在看起来应该是这样的(我们将逐一查看各个组件以确保我们都在同一页面上):


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

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

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

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

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


由于一幅图片有1000个值,如果您刷新 localhost:8000/的话,您的web-app现在应该是这个样子 (您可能也想从这里复制最简的CSS,这样它看起来会好一点):

基础的聊天ĐApp与一些最简的CSS样式


正如您所看到的(我已经在输出div中添加了蓝色背景以供视觉参考),我们有三个主要元素:i) 一个“name”的输入,用于控制用户名的元素,ii)一个用于显示聊天记录的“output” div(这将保留一系列信息的div,包括用户名、IPNS链接、聊天消息),以及iii)输入的“text”信息来把我们的信息打出来。您唯一可能不熟悉的“新”语法是data-bind属性,因此我们将一次性检查这些内容:


1. :将VIEW模型的Name属性绑定到该输入元素的值。


2. : 将VIEW模型的消息属性绑定到此输入元素的值,如果订阅为真,则只启用元素。


3.

: 每个项目的项目信息,标签的数组作为信息,和...


4. : 把超链接元素的text 绑定到 msg的name属性中 ,将href属性设置为包含msg的from属性的字符串(临时文字),最后,如果msg的from属性等于根viewModel的id,则将元素的CSS类设置为'local'(即,如果这是你自己的消息)。


有很多新的想法!但是标记实际上非常简单,并且它大大简化了我们的整体代码,因为Knockout会处理应用程序状态和视图元素之间的所有交互。就这样,我们在进行下一步之前处于同一页面上,这是我们现在拥有的网络应用程序的当前状态 ......



Pubsub 互动


好的,现在是时候实际添加一些聊天功能了。为此,我们将使用非常棒的IPFS-Pubsub-room 库。我们将首先再次修改 src/main.js文件,这次,通过创建一个新的try/catch区块包含我们需要的所有的交互反馈。我们将分别浏览此代码的每个部分,但您也可以通过检查此要点中的文件差异或次要状态来进行操作。


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,更新我们 viewModel的id属性,设置Pubsub Room(这里我们使用固定的空间ID,但实际上您可能想用一个查询字符串来指定它...参见GitHub上的示例),然后确认在这个Room中subscribed该事件并将其链接到我们viewModel的 subscribed属性中。一旦我们成功确认了room,这将自动启用聊天输入框。


...

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

})

...


现在我们确认Room的 message事件,在这里我们指定返回解析的msg 数据(类似JSON),更新用户名(或使用'anonymous'),更新msg文本,然后添加msg对象到我们viewModel的messages观察序列。


...

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

})


最后,我们在我们的视图模型上确认信息的变更(可能是用户交互的结果),并为将获得当前消息(msg)、用户名(name)指定一个反馈,并在整个room中对作为一个JSON编码字符串的这条msg进行广播。这就实现了用户在提交消息时,可以在输入文本框中进行输入。其余的代码是清理和错误处理......



测试一下



如果您继续在浏览器中刷新应用程序,并打开另一个窗口或浏览器,应该可以在窗口之间进行通讯,类似于此处描述的对话。


两个浏览器窗口之间的模拟聊天对话(Firefox和Safari)


您可以在聊天会话中添加更多的窗口(用户)(随意添加),并添加其他功能,例如在有人加入或离开房间时提示等等,这些将作为练习留给读者。同时,我们知道您刚刚使用一些Javascript创建了一个完整的聊天应用程序、一些最小的HTML标记、一小部分CSS,以及对分布式网络的全新评价。关于所有这一切的最好的是没有涉及中心化的控制点。当然,我们没有添加任何安全措施、加密或隐私控制,因此在通过分布式的Web使用它进行实际转换之前要三思而后行。







请到「今天看啥」查看全文