以太坊Dapp终极教程——如何构建一个完整的全栈去中心化应用(二)

以太坊Dapp终极教程——如何构建一个完整的全栈去中心化应用(一)中,我们已经完成了一切所需的设置,让我们通过列出将在选举中运行的候选人来继续构建智能联系。我们需要一种方法来存储多个候选者,并存储关于每个候选者的多个属性。我们希望跟踪候选人的身份,姓名和投票计数。以下是我们如何为候选人建模:

1
2
3
4
5
6
7
8
9
10
contract Election {
// Model a Candidate
struct Candidate {
uint id;
string name;
uint voteCount;
}

// ...
}

我们使用Solidity Struct为候选人建模。Solidity允许我们创建自己的结构类型,就像我们在这里为候选人所做的那样。我们指定此结构具有无符号整数类型的id,字符串类型的名称和无符号整数类型的voteCount。简单地声明这个结构实际上不会给我们一个候选人。我们需要实例化它并将其分配给变量,然后才能将其写入存储。

接下来我们需要的是存放候选人的地方。我们需要一个地方来存储我们刚刚创建的结构类型之一。我们可以使用Solidity mapping来完成此操作。Solidity中的映射类似于关联数组或散列,它将键值对关联起来。我们可以像这样创建这个映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
contract Election {
// Model a Candidate
struct Candidate {
uint id;
string name;
uint voteCount;
}

// Read/write Candidates
mapping(uint => Candidate) public candidates;

// ...
}

在这种情况下,映射的关键是无符号整数,值是我们刚刚定义的候选结构类型。这基本上为我们提供了基于身份的每个候选人的查找。由于此映射已分配给状态变量,因此只要我们为其分配新的键值对,我们就会将数据写入区块链。接下来,我们将此映射的可见性设置为public以获取getter函数,就像我们在冒烟测试中使用候选名称一样。

接下来,我们使用计数器缓存状态变量跟踪选举中存在多少候选者,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
contract Election {
// Model a Candidate
struct Candidate {
uint id;
string name;
uint voteCount;
}

// Read/write Candidates
mapping(uint => Candidate) public candidates;

// Store Candidates Count
uint public candidatesCount;

// ...
}

在Solidity中,无法确定映射的大小,也无法迭代它。这是因为尚未赋值的映射中的任何键都将返回默认值(在这种情况下为空候选)。例如,如果我们在这次选举中只有2名候选人,并且我们尝试查找候选人#99,那么映射将返回空的候选人结构。此行为使得无法知道存在多少候选项,因此我们必须使用计数器缓存。

接下来,让我们创建一个函数来将候选添加到我们创建的映射中,如下所示:

1
2
3
4
5
6
7
8
contract Election {
// ...

function addCandidate (string _name) private {
candidatesCount ++;
candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);
}
}

我们已经声明了函数addCandidate,它接受一个表示候选者名称的字符串类型参数。在函数内部,我们递增候选计数器缓存以表示已添加新候选者。然后我们使用当前候选计数作为关键字,用新的Candidate结构更新映射。使用当前候选计数中的候选ID,函数参数中的名称以及初始投票计数来初始化此Candidate结构。请注意,此函数的可见性是私有的,因为我们只想在合约中调用它。

现在我们可以通过在构造函数中调用两次addCandidate函数来添加两个候选者,如下所示:

1
2
3
4
5
6
7
8
9
10
contract Election {
// ...

function Election () public {
addCandidate("Candidate 1");
addCandidate("Candidate 2");
}

// ...
}

当我们将合约部署到区块链时,将执行此迁移,并使用两个候选人填充我们的选举。此时,你的完整合约代码应如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pragma solidity ^0.4.2;

contract Election {
// Model a Candidate
struct Candidate {
uint id;
string name;
uint voteCount;
}

// Read/write candidates
mapping(uint => Candidate) public candidates;

// Store Candidates Count
uint public candidatesCount;

function Election () public {
addCandidate("Candidate 1");
addCandidate("Candidate 2");
}

function addCandidate (string _name) private {
candidatesCount ++;
candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);
}

}

现在让我们像这样迁移我们的合约:

1
$ truffle migrate --reset

现在尝试与控制台内的候选人进行交互。

现在让我们编写一些测试来确保我们的智能合约正确初始化。首先,让我解释为什么在开发智能合约时测试非常重要。我们希望确保合约没有错误,原因如下:

  • 1.以太坊区块链上的所有代码都是不可变的;它无法改变。如果合约包含任何错误,我们必须禁用它并部署新副本。此新副本与旧合约的状态不同,它将具有不同的地址。
  • 2.部署合约需要耗费gas,因为它会创建一个交易并将数据写入区块链。这会花费Ether,我们希望尽量减少我们必须支付的以太网数量。
  • 3.如果我们写入区块链的任何合约函数都包含错误,则调用此函数的帐户可能会浪费Ether,并且可能不会按预期方式运行。

