大数跨境

第9章 去中心化DAPP实战

第9章 去中心化DAPP实战 数组智控产业发展科技院
2023-05-23
3
导读:文章来源《DAPP应用开发指南》通过本书前面的内容,我们知道如何使用Solidity来开发智能合约,严格地讲

文章来源《DAPP应用开发指南》

通过本书前面的内容,我们知道如何使用Solidity来开发智能合约,严格地讲,智能合约不是一个独立的应用,而只是业务逻辑的一部分,一个完整的应用还应该包括友好的用户交互界面。

在智能合约之上构建的应用,我们称之为DAPP。

本章就来介绍如何开始构建一个DAPP。

9.1 什么是DAPP

现在的互联网应用通常都有相应的中心化服务器,在应用端(前端)展现内容的时候,通常是应用端发送一个请求到服务器,服务器返回相应的内容到应用端。

整个应用实际上是由中心化的服务器控制的。

DAPP,即Decentralized APP,意为“去中心化应用”,它运行在去中心化的网络节点上,其应用端其实和现有的互联网应用一样,不过应用的后端不再是中心化的服务器,而是去中心化的网络节点。

这个节点可以是网络中任意的节点,应用端发给节点的请求,当节点收到交易请求之后,会把请求广播到整个网络,交易在网络达成共识之后才算是真正得到执行(即处理请求的是整个网络,连接的节点不能独立处理请求)。

传统APP与DAPP架构上的异同如图9-1所示。

图9-1 DAPP架构与APP架构的对比

在去中心化应用中,发送给节点的请求通常称为“交易”,交易和普通的请求有几个很大的不同:

交易的数据经过用户个人签名(因此需要关联钱包)之后发送到节点;

另外,普通的请求大多数都是同步的(及时拿到结果),而交易大多数都是异步的(主要是因为网络共识比较耗时),从节点上获得数据状态(比如交易的结果),一般是通过事件回调来获得。

如何开发DAPP

在开发中心化应用过程中,最重要的两部分是客户端和后端的服务程序,客户端通过HTTP请求链接到后端服务程序,后端服务程序运行在服务器上,比如Nginx、Apache等。

开发去中心化应用,最重要的也是两部分:客户端及智能合约。

智能合约的作用就像后端服务程序,智能合约运行在节点的以太坊虚拟机(EVM)上,客户端调用智能合约,是通过向节点发起请求完成的。

我们将两者作一个对比:

客户端<=>客户端

HTTP请求<=> RPC请求

后端服务程序<=>智能合约

Nginx/Apache <=>节点服务器

DAPP客户端的开发和现有互联网应用一样。

此外,通过上一章的学习,我们已经了解如何进行智能合约的开发,所以我们现在只需要了解客户端如何与智能合约交互。

交互是通过对以太坊节点发起RPC(远程过程调用)请求来进行的。

以太坊节点其实会提供一系列JSON-RPC接口,对于开发者来讲,通常需要使用JSON-RPC接口封装Web3函数库,如Web3提供的接口包含获取节点状态、获取账号信息、调用合约、监听合约事件等,目前的主流语言都支持Web3的实现,例如:

(1)web3.js是JSON-RPC接口JavaScript版本的封装(代码库:https://github.com/ethereum/web3.js)。

(2)web3j是JSON-RPC接口Java版本的封装(代码库:https://github.com/web3j/web3j)。

(3)web3.py是JSON-RPC接口Python版本的封装(代码库:https://github.com/ethereum/web3.py)。

还有更多版本可以在GitHub上找到,本章主要以Web应用进行介绍,将使用web3.js库。

小知识:当前互联网应用通常称为Web2.0,而基于合约的互联网应用是一场大升级,因此取名为Web3.0。

9.2 Web3.js

9.2.1 Web3.js简介

web3.js库是一系列模块的集合,服务于以太坊生态系统的各个功能,举几个例子。

·web3-eth:用来与以太坊区块链及合约的交互;

·web3-shh:包含了Whisper协议(点对点通信协议)相关的API;

·web3-bzz:包含了Swarm协议(去中心化文件存储协议)相关的API;

·web3-utils:包含一些常用的工具方法,比如货币单位wei与ether之间的转换等。

本书并不会讲述所有Web3.js中的API,大家应该养成从API文档中查找函数用法的习惯,Web3.js文档的链接为:https://web3js.readthedocs.io/,如果中文不是很好,推荐登链社区翻译的版本:https://learnblockchain.cn/docs/web3.js/。

其实我们在前面“进入以太坊世界”一章介绍Geth时,已经使用了Web3.js,这是因为在Geth库客户端中集成了web3.js库,比如Geth的查看余额命令:eth.getBalance(eth.accounts[0])就使用了以下的API:

因为与链的交互多是异步操作,很多方法都带有一个回调函数作为最后一个参数,有一点需要注意,web3.js有两个不兼容的版本:0.20.x及1.x。

1-x对0.20.x版本做了重构,并且引入了Promise(这是一个“承诺将来会执行”的对象)来简化异步编程,避免层层的回调嵌套。

作一个对比,下面使用两个版本来获取当前区块号:

使用1.x版本代码上要比0.20.x版本简洁一些。

9.2.2 引入Web3.js

如果应用中需要和链进行交互,需要先引入web3.js库。根据项目的不同,使用不同的方式引入Web3.js,例如:

·npm项目,使用命令npm install web3来安装Web3.js。

·meteor项目,使用命令meteor add ethereum:web3来安装Web3.js。

·纯js项目,直接用<script>标签引入web3.js文件。

引入web3.js库之后就可以创建web3实例,代码如下。

创建web3实例时,需要给Web3设置一个提供者(Provider)参数,它用来指定Web3和哪一个节点通信,有了Web3实例之后,就可以调用web3的成员方法,如上面9.2.1节的获取账户余额。

9.2.3 用web3.js跟合约交互

1-调用合约函数

要调用合约函数,需要先创建一个对应合约的实例,使用如下方法:var myContract = new web3.eth.Contract(ABI, [, address][, options])

·第一个参数ABI是合约的接口描述,在第7章Solidity进阶介绍过,它会由编译器输出。

·第二个参数是合约部署后的地址。

有了合约实例之后,就可以通过myContract.methods.myMethod()调用合约的函数,例如要调用前面在探索智能合约编写的Counter合约的count()函数,代码如下:

[插图]你也许注意到以上代码调用合约的方法有点不同,调用合约其实有两种方式:call()及send()。

·call():用来调用合约的视图方法,它不会修改链上的数据。

·send():用来调用合约中会修改状态的方法。

call()和send()都带一个可选的options对象参数,options对象包含下面几个字段。

·from:用来指定发起调用的账号。

·gasPrice(可选):指定发起调用的单位gas的价格。

·gas(可选):指定发起调用最多能使用的gas(gas limit)。

·value(可选):指定交易附加的以太币(仅send方式有效)。

2.监听事件

监听是获取区块链状态变化的主要方式,web3.js提供了web3.eth.subscribe接口来订阅区块链的状态,例如下面的代码监听了区块头生成事件,当节点收到一个新区块时,将回调我们传入的函数。

var subscription = web3.eth.subscribe('newBlockHeaders', function(error,result){})下面介绍如何监听合约的自定义事件。

假设合约有MyEvent事件,通过web3.eth.Contract创建合约实例myContract,以下代码就可以监听合约MyEvent事件。

myContract.events.MyEvent({可选选项},function(error, event){console.log(event); })

当链上发生了MyEvent事件,我们传入的函数(上述代码的第2个参数)就会被调用,可选选项部分可以指定从哪一个区块块开始监听,或者指定监听的数据等。

用户监听事件的程序通常需要常驻后台运行,否则将可能错过一些事件的发生,在9.6节我们会给出一个监听合约的示例,帮助大家理解事件,关于web3.js API接口的详情,还需要多多阅读文档。

9.3 DAPP开发工具

通过前面几章的介绍,我们基本可以通过以下两步开发一个DAPP:

(1)在Remix完成合约的编写、编译、部署;

(2)编写前端,同时利用Web3接口调用合约方法。

当我们按照这个步骤去开发应用的时候,项目会很难管理,因为在开发过程中,合约是需要更改的,这样合约的ABI及合约地址也会变化,而前端依赖的合约ABI及地址也需要进行相应的更改。

因此,大一点的项目需要用到相应的框架和脚手架命令来帮助进行项目管理。

9.3.1 Truffle

Truffle是目前最流行的以太坊DAPP开发及测试框架,它可以帮我们简化开发流程,处理大量开发中的琐事,Truffle的功能包括:

·内置智能合约编译、链接、部署和二进制(文件)管理。

·可快速开发自动化智能合约测试框架。

·可脚本化、可扩展的部署和迁移框架。

·可管理多个不同的以太坊网络,可部署到任意数量的公共主网和私有网络。

·使用ERC190标准,使用EthPM和NPM进行包安装管理。·支持通过命令控制台直接与智能合约进行交互。

·支持在Truffle环境中使用外部脚本运行器执行脚本。

通过使用Truffle提供的命令,可以方便进行合约编译、部署、测试、打包DAPP。

Truffle本身是使用Node开发的,因此可以使用npm命令来安装Truffle,使用以下命令:

9.3.2 Ganache

Ganache是另一个开发者工具,它可以很容易地帮我们在本机模拟出一个以太坊私有链。

Ganache是一个图形界面的应用,安装之后,打开的界面如图9-2所示。

从图中可以看到,Ganache会默认创建10个账户,RPC服务地址是http://127.0.0.1:7545,在应用上可以实时看到当前的区块高度、账号等信息。

Ganache提供了多个平台的版本,大家可以到官网(https://www.trufflesuite.com/ganache)进行下载和安装。

图9-2 Ganache界面

9.4 DAPP投票应用

安装好前面的工具,就可以进行实际的DAPP开发,我们通过几个真实的案例来介绍如何使用前面的工具来进行开发。

第一个案例是DAPP投票应用,投票最担心的是暗箱操作,我们可以利用区块链的去中心化技术来实现一个DAPP,保证投票的公平、公正。

本案例会用到Solidity中的映射(mapping)、结构体(struct)及事件(event),可以回顾本书第6、7章相关的知识。

9.4.1 投票应用需求

要实现一个投票DAPP,一般的基本需求是:

(1)每人(账号)只能投一票;

(2)记下一共有多少候选人;

(3)记录每个候选人的得票数。

在用户界面上,需要看到每个候选人的得票数以及选择候选人进行投票,需求设计效果如图9-3所示。

图9-3 投票DAPP效果图

9.4.2 创建项目

先为DAPP创建一个目录,进入目录使用truffle init初始化项目,示例代码如下。

truffle init命令会为我们下载一个空的项目模板来创建工程,工程下会自动创建以下目录和文件。

·contracts:为智能合约的文件夹,所有的智能合约文件都放置在这里。

·migrations:用来指示如何部署(迁移)智能合约。

·test:智能合约测试用例文件夹。

·truffle-config.js:配置文件,配置truffle连接的网络及编译选项。

·src:web网页文件源码文件夹。

truffle init是从零创建一个项目,这种方式创建的项目通常还需要用npm init,以便后面安装一些依赖的npm软件包。

如果大家使用作者GitHub上的完整代码(地址:https://github.com/xilibi2003/election),可以直接npm install安装所有的依赖。

truffle还提供了一些Box(一个打包好的样板工程)为我们配置好了数个相应的前端依赖(例如React、Vue等),因此也可以基于已有的Box来创建项目。

例如,开发React项目时,可以使用以下命令创建项目:

9.4.3 编写智能合约

在项目的contracts目录下新建一个合约文件:Election.sol,合约代码如下:

投票合约Election代码中也加入了注释,阅读起来应该不困难,读者最好自己在电脑上演练,以便加深印象。Election合约有3个函数:

(1)constructor()构造函数,用来初始化两个候选人。

(2)addCandidate()函数,用来添加候选人。

(3)vote()函数,用来给投票人投票。

9.4.4 合约编译及部署

Truffle提供了命令来编译合约,在项目目录下使用truffle compile就可以编译合约,示例代码如下。

如果合约没有语法错误,会在build/contracts目录下生成包含合约ABI及合约字节码的构建文件:

Election.json,之后与合约交互时用到ABI,就需要引入这个文件。

9.4.5 合约部署

如果没有编译错误,合约就可以部署到区块链网络上。部署需要进行两步:

(1)连接区块链网络;

(2)编写一个部署脚本,说明部署规则。

1连接区块链网络

在项目初始化时,Truffle会帮我们创建truffle-config.js文件,在这个文件里就可以配置在Truffle中连接的网络。

truffle-config.js支持配置多个网络,通常开发时使用本地开发网络,灰度发布时使用以太坊测试网络,产品发布上线使用以太坊主网。

使用本地开发者网络

开发者网络是Truffle连接的默认网络,配置一个开发者网络(对应以下代码的development)的代码如下。

开发者网络一般会设置为链接到本地网络,上面的代码使用了Ganache RPC服务的地址及端口(通过配置host及port也可以链接到其他客户端,如Geth),表示在部署时将连接到Ganache进行部署,配置网络时,还可以指定一些可选的参数,例如:·gas:指定部署的gas limit。

默认为4712388。·gasPrice:指定部署的gas价格。

默认为100000000000,即100 Gwei。

·network_id:指定网络(以太坊每一个网络会有一个对应的编号,以便网络节点之间相互检验)。

例如,1为以太坊主网,3为Ropsten网络,上例使用的5777是Ganache的网络ID。

当Truffle执行部署时,会使用Ganache账号列表中的第一个账号进行部署(即使用第一个账号进行交易签名并支付部署合约的费用)。

如果网络配置是连接到Geth节点,也同样使用Geth加载的第一个账号(Geth需要用户输入密码解锁账号签名交易,Ganache则会自动进行交易签名)。

使用Infura节点链接到以太坊网络

在truffle-config.js的networks字段中加入一个连接以太坊测试网络Ropsten的配置,如果我们本地有Geth节点连接了Ropsten网络,则和上面配置开发者网络类似;

如果没有搭建自己的节点,可以使用Infura提供的节点。

Infura是以太坊基础服务提供商,在Infura官网注册一个用于访问Infura服务的token,注册后创建一个项目,复制节点地址(ENDPOINT),如图9-4所示。

图9-4 使用Infura服务节点

Infura仅仅提供节点服务,我们还需要一个部署交易的账号,以便将交易的本地签名打包后提交到Infura节点,软件包HDWalletProvider可以帮助我们完成这些工作。

通过在项目根目录下执行以下命令安装HDWalletProvider:

然后修改truffle-config.js加入一个新网络,这个新的网络使用HDWalletProvider来配置:

如上,Ropsten网络是通过provider来进行配置的,它利用HDWalletProvider来连接网络,HDWalletProvider的第一个参数是助记词,第二个参数是上面复制的Infura节点服务地址。

注意,在部署合约前,要确保账号有足够的余额。另外,本书为了方便,直接在truffle-config.js配置文件中使用的是明文的助记词,在正式的项目中,项目通常有多人开发,truffle-config.js通常也会被上传到代码服务器中,泄露助记词可能让我们丢失以太币,因此最好是把助记词保存在一个不被git管理的文件中。

对于truffle-config.js中的每个网络(networks),可以指定host/port或provider这两种方式来配置,我们在开发者网络中使用了host/port,在Ropsten网络使用了指定provider的方式,但是我们不能在同一个网络中同时指定两个方式。

2.编写部署脚本

在Truffle知道了如何连接网络后,就可以编写部署脚本(也被称为迁移脚本)。

部署脚本的作用是告诉Truffle如何处理我们的合约,比如部署合约的先后顺序、给合约传参数或给合约链接库等。

部署脚本通常都会放置在migrations文件夹,如果你动手操作,也许已经发现,在migrations文件夹下已经有一个名为1_initial_migration.js的部署脚本,用来部署Migrations.sol合约(我们稍后介绍Migrations合约的作用)。

部署脚本名称前面的序号是用来表示部署脚本的顺序,部署时按照序号从小到大来运行脚本,各个部署脚本不需要保持连续。

现在参照1_initial_migration.js创建一个部署Election合约的脚本2_deploy_contracts.js,2_deploy_contracts.js内容如下:

脚本开始处通过artifacts.require()方法告诉Truffle我们想要与哪些合约进行交互。

这个方法类似于Node的require(),require()中可以指定合约文件名或合约名(Truffle要求文件名与合约名一致)。

部署脚本通过module.exports语法导出函数,Truffle就是通过这个导出函数来执行部署,函数会接受deployer对象作为它的第一个参数。

deployer对象是用于部署任务最主要的接口,用deploy函数进行部署。deploy函数的原型如下:

args参数用来指定合约的初始化参数(如果合约的constructor有参数,就使用args传入),options可以指定部署合约交易的一些属性,例如指定交易的gas等,更多高级用法可以参考https://learnblockchain.cn/docs/truffle/getting-started/running-migrations.html。

9.4.6 执行部署

配置好区块链网络和部署好脚本后,接下来就可以使用truffle migrate命令执行部署。

truffle migrate默认会使用开发者网络(注意要先运行Ganache),执行truffle migrate命令的示例代码如下。

执行truffle migrate时,在控制台会显示部署的详情,如部署交易的hash、部署的合约地址、消耗的gas费用、部署在哪一个区块上,等等。

回到Ganache,我们也会看到区块号增长到了4,如图9-5所示。

你一定会好奇,明明只用2个部署脚本部署了2个合约,应该增长2个区块才对。

图9-5 部署后Ganache区块号增长

这就需要解释一下Migrations.sol合约(这个合约可以在contracts目录下找到)的作用,Migrations.sol是Truffle用来避免重复部署的合约,它作为第一个合约进行部署(因为是使用前缀为1的脚本1_initial_migration.js进行部署的),每当Truffle执行完一个部署后,就会把部署的需要写入Migrations合约,因此在truffle migrate运行时实际会发生4笔交易:

(1)运行1_initial_migration.js进行部署;

(2)把序号1写入合约Migrations;

(3)运行2_deploy_contracts.js进行部署;

(4)把序号2写入合约Migrations。

把序号写入合约是通过调用Migrations合约的setCompleted函数完成的,序号保存在last_completed_migration变量中,表示的是最后部署的脚本序号,之后再加入其他部署文件,假设是3_yourcontract.js,运行truffle migrate时,Truffle就会首先读取上一次部署到哪个文件,再继续运行比last_completed_migration序号大的(所有)部署文件,这样就可以避免重复部署。

如果需要强制从某一个序号的部署文件开始执行,可以使用truffle migrate-f序号,即通过-f来指定部署序号,Truffle此时将会忽略last_completed_migration的值。


例如,truffle migrate-f2会从第2个迁移文件开始部署。

在完成部署后,部署信息如合约地址会写入之前编译生成的构建文件Election.json中。

如果要部署其他的网络,可以通过--network来指定网络,例如部署到Ropsten网络,则使用命令:

truffle migrate--network ropsen,参数ropsten对应truffle-config.js在networks字段下定义的网络。

9.4.7 合约测试

Truffle使用Mocha测试框架,支持使用JavaScript和Solidity来编写测试用例。

Mocha是JavaScript的一种单元测试框架,既可以在浏览器环境下运行,也可以在Node.js环境下运行。

Mocha可以自动运行所有的测试,并给出测试结果。

以下是一个JavaScript测试脚本,用contract()函数对一个合约进行测试,里面可以包含多个测试用例,每一个测试用例使用it指定。

例如,下面的测试代码用来验证合约的时候提供了两个候选人。

注:①断言:用于判断一个表达式,在表达式结果为False时触发异常。

如果不熟悉JavaScript,也可以选Solidity来编写测试,以下是使用Solidity完成同样功能的测试用例脚本。

测试用例脚本编写完之后,使用命令truffle test运行测试用例,它会在控制台打印出测试用例的通过情况,示例代码如下。

最后一行说明通过了两个测试用例。

如果说传统的互联网应用开发中,开发和测试的时间比是1:1,那么在智能合约开发中,测试的时间应该是开发时间的3倍,智能合约的测试需要更加重视,尽可能覆盖每一条语句,因为一旦合约中出现bug,就不像传统的互联网应用那样容易升级,你可能目睹黑客攻击而无能为力。

9.4.8 编写应用前端

在项目目录下新建一个src目录用来放置前端代码,新建一个html文件,使用table标签显示候选人列表(代码有删减,可对照作者在Github上提供的源码):

candidatesResultsid对应tbody的内容,稍后在JavaSript使用web3.js从合约中读取候选人信息后动态填入。

使用form标签来进行投票操作:

本案例的界面只需要这两段HTML就可以完成,接下来使用JavaScript来完成动态操作的部分。

9.4.9 前端与合约交互

新建一个文件app.js用来完成交互部分的功能,主要涉及三个部分的内容:

·初始化web3及合约·获取候选人填充到前端页面

·用户提交投票app

.js定义一个APP类,在类中使用不同的函数完成上面的功能。

1-web3及合约初始化

为了简单工程使用到的web3.js及truffle-contract.js,我已经在投票合约源代码提供了,大家可以通过HTML标签<script>直接引入,大一些的项目通常会使用npm来依赖包,在下一个案例会进行介绍。

在APP类中使用initWeb3函数,完成web3的初始化,示例代码如下。

Election.json是之前编译部署生成的构建文件,其中记录了合约的ABI及合约地址信息,initContract中使用jQuery函数获得Election.json的内容进而构造TruffleContract对象。

提示:Truffle生成的构建文件(本例中的Election.json)非常大,包含了合约的源码、编译后的字节码、编译器信息、文档等,在正式的产品中,一定要对构建文件进行精简再使用,否则将严重影响前端页面的加载速度


笔者在GitHub中开源了一段用于精简构建文件的脚本,读者可以使用,脚本地址:https://github.com/xilibi2003/truffle-min。

在Truffle项目中,我们通常会使用TruffleContract与合约交互,在前面9.2.3节我们介绍了web3.js与合约交互,TruffleContract其实是对web3.js与合约交互相关的API再进一步进行了封装,结合Truffle生成的构建文件,与合约交互的API更直观和精炼。

例如,使用web3.js获取候选人个数的代码大概是这样的:

而使用TruffleContract的话,代码大概是这样的:

TruffleContract使用方法简单,如果要了解更多,可以查看Truffle的文档:https://learnblockchain.cn/docs/truffle/reference/contract-abstractions.html。

2.界面渲染

有了合约对象就可以调用合约函数,获取候选人进行界面渲染,这就是render()函数完成的事情,示例代码如下。

代码中的几处注释分别表示:

①获取候选人数量;

②依次获取每一个候选人信息;

③将候选人信息写入候选人表格内;

④将候选人信息写入投票选项。

9.4.10 运行DAPP

由于本案例是一个Web应用,我们需要为它准备一个Web服务器,这里选择最简单的lite-server,使用npm安装lite-server,命令如下:

添加一个服务器配置文件:bs-config.json,这里主要是用来告诉lite-server服务器从哪些位置加载网页文件,bs-config.json配置如下:

baseDir就是用来配置lite-server的加载目录,./src是网页文件目录,./build/contracts是Truffle编译部署合约输出的构建文件的目录。

与此同时,在package.json文件的scripts中添加dev命令,以便我们使用npm命令启动lite-server,内容如下:

服务器启动在3000端口,在网页浏览器地址栏输入http://localhost:3000,就可以看到DAPP应用。

因为合约部署在本地的Ganache网络,因此需要把浏览器的MetaMask插件连接到Ganache网络,只有网络一致DAPP才可以读取到网络上的合约数据。

我们在5.5.2节介绍过如何切换到以太坊的Ropsten网络,方法类似,不过Ganache网络是属于自定义的网络,因此在网络列表下选择Custom RPC,然后使用http://127.0.0.1:7545作为RPC URL添加一个网络。

如果MetaMask中没有Ganache中的账号,可以从Ganache中获取一个私钥,使用MetaMask的账号导入功能导入Ganache内的账号。

9.4.11 部署到公网服务器

现在的DAPP仅可以通过localhost在本地访问,现实中,我们需要在DAPP.mydoname.com域名下访问DAPP,并且域名已经指向一台在公网可以访问的服务器。

接下来以Nginx(一个高性能的HTTP和反向代理Web服务器)Web服务器为例,介绍如何进行DAPP部署。

在Nginx上加入新的站点,如果是Linux系统,通常是在路径/etc/nginx/sites-enabled/中添加新的站点配置文件,如DAPP.conf,内容如下:

以上代码配置了站点的根目录:/home/www/DAPP,首页为index.html,然后把src及build/contracts目录下的文件复制到站点的根目录,即home/www/DAPP。

根目录包含如下文件:

之后在浏览器里就可以通过DAPP.mydoname.com来访问DAPP。

9.5 使用Vue.js开发众筹DAPP

9.5.1 Vue.js简介

Vue.js是一套在前端开发中广泛采用的用于构建用户界面的渐进式JavaScript框架。

Vue.js通过响应的数据绑定和组合的视图组件让界面开发变得非常简单。

除JavaScript框架之外,Vue.js还提供了一个配套的命令行工具Vue CLI,通常称之为脚手架工具,用来进行项目管理,用来实现比如快速开始零配置原型开发、安装插件库等功能。

Vue CLI可以通过以下命令安装:

运行以下命令来创建一个新项目crowdfunding:

命令会生成一个项目目录(稍后我们使用这个目录开发本案例),并安装好相应的依赖库,生成的主要文件有:

简单介绍一下Vue.js生成的文件,更多的使用介绍可参考Vue.js官方文档。

index.html是入口文件,里面定义了一个div标签:

在main.js中,会把APP.vue的组件内容渲染到id为app的div标签内:

APP.vue组件又引用了Hello.vue组件,而Hello.vue组件的内容则是图9-6的页面标签。

创建完成后进入目录,就可以运行项目,命令如下:

此时会在8080端口下启动一个Web服务,在浏览器中输入URL:http://localhost:8080,就可以打开如图9-6所示的界面。

图9-6 Vue.js默认启动界面截图

9.5.2 众筹需求分析

要完成一个项目,应该先进行需求分析,假设有这样一个场景:我准备写作一本区块链技术的图书,但是不确定有多少人愿意购买这本书。

于是,我发起一个众筹,如果在一个月内,能筹集到10个ETH,我就进行写作,并给参与的读者每人赠送一本书,如果未能筹到足够的资金,我就不进行写作,之前参与众筹的读者可以取回之前投入的资金。

同时,为了让读者积极参与,我设置了一个阶梯价格,初始时,参与众筹的价格非常低(0.02 ETH),每筹集满1个ETH时,价格上涨0.002ETH。

读者不妨先停一下想想,如果自己接到这样一个需求,应该如何实现。

众筹案例完整的代码我已经上传到GitHub:https://github.com/xilibi2003/crowdfunding,供大家参考。

从以上需求可以归纳出合约三个对外动作(函数):

(1)汇款进合约,可通过实现合约的回退函数来实现。

(2)读者赎回汇款,这个函数仅仅在众筹未达标之后,由读者本人调用生效。

(3)创作者提取资金,这个函数需要在众筹达标之后,由创作者调用

除此之外,进一步梳理逻辑,我们发现还需要保存一些状态变量以及添加相应的逻辑:

(1)记录用户众筹的金额,可以使用一个mapping类型来保存。

(2)记录当前众筹的价格,价格可以使用一个uint类型来保存(还需要一个函数来控制价格逐步上涨)。

(3)记录合约众筹的截止时间,用uint类型来保存截止时间戳,可以在构造函数中使用当前时间加上30天作为截止时间。

(4)记录合约众筹的收益者(即创作者),用address类型记录,在构造函数中记录合约创建者就是创作者。

(5)记录当前众筹状态(是否已经关闭),如果众筹达标(创作者提取资金时应及时关闭状态)之后,就需要阻止用户参与。

9.5.3 实现众筹合约

进入crowdfunding目录(前面我们使用Vue.js创建了这个目录),使用truffle init进行一次Truffle项目初始化:

初始化完成后,会在当前目录下生成truffle-config.js配置文件及contracts migrations文件夹等内容,Truffle项目初始化完成之后,就可以在项目下使用truffle compile来编译合约以及用truffle migrate来部署合约。

在contracts目录下创建一个合约文件Crowdfunding.sol:

代码的说明可参照注释,合约代码中使用到了Solidity中的一些知识点:

(1)ether:这是货币单位,在第4章介绍过。

(2)days:这是时间单位,1 days对应1天的秒数。

(3)now:这是一个Solidity的内置属性,用于获取当前的时间戳,单位是秒。

(4)require:在第6章的错误处理部分介绍过,如果条件不满足回退交易。

(5)address.transfer(value):对某一个地址进行转账。

9.5.4 合约部署

在migrations下创建一个部署脚本2_crowfunding.js,和投票合约类似,代码如下:

在truffe-config.js配置要部署的网络,同时确保对应的网络节点程序是开启状态,方法和投票合约案例中一样,然后就可以用命令truffle migrate进行部署。

9.5.5 众筹Web界面实现

Vue.js创建项目时,默认会有一个HelloWorld.vue,我们新写一个自己的组件CrowdFund.vue,并把App.vue中对HelloWorld.vue的引用替换掉。

App.vue修改为:

然后在CrowdFund.vue里完成众筹界面及相应逻辑,众筹界面需要显示以下几个部分:

(1)当前众筹到的金额。

(2)众筹的截止时间。

(3)当前众筹的价格,参与众筹按钮。

(4)如果是已经参与,显示其参与的价格以及赎回按钮。

(5)如果是创作者,显示一个提取资金按钮。

因为Vue.js具有很好的数据绑定及条件渲染特性,因此前端代码写起来会比上一个案例更简单,可以直接在HTML模板中使用从合约中获取数据的变量,Vue.js在渲染时变量替换为对应的数据,代码如下:

代码中使用Vue.js的特性包含:

(1)使用v-if进行条件渲染,例如v-if="joined"表示当joined变量为true时,才渲染该标签。

(2)使用{{变量}}进行数据绑定,例如:<b>{{price}} ETH </b>,price会用其真实的值进行渲染,并且当price变量的值更新时,标签才会自动更新。

(3)使用@click指令来监听事件,@click实际上是v-on:click的缩写,例如@click="join"表示当标签单击时,会调用join()函数。

(4)使用:disabled绑定一个属性,这实际是v-bind:disabled,属性的值来源于一个变量。

如果读者对Vue.js不了解,可以先在网上阅读Vue.js的官方教程,再来阅读本节。

9.5.6 与众筹合约交互

接下来编写JavaScript逻辑部分,前端界面与合约进行交互时,需要用到truffle-contract及web3,因为Vue.js本身也是通过npm进行包管理,因此可以直接通过npm进行安装,命令如下:

在CrowdFund组件中,我们用几个变量保存从合约获取的值,再加上相应的初始化,这样组件逻辑的主体框架代码就出来了:

以上代码通过data()定义好了HTML模板中使用的变量,当Vue组件被创建时通过回调的created()函数来进行初始化工作(这里使用了async/await来简化异步调用),在created()函数中调用了三个函数:

·initWeb3Account()

·initContract()

·getCrowdInfo()我们接下来依次实现三个函数。

initWeb3Account()用来完成web3及账号初始化,代码和投票案例基本类似,代码如下:

这段代码完成了this.provider、this.web3、this.account三个变量的赋值,在后面的代码中会被用到。

initContract()初始化合约实例如下:

第2行的Crowdfunding.json是Truffle编译部署输出的构建文件,同样注意,正式产品中应该使用压缩后的文件。

this.crowdFund变量就是部署的众筹合约JavaScript实例,之后就可以通过this.crowdFund来调用合约的函数,获取相关变量的值,在getCrowdInfo()函数完成这一步:

解释代码中使用到的几个技术点。

(1)合约实例this.crowdFund调用的函数joined()、closed()、price()是由合约中public类型的状态变量相应自动生成的访问器函数。

可以回顾本书6.2.8节。

(2)代码中使用的this.web3.eth.getBalance()和this.web3.utils.fromWei()是web3.js中定义的函数,分别用来获取余额及把单位从wei转化为ether。

至此,完成DAPP状态数据的获取,接下来开始处理3个单击动作(即HTML模板中@click触发的函数):

(1)读者参与众筹的join()函数;

(2)读者赎回的withdraw()函数;

(3)创作者提取资金的withdrawFund()函数。

join()函数实际上是由读者账号向众筹合约账号发起一笔转账,通过web3.eth.sendTransaction完成,代码如下:

读者进行转账时,就会触发合约的接收函数。

如果众筹未达标,读者可以单击赎回按钮,对应的withdraw()函数实现如下:

[插图]如果众筹达标,创作者提取资金withdrawFund()函数实现如下:

到这里众筹案例就全部完成了,完整的代码参考网址:https://github.com/xilibi2003/crowdfunding。

9.5.7 DAPP运行

在项目的目录下,输入以下命令:

在浏览器地址栏输入网址:http://localhost:8080,效果如图9-7(1)所示。

图9-7(1) 第一次参与众筹的界面

如果已经参与过众筹,界面如图9-7(2)所示。

图9-7(2) 以前参与过众筹的界面

因为我还有一个创作者的身份,因此图9-7(2)还显示一个“提取资金”按钮。

在运行DAPP时,要确保MetaMask链接的网络和合约部署的网络(此例中使用的是development网络)一致,这样DAPP才能正确地通过web3获取合约的数据。

9.5.8 DAPP发布

Vue内建一个用来构建前端页面的命令,我们只需要简单输入以下命令:

它就会在dist目录下,构建出用于发布的完整的前端代码,其文件如下:

index.html就是DAPP前端入口文件,把dist目录下的所有文件拷贝到公网服务器即可。

9.6 后台监听合约事件

在上面的众筹案例中,每个参与者可以看到自己的参与状态,创作者却没有办法查看所有参与者,有两个办法可以实现查看所有参与者:

(1)加入一个状态变量:address[] joinAccounts,这是一个数组,用来记录所有参与者的地址,每当有新的参与者进来时,往数组中加入参与者地址。

(2)通过触发事件把参与者地址记录到日志中,然后启动一个服务程序监听事件,当事件触发时,把参与者地址记录到数据库中,并提供一个后端服务,把数据库中的参与者列表返回给前端。

两种方法各有优缺点:

方法1的gas消耗会远高于方法2,优点是不需要额外引入服务器;

方法2则相反,使用事件的方法2其实还有一个好处,就是可以实时监听到事件的变化(通常对应着链上状态的变化),这在一些场合非常有用。

本节将主要介绍方法2,看看如何通过后台服务,监听事件的变化,本例中我们将使用Node.js及Express框架作为后台服务(读者也可以选用其他技术栈作为后台服务,技术原理一样)。

9.6.1 Node.js及Express简介

Node.js就是运行在服务端的JavaScript。

Node.js是一个基于Google的V8引擎建立的服务端JavaScript运行环境,速度快,性能好。

Express是一个简洁而灵活的node.js Web应用框架,提供了一系列强大特性帮助创建各种Web应用。

下面通过编写一个简单的Hello World程序,来看看如何使用Express。

在众筹项目目录下新建一个文件夹server,进入此目录并将其作为后端项目工作目录,命令如下:

然后通过npm init命令进行初始化,npm init会创建一个package.json文件进行包管理,同时还会要求我们输入几个参数,例如此应用的名称和版本,命令效果如下:

大部分直接按“回车”键接受默认设置即可,这个时候,就可以看到在server目录下产生了一个pacckage.json文件,继续安装Express,命令如下:

到此,环境安装就完成了,新建一个文件index.js,编写一段服务端的程序HelloWorld,代码如下:

上面代码中,引入了express模块,它在后台常驻运行,并监听3000端口,当客户端发起请求后,响应“Hello World!”字符串,通过以下命令启动服务:

启动后,在浏览器访问地址:http://localhost:3000,就可以看到Hello World!,如图9-8所示。

图9-8 访问Express截图

9.6.2 常驻服务监听合约事件

Express会启动一个常驻在后台的服务,对刚刚编写的index.js进行修改,加入web3.js相关代码以实现监听合约事件。

不过我们需要先在Crowdfunding合约中加入一个事件:

然后在接收函数中触发这个事件:

修改合约后,使用truffle migrate --reset命令重新编译部署(或使用truffle migrate-f2指定第2个部署脚本编译)。

回到后端,在server目录下安装web3:

然后修改index.js,在index.js中加入监听Join事件代码,代码如下:

除以上注释外,对以上代码关键点进行如下介绍。

·在初始化web3时,使用了WebsocketProvider,通过WebSocket通信协议与节点通信,如果是使用Geth节点,需要使用选项--ws开启服务,开发使用的Ganache默认开启了WebSocket服务。

补充说明:由于HTTP协议只能单向通信,通信只能由客户端发起,客户端通过"轮询"(周期性地查询状态)服务器获取状态的更新,而WebSocket支持双向通行,服务端可以主动向客户端推送信息,客户端也可以主动向服务器发送信息(注意:Express服务在与节点程序通信时,节点是服务端,Express服务是客户端)。

·获取合约实例时,使用的合约的ABI及合约地址参数,均是通过Truffle编译部署生成的构建文件Crowdfunding.json获取。

·通过Web3合约实例后,可以通过myContract.events.EventName()函数传入一个回调函数监听事件的变化。

更多的监听事件可以查看文档:https://learnblockchain.cn/docs/web3.js/web3-eth-contract.html#events。

重新启动后台服务:

另开一个命令行控制台窗口,启动前端:

在浏览器地址栏中输入:http://localhost:8080/,进入前端页面,单击“参与众筹”,切换到后端的命令行控制台窗口,可以看到打印出四条日志记录:

为了方便管理,接下来把监听到的数据写入数据库里。

9.6.3 MySQL数据库环境准备

这里以MySQL数据库为例进行介绍(读者也可以根据自己的喜好选用其他的数据库,比如PostgreSQL数据库)。

第一步,安装MySQL服务器,进入MySQL官方网站下载页面,选择对应版本进行下载(或根据安装指引命令行安装),如图9-9所示。

图9-9 MySQL下载页面

MySQL在不同平台下的安装提示略有差别,一般会默认提供一个默认的用户root,用于登录MySQL服务器,在安装过程中提示我们设置root密码(如果没有提示,可以在MySQL日志中找到默认密码)。

如果觉得自己安装MySQL有些吃力,也可以选择集成开发环境,如Linux平台的LAMP、Windows平台的WAMP以及Mac平台的MAMP,它们有更友好的图形界面来管理服务器。

使用Mac电脑安装完MAMP之后,效果如图9-10所示,截图右上角的绿点表示MySQL服务器已启动。

图9-10 MAMP运行截图

通过“启动”页面可以查看MySQL服务器的信息,如图9-11所示。

图9-11 MySQL端口、登录名及密码

9.6.4 创建数据库及表

有了前面连接数据库的信息,就可以通过MySQL的客户端连接上MySQL服务器,MySQL客户端有很多选择,可以选择命令行方式,例如:

也可以选择MySQL官方的客户端MySQL Workbench,如图9-12所示。

图9-12 下载MySQL Workbench

笔者自己使用的是Sequel Pro(Mac平台),各种客户端连接服务器的方式都类似,需要填写如图9-13所示的几个信息。

图9-13 连接MySQL服务器

连接上MySQL服务器,使用以下命令创建数据库和表来存储众筹数据。

以上MySQL代码可以复制到MySQL客户端中执行,它创建名为crowdfund的数据库,并在数据库中创建joins表,joins表用来存储用户参与众筹的信息,包含的列有:主键id、地址address、价格price、交易hash、区块号blockNo。

9.6.5 监听数据入库

先安装node-mysql驱动,它提供在Node.js程序里连接MySQL服务器的接口,安装命令如下:

并在上面监听打印日志的后面调用insertJoins()函数插入数据库,调用代码如下:

使用node重新启动服务:

在DAPP前端页面单击“参与众筹”后(如果当前账号参与过,就切换不同的账号参与众筹),如果一切正常,就可以在数据库查询到相应的众筹记录,使用SQL语句select * from joins,如图9-14所示。

图9-14 查询数据

在图9-14中,显示已经有3个用户参与了众筹。

9.6.6 为前端提供众筹记录

通过读取数据库的数据,并在Express加入一个路由,接受前端请求后返回读取到的数据,在index.js先加入一个getJoins()函数来读取数据库数据:

getJoins()和insertJoins()都需要获取数据库链接,为了代码复用,抽象出getConn()函数:

使用node index.js,重新启动index.js,浏览器访问:http://localhost:3000/joins,将返回参与众筹用户的JSON数组:

接下来前端组件就可以通过Ajax请求访问http://localhost:3000/joins接口获取众筹用户列表,前端可以使用Axios(它是一个基于promise的HTTP库)来发起HTTP请求,在项目根目录下通过npm命令来安装Axios:

修改前端CrowdFund.vue,加入获取众筹列表的getJoins()函数:

以上代码把Axios获取到的众筹列表赋值给joinList变量,在HTML模板中加入joinList的渲染:

再次用yarn serve启动前端应用,浏览器地址栏输入:http://localhost:8080,在MetaMask中切换到创作者账号,可以看到输出的众筹列表,效果如图9-15所示。

图9-15 展示众筹列表

到这里,我们就完成在9.6节开头所设的目标——创作者查看所有参与用户,我们也简单作一个小结,在众筹DAPP案例中,我们通过Vue.js开发应用前端,使用Node.js后端监控合约事件,并且将一些核心数据用中心化数据库进行了缓存。

其实这就囊括所有在DAPP开发中涉及的内容。限于篇幅有限,本节对于Vue.js、Node.js以及数据库MySQL的使用说明仅仅是抛砖引玉,没有深入介绍,如果读者不熟悉相关知识点,可以进一步阅读相关材料。

书中展示的案例代码略有删减,完整代码请查看GitHub代码库:https://github.com/xilibi2003/crowdfunding。

9.7 DAPP去中心化存储

前面实现的DAPP,不管是投票还是众筹,都通过智能合约实现了规则的透明、运行时的去中心化(假定部署在主网,并公开验证了源码),细心的读者也许会发现一个问题,用户参与交互的前端页面是来自中心化的Web服务器,以致我们的去中心化不那么纯粹。

这是当前HTTP协议的规则决定的,简单来讲,当我们在浏览器里输入一个URL后,总是会先找到这个URL(域名)对应的服务器IP地址(DNS域名解析),然后请求服务器,并把服务器的响应在浏览器中渲染。

如果中心化服务器对显示内容有完全的控制权,那么内容就有被篡改或删除的风险。

9.7.1 IPFS协议

IPFS(InterPlanetary File System,星际文件系统)是一种根据内容可寻址、版本化、点对点的分布式存储、传输协议。

我们知道,在现在的网络服务里,内容是基于位置(IP)寻址的,就是说在查找内容的时候,需要先找到内容所在的服务器(根据IP),然后再到服务器上寻找对应的内容。

而在IPFS的网络里是根据内容寻址,每一个上传到IPFS上面去的文件、文件夹,都是以Qm为开头字母的哈希值,无须知道文件存储在哪里,通过哈希值就能够找到这个文件,这种方式叫内容寻址。

IPFS的目标取代HTTP,为我们构建一个更好的去中心化的Web,如果IPFS能够得到普及,访问内容就按照如下的方式:

不过当前的浏览器都无法支持ipfs://文件hash访问内容,目前依靠浏览器插件ipfs伴侣,通过一个网关访问内容。

IPFS的几个特点:

(1)当知道一个文件的哈希值之后,可以确保文件不被修改,即可以确保访问的文件是没有被篡改的。因为根据哈希的特点,哪怕源文件有一丁点儿的更改,对应的哈希值也会完全不同。

(2)(理论上)如果IPFS得以普及,节点数达到一定规模,内容将永久保存,就算部分节点离线,也不会影响文件的读取。

(3)由于IPFS是一个统一的网络,只要文件在网络中被存储过,除了必要的冗余备份,文件不会被重复存储,对比现有互联网的信息孤岛,各中心之间不共享数据,数据不得不重复存储,IPFS一定意义上节约了空间,使得整个网络带宽的消耗更低,网络更加高效。

(4)相对于中心化存储容易遭受DDOS攻击,IPFS采用分布式存储网络,文件被存储在不同的网络节点,天然避免了DDOS攻击,同时,一个文件可以同时从多个节点下载,通信的效率也会更高。

IPNS

在IPFS中,一个文件的哈希值完全取决于其内容,修改它的内容,其对应的哈希值也会发生改变。

这样有一个优点是保证文件的不可篡改,提高数据的安全性。

但同时我们在开发应用(如DAPP)时,经常需要更新内容发布新版本,如果每次都让用户在浏览器中输入不同的IPFS地址来访问更新后内容的网页,这个体验就会非常糟糕。

IPFS提供了一个解决方案IPNS(Inter-Planetary Naming System),它提供了一个被私钥限定的IPNS哈希ID(通常是PeerID),用来指向具体IPFS文件哈希,当有新的内容更新时,就可以更新IPNS哈希ID的指向。

为了方便读者理解,作一个类比,和DNS类似,DNS记录了域名指向的IP地址,如果服务器更改,我们可以更改DNS域名指向,保证域名指向最新的服务器。

IPNS则是用一个哈希ID指向一个真实内容文件的哈希,文件更新时就可以更改哈希ID的指向,当然更新指向需要有哈希ID对应的私钥。

9.7.2 IPFS安装

要使用IPFS,第一步肯定是先把IPFS安装好,IPFS在Mac OSX、Linux及Window平台均有提供,可以通过链接https://dist.ipfs.io/#go-ipfs下载对应平台可执行文件的压缩包。

对于Mac OS X及Linux平台,使用以下命令进行安装:

上面先使用tar对压缩包进行解压,然后执行install.sh进行安装,安装脚本install.sh其实就是把可执行文件ipfs移动到$PATH目录下。

安装完成之后,可以在命令行终端敲入ipfs试试看,如果显示一堆命令说明,则说明IPFS安装成功。

在Windows平台也是类似的,把ipfs.exe移动到环境变量%PATH%指定的目录下。

9.7.3 IPFS初始化

安装完成之后,使用IPFS的第一步,是对IPFS进行初始化,使用ipfs init进行初始化:

上面是执行命令及对应输出,在执行ipfs init进行初始化时,会执行以下动作:

·生成一个密钥对并产生对应的节点身份id,在上面ipfs init命令输出的内容提示:peer identity后面的哈希值。

节点的身份id用来标识和连接一个节点,每个节点的身份id是独一无二的,因此大家看到的提示也会和这里的不一样。

·在当前用户的主目录下产生一个

.ipfs的隐藏目录,这个目录称之为库(repository)目录,IPFS所有相关的数据都会放在这个目录下。

例如,同步文件数据块放在.ipfs/blocks目录,密钥放在.ipfs/keystore目录,IPFS配置文件为.ipfs/config。

9.7.4 上传文件到IPFS

先创建一个tinyxiong.txt文件,可以使用命令行方式创建:

IPFS使用add命令来添加内容到节点,命令如下:

当它把文件添加到节点时,会为文件生成唯一的哈希:QmcPwAPCWkwi5pHqxmPP wgS9vMEx7okvaX2tYkCSxeg5kj,可以使用ipfs cat查看文件的内容:

注意:此时文件仅仅是在本地的IPFS节点中,如果需要把文件同步到网络,就需要开启daemon服务,使用命令:

开启daemon之后,它就会尝试连接其他的节点,同步数据,通过以下命令可以获得它所连接节点的信息:

同时,在本地还会开启两个服务:API服务及Web网关服务。

Web网关服务默认在8080端口,由于当前浏览器还不支持通过IPFS协议(ipfs://)来访问文件,如果我们要在浏览器里访问文件的话,就需要借助于IPFS提供的网关服务,由浏览器先访问网关,网关去获取IPFS网络上的文件。

刚刚上传的文件可以通过这个链接访问:

http://127.0.0.1:8080/ipfs/QmcPwAPCWkwi5pHqxmPPwgS9vMEx7okvaX2tYkCS xeg5kj,浏览器访问后的结果如图9-16所示。

图9-16 IPFS网管访问截图

IPFS也提供了官方的网关服务:

https://ipfs.io/,因此也可以通过https://ipfs.io/ipfs/QmcPwAPCWkwi5pHqxmPPwgS9vMEx7okvaX2tYkCSxeg5kj来访问刚刚上传到ipfs的文件。

Infura也提供了IPFS网关服务,通过https://ipfs.infura.io/ipfs/QmcPwAPCWkwi5pH qxmPPwgS9vMEx7okvaX2tYkCSxeg5kj也同样可以访问到tinyxiong.txt。

API服务配套了一个IPFS Web版的管理控制台,可以通过http://localhost:5001/webui进行访问,通过这个控制台添加文件、查看节点连接情况等,如图9-17所示。

图9-17 IPFS管理控制台界面

9.7.5 上传目录到IPFS

我们先创建一个文件夹upchain,并把之前的tinyxiong.txt放进目录:

上传目录到IPFS需要在使用add命令时加上-r,示例如下:

在上传文件夹时,文件夹也会生成一个对应的哈希,可以通过哈希后接文件名来进行访问,例如:

在浏览器地址栏可以输入:http://127.0.0.1:8080/ipfs/QmcbnUuTyuqGErHtbdpn6Tmr5zXJzBHN4Vs26LsiEv1fo7/tinyxiong.txt来访问文件。

通过上传目录的方式,我们可以把DAPP前端的整个目录上传到IPFS,实现前端的去中心化。

不过,如果页面不是使用相对路径引用css、js等文件的话,通过IPFS访问index.html时,页面控制台会提示一些404错误,找不到相应的引用文件,有兴趣的读者可以自己尝试一下,下面要介绍的Embark框架集成了IPFS,就可以解决这个问题。

9.8 Embark框架

9.8.1 Embark概述

和前面介绍的Truffle类似,Embark也是一个功能强大的DAPP开发框架,它可以帮助开发者快速构建和部署DAPP。

Embark不单可以与以太坊区块链通信,还集成了IPFS/Swarm去中心化存储和Whisper网络通信功能。

Embark有以下特点:

·合约自动部署,Embark启动后会监听合约的更改,并自动部署。

·提供命令行工具,比如可直接与合约交互等。·提供了非常方便的Debug和测试工具。

·集成去中心化存储IPFS等,可以方便地把DAPP部署到IPFS等网络上,实现完全的去中心化。

·方便使用Whisper协议实现点对点的信息通信。

·提供状态面板(dashboard)及Cockpit辅助应用程序,方便查看合约信息、账号信息、交易状态等,甚至进行代码修改及调试。

9.8.2 Embark安装

Embark安装前需要先安装Geth、IPFS,在Geth安装及IPFS安装完之后,通过NPM安装Embark:

可以通过查看软件版本来验证安装是否正确:

9.9 Embark重写投票DAPP

9.9.1 创建Embark项目

Embark提供new命令创建新项目,命令如下:

控制台输入embark new embark-election命令后,会利用模板创建一个名为embark-election的项目,并安装好相应的依赖库,以下是embark new embark-election的运行输出:

来看一下embark项目的文件结构。

9.9.2 Embark项目结构

进入embark-election之后,可以看到项目下主要包含了以下文件:

·app:DAPP的前端代码放在这个目录下,前端代码可以使用自己喜欢的框架来编写,如Vue、Angular、React等。

·contracts:智能合约的源代码放在这个目录下,Embark启动后,默认会跟踪文件夹下合约文件的变化,自动编译部署合约。

·config:包含了不同模块的配置。

〇blockchain.js:连接区块链的网络配置,如rpc地址、端口、账号等。

〇contracts.js:配置DAPP连接及部署合约参数等。

〇storage.js:配置分布式存储组件(如IPFS)及相应连接参数。

〇communication.js:配置通信协议(如Whisper)及相应连接参数。

〇webserver.js:配置启动DAPP的Web服务,如服务地址和端口。

config下的每个配置文件都有默认配置。

·test:合约测试脚本放在这个目录下,支持使用Solidity和JavaScript编写测试用例。

·dist:DAPP构建后,所有需要部署的文件都放置在这个目录内。

·embark.json:Embark项目本身的配置,比如可以更改项目的目录结构、编译器版本等。

9.9.3 编写合约及部署

在项目的contracts目录下新建一个合约文件Election.sol,在前面第4节,已经编写过这个代码,可以直接复制过来。

Embark合约部署的配置在config/contracts.js,在deploy字段加入Election合约:

现在运行embark run,Embark会自动编译及部署Election.sol到config/blockchain.js配置的development网络。

因为embark run等价embark run development(最后一个参数表示对应的网络)。

blockchain.js中的development网络是使用ganache-cli启动的网络,其配置如下:

Embark启动后,Election.sol合约的部署日志会在Embark的DashBoard和Cockpit中看到,类似下面的内容:

9.9.4 Embark DashBoard

Embark框架提供的DashBoard和Cockpit,是两个非常强大的开发者工具,图9-18所示是DashBoard的界面截图。

图9-18 DashBoard的界面截图

在这个面板中可以看到合约的部署状态、部署地址、当前连接的网络、服务的状态以及Embark运行日志,在DashBoard最下方还有一个交互式控制台,在这个控制台可以直接使用Web3 API,并且可以直接使用合约名调用合约方法,例如调用candidatesCount获得有多少个候选人:

结果会在日志区域输出。

也可以直接使用web3对象,如果要发起一个转账,可以输入以下命令:

交易式控制台也可以直接通过命令embark console进入。

9.9.5 Embark Cockpit

Cockpit比DashBoard更为强大,它集合了DashBoard、区块链浏览器、代码编辑IDE(包含代码的编辑、编译运行及调试)以及一些常用工具集。

DashBoard的日志里提供了一个链接用来进入Cockpit,注意查看一下日志:

URL中的token是基于安全考虑的,防止其他人通过Cockpit访问到我们的Embark进程,也可以通过在控制台中输入token命令来获得token的值。

进入Cockpit,顶部的5个菜单——Dashboard、Deployment、Explorer、Editor、Utils,分别对应5个功能,我们看看区块链浏览器Explorer(见图9-19)及代码编辑器的界面(见图9-20)。

图9-19 Cockpit区块链浏览器

图9-20 Cockpit代码编辑器

在Cockpit中对代码的更改可直接保存到本地文件系统,非常方便。

9.9.6 Embark Artifacts

合约已经部署好了,接下来需要编写前端与合约交互。Embark提供了一个EmbarkJS的JavaScript库,来帮助开发者和合约进行交互。

在使用web3.js时,跟合约交互需要知道合约的ABI及地址,以创建JS环境中对应的合约对象,一般代码是这样的:

Embark在编译部署后,每个合约会生成一个对应的构件Artifact(可以在embarkArtifacts/contracts/目录下找到这些文件),我们可以直接使用Artifact生成的合约对象调用合约。

一个构件通常会包含合约的ABI、部署地址、启动代码及其他的配置数据。

查看一下Election.sol对应的构件Election.js代码就更容易理解上面这句话:

Election.js最后一行导出了一个与合约同名的JavaScript对象,接下来我们看看怎么使用这个对象。

9.9.7 前端index.html

在使用embark new embark-election创建项目时,前端app/目录下生成了一个index.html:

这里有一个地方需要注意一下,第4.5行引入了css/app.css和js/app.js,而其实app/下并没有这两个文件,这两个文件其实是按照embark.json配置的规则生成的。

embark.json关于前端的配置如下:

"css/app.css": ["app/css/**"]表示所有在app/css/目录下的文件会被压缩到dist目录下的css/app.css,app/js/index.js则会编译为js/app.js,其他的配置类似。

embark以这个方式来统一引用css及js代码文件,应该就是为了在IPFS之类的去中心化存储上访问起来更方便,在IPFS上传整个目录时,只能以相对路径去访问资源

接下来修改前端部分的代码,主要是在index.html的body加入一个table标签用来显示候选人,以及加入一个投票框,示例代码如下(节选):

我们使用了bootstrap前端样式库,把文件拷贝到app/css目录下,接下来看关键的一步:前端如何与合约交互。

9.9.8 使用Artifacts与合约交互

EmbarkJS连接Web3

创建项目时生成的app/js/index.js产生了如下代码:

这段代码里,EmbarkJS为我们准备了一个onReady回调函数,这是因为EmbarkJS会自动帮我们完成与web3节点的连接与初始化,当这些就绪后会回调用在onReady注册的函数上,前端就可以和链进行交互了。

大家也许会好奇,EmbarkJS怎么知道我们需要连接那个节点呢?

其实在config/contracts.js中有一个DAPPConnection配置项:

$EMBARK是Embark在DAPP和节点之间实现的一个代理,使用$EMBARK有几个好处:

(1)可以在config/blockchain.js配置与DAPP交互的账号accounts。

(2)可以更友好地看到交易记录。EmbarkJS会从上到下依次尝试DAPPConnection提供的链接,如果有一个可以链接上,就会停止尝试。

获取合约数据渲染界面

当EmbarkJS环境准备好后,在onReady回调函数中,就可以使用构件Election.js获取合约数据,如获取调用合约进而获得候选人数量:

代码中直接使用构件导出的Election对象,调用合约方法为Election.methods.candidatesCount().call(),调用合约方法与web3.js一致。

了解完如何与合约交互,接下来渲染界面就简单了,我们把代码整理下,分别定义3个函数:

App.getAccount()、App.render()、App.onVote()来获取当前账号(需要用来判断哪些账号投过票)、界面渲染、处理投票。

App.getAccount()的实现如下:

在代码中,我们直接使用了web3对象,就是因为EmbarkJS帮我们进行了web3的初始化。另外,我们引入jquery.min.js来进行UI界面的渲染。

App.render()的实现(主干)如下:

9.9.9 Embark部署

使用embark run时,Embark会为我们启动一个Geth或ganache-cli的本地网络部署合约,以及在8000端口上启用一个本地服务器来部署前端应用,我们在浏览器输入http://localhost:8000/就可以看到DAPP界面,如图9-21所示。

图9-21 DAPP界面

当我们的DAPP在测试环境通过后,就可以部署到以太坊的主网。

利用Infura部署合约

要部署到主网,需要在blockchain.js中添加一个主网网络,这里以测试网Ropsten网络为例:

如果我们没有自己的主网节点,可以使用endpoint来指向外部节点,最常用的就是Infura。

添加好配置之后,使用build命令来构建主网发布版本:

所有的文件生成在dist目录下,把它们部署到线上服务器就完成了部署。

DAPP部署到IPFS

由于Embark集成了IPFS服务,可以直接使用embark upload命令把DAPP部署到IPFS,如使用embark upload ropsten命令:

embark upload的日志会输出具体上传了哪些内容,上传完成之后,控制台会提示我们可以通过多个IPFS网关访问DAPP,例如本地的IPFS网关可以使用链接:

http://localhost:8080/ipfs/QmQWecdHKjXUpKCvEV4JWveEo2QqMaeEVs6UN4k7nbU6e1/,显示的内容和之前用http://localhost:8000访问的内容一样。

部署之后,就可以通过IPFS网络来访问DAPP。

       

【声明】内容源于网络
0
0
数组智控产业发展科技院
以AI技术为底层能力,聚焦智慧园区、城市公共安全、数智警务、健康医疗、能源电力、科研实验及平安校园等领域,提供从感知到决策的全流程软硬件一体化的国产装备智能体产品解决方案。
内容 986
粉丝 0
数组智控产业发展科技院 以AI技术为底层能力,聚焦智慧园区、城市公共安全、数智警务、健康医疗、能源电力、科研实验及平安校园等领域,提供从感知到决策的全流程软硬件一体化的国产装备智能体产品解决方案。
总阅读2.5k
粉丝0
内容986