测试

现在让我们写一些测试。确保你先运行Ganache。然后,从项目的根目录在命令行中创建一个新的测试文件,如下所示:

1
$ touch test/election.js

我们将使用Mocha测试框架和Chai断言库在此文件中的Javascript中编写所有测试。这些与Truffle框架捆绑在一起。我们将在Javascript中编写所有这些测试,以模拟与智能合约的客户端交互,就像我们在控制台中所做的那样。以下是测试的所有代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var Election = artifacts.require("./Election.sol");

contract("Election", function(accounts) {
var electionInstance;

it("initializes with two candidates", function() {
return Election.deployed().then(function(instance) {
return instance.candidatesCount();
}).then(function(count) {
assert.equal(count, 2);
});
});

it("it initializes the candidates with the correct values", function() {
return Election.deployed().then(function(instance) {
electionInstance = instance;
return electionInstance.candidates(1);
}).then(function(candidate) {
assert.equal(candidate[0], 1, "contains the correct id");
assert.equal(candidate[1], "Candidate 1", "contains the correct name");
assert.equal(candidate[2], 0, "contains the correct votes count");
return electionInstance.candidates(2);
}).then(function(candidate) {
assert.equal(candidate[0], 2, "contains the correct id");
assert.equal(candidate[1], "Candidate 2", "contains the correct name");
assert.equal(candidate[2], 0, "contains the correct votes count");
});
});
});

让我解释一下这段代码。首先,我们要求合约并将其分配给变量,就像我们在迁移文件中所做的那样。接下来,我们调用合约函数contract,并在回调函数中编写所有测试。此回调函数提供了一个帐户Accounts变量,表示我们的区块链上的所有帐户,由Ganache提供。

第一次测试通过检查候选人数等于2来检查合约是否已使用正确数量的候选人进行初始化。

下一个测试检查每个候选人在选举中的价值,确保每个候选人都有正确的身份证,姓名和投票计数。

现在让我们从命令行运行测试,如下所示:

1
$ truffle test

是的,他们通过了!

客户端应用程序

现在让我们开始构建将与智能合约对话的客户端应用程序。我们将通过修改我们在上一节中安装的Truffle Pet Shop框附带的HTML和Javascript文件来完成此操作。我们将使用此现有代码开始。我们还要注意Truffle Pet Shop盒子附带的一些其他东西,比如Bootstrap框架,它将使我们不必在本教程中编写任何CSS。我们还有lite-server,它将为我们的资产提供服务以用于开发目的。

你不必是前端专家就可以按照本教程的这一部分进行操作。我故意保持HTML和Javascript代码非常简单,我们不会花太多时间专注于它。我想继续专注于开发dApp的智能合约部分!

继续使用以下代码替换index.html文件的所有内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Election Results</title>

<!-- Bootstrap -->
<link href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container" style="width: 650px;">
<div class="row">
<div class="col-lg-12">
<h1 class="text-center">Election Results</h1>
<hr/>
<br/>
<div id="loader">
<p class="text-center">Loading...</p>
</div>
<div id="content" style="display: none;">
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">Votes</th>
</tr>
</thead>
<tbody id="candidatesResults">
</tbody>
</table>
<hr/>
<p id="accountAddress" class="text-center"></p>
</div>
</div>
</div>
</div>

<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="js/bootstrap.min.js"></script>
<script src="js/web3.min.js"></script>
<script src="js/truffle-contract.js"></script>
<script src="js/app.js"></script>
</body>
</html>

接下来,使用以下代码替换app.js文件的所有内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
App = {
web3Provider: null,
contracts: {},
account: '0x0',

init: function() {
return App.initWeb3();
},

initWeb3: function() {
if (typeof web3 !== 'undefined') {
// If a web3 instance is already provided by Meta Mask.
App.web3Provider = web3.currentProvider;
web3 = new Web3(web3.currentProvider);
} else {
// Specify default instance if no web3 instance provided
App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
web3 = new Web3(App.web3Provider);
}
return App.initContract();
},

initContract: function() {
$.getJSON("Election.json", function(election) {
// Instantiate a new truffle contract from the artifact
App.contracts.Election = TruffleContract(election);
// Connect provider to interact with contract
App.contracts.Election.setProvider(App.web3Provider);

return App.render();
});
},

render: function() {
var electionInstance;
var loader = $("#loader");
var content = $("#content");

loader.show();
content.hide();

// Load account data
web3.eth.getCoinbase(function(err, account) {
if (err === null) {
App.account = account;
$("#accountAddress").html("Your Account: " + account);
}
});

// Load contract data
App.contracts.Election.deployed().then(function(instance) {
electionInstance = instance;
return electionInstance.candidatesCount();
}).then(function(candidatesCount) {
var candidatesResults = $("#candidatesResults");
candidatesResults.empty();

for (var i = 1; i <= candidatesCount; i++) {
electionInstance.candidates(i).then(function(candidate) {
var id = candidate[0];
var name = candidate[1];
var voteCount = candidate[2];

// Render candidate Result
var candidateTemplate = "<tr><th>" + id + "</th><td>" + name + "</td><td>" + voteCount + "</td></tr>"
candidatesResults.append(candidateTemplate);
});
}

loader.hide();
content.show();
}).catch(function(error) {
console.warn(error);
});
}
};

$(function() {
$(window).load(function() {
App.init();
});
});

让我们注意一下这段代码所做的一些事情:

  • 设置web3:web3.js是一个javascript库,允许我们的客户端应用程序与区块链交谈。我们在initWeb3函数中配置web3。
  • 初始化合约:我们在此函数中获取智能合约的已部署实例,并分配一些允许我们与之交互的值。
  • 渲染功能:渲染功能使用智能合约中的数据列出页面上的所有内容。目前,我们列出了我们在智能合约中创建的候选人。我们通过遍历映射中的每个候选项并将其呈现给表来完成此操作。我们还获取连接到此功能内的区块链的当前帐户并将其显示在页面上。

现在让我们在浏览器中查看客户端应用程序。首先,确保你已按照以下方式迁移合约:

1
$ truffle migrate --reset

接下来,从命令行启动你的开发服务器,如下所示:

1
$ npm run dev

这应该会自动使用客户端应用程序打开一个新的浏览器窗口。

请注意你的应用程序显示正在加载loading...。那是因为我们还没有登录到区块链!为了连接到区块链,我们需要将其中一个帐户从Ganache导入Metamask。

与Metamask连接后,你应该会看到所有合约和帐户数据都已加载。

======================================================================

分享一些比特币、以太坊、EOS、Fabric等区块链相关的交互式在线编程实战教程:

  • java比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Java代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Java工程师不可多得的比特币开发学习课程。
  • php比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Php代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Php工程师不可多得的比特币开发学习课程。
  • c#比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在C#代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是C#工程师不可多得的比特币开发学习课程。
  • java以太坊开发教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
  • python以太坊,主要是针对python工程师使用web3.py进行区块链以太坊开发的详解。
  • php以太坊,主要是介绍使用php进行智能合约开发交互,进行账号创建、交易、转账、代币开发以及过滤器和交易等内容。
  • 以太坊入门教程,主要介绍智能合约与dapp应用开发,适合入门。
  • 以太坊开发进阶教程,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
  • ERC721以太坊通证实战,课程以一个数字艺术品创作与分享DApp的实战开发为主线,深入讲解以太坊非同质化通证的概念、标准与开发方案。内容包含ERC-721标准的自主实现,讲解OpenZeppelin合约代码库二次开发,实战项目采用Truffle,IPFS,实现了通证以及去中心化的通证交易所。
  • C#以太坊,主要讲解如何使用C#开发基于.Net的以太坊应用,包括账户管理、状态与交易、智能合约开发与交互、过滤器和交易等。
  • EOS入门教程,本课程帮助你快速入门EOS区块链去中心化应用的开发,内容涵盖EOS工具链、账户与钱包、发行代币、智能合约开发与部署、使用代码与智能合约交互等核心知识点,最后综合运用各知识点完成一个便签DApp的开发。
  • 深入浅出玩转EOS钱包开发,本课程以手机EOS钱包的完整开发过程为主线,深入学习EOS区块链应用开发,课程内容即涵盖账户、计算资源、智能合约、动作与交易等EOS区块链的核心概念,同时也讲解如何使用eosjs和eosjs-ecc开发包访问EOS区块链,以及如何在React前端应用中集成对EOS区块链的支持。课程内容深入浅出,非常适合前端工程师深入学习EOS区块链应用开发。
  • Hyperledger Fabric 区块链开发详解,本课程面向初学者,内容即包含Hyperledger Fabric的身份证书与MSP服务、权限策略、信道配置与启动、链码通信接口等核心概念,也包含Fabric网络设计、nodejs链码与应用开发的操作实践,是Nodejs工程师学习Fabric区块链开发的最佳选择。
  • Hyperledger Fabric java 区块链开发详解,课程面向初学者,内容即包含Hyperledger Fabric的身份证书与MSP服务、权限策略、信道配置与启动、链码通信接口等核心概念,也包含Fabric网络设计、java链码与应用开发的操作实践,是java工程师学习Fabric区块链开发的最佳选择。
  • tendermint区块链开发详解,本课程适合希望使用tendermint进行区块链开发的工程师,课程内容即包括tendermint应用开发模型中的核心概念,例如ABCI接口、默克尔树、多版本状态库等,也包括代币发行等丰富的实操代码,是go语言工程师快速入门区块链开发的最佳选择。

汇智网原创翻译,转载请标明出处。这里是以太坊Dapp终极教程——如何构建一个完整的全栈去中心化应用