我有一壶酒,足以慰平生。

0%

web3.js

介绍 Web3.js

完成第五课以后,我们的僵尸 DApp 的 Solidity 合约部分就完成了。现在我们来做一个基本的网页好让你的用户能玩它。 要做到这一点,我们将使用以太坊基金发布的 JavaScript 库 —— Web3.js\.

什么是 Web3.js?

还记得么?以太坊网络是由节点组成的,每一个节点都包含了区块链的一份拷贝。当你想要调用一份智能合约的一个方法,你需要从其中一个节点中查找并告诉它:

  1. 智能合约的地址
  2. 你想调用的方法,以及
  3. 你想传入那个方法的参数

以太坊节点只能识别一种叫做 JSON-RPC\ 的语言。这种语言直接读起来并不好懂。当你你想调用一个合约的方法的时候,需要发送的查询语句将会是这样的:

1
2
3
// 哈……祝你写所有这样的函数调用的时候都一次通过
// 往右边拉…… ==>
{"jsonrpc":"2.0","method":"eth_sendTransaction","params":[{"from":"0xb60e8dd61c5d32be8058bb8eb970870f07233155","to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","gas":"0x76c0","gasPrice":"0x9184e72a000","value":"0x9184e72a","data":"0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"}],"id":1}

幸运的是 Web3.js 把这些令人讨厌的查询语句都隐藏起来了, 所以你只需要与方便易懂的 JavaScript 界面进行交互即可。

你不需要构建上面的查询语句,在你的代码中调用一个函数看起来将是这样:

1
2
CryptoZombies.methods.createRandomZombie("Vitalik Nakamoto 🤔")
.send({ from: "0xb60e8dd61c5d32be8058bb8eb970870f07233155", gas: "3000000" })

我们将在接下来的几章详细解释这些语句,不过首先我们来把 Web3.js 环境搭建起来。

准备好了么?

取决于你的项目工作流程和你的爱好,你可以用一些常用工具把 Web3.js 添加进来:

1
2
3
4
5
6
7
8
9
10
// 用 NPM
npm install web3

// 用 Yarn
yarn add web3

// 用 Bower
bower install web3

// ...或者其他。

甚至,你可以从 github 直接下载压缩后的 .js 文件 然后包含到你的项目文件中:

1
<script language="javascript" type="text/javascript" src="web3.min.js"></script>

因为我们不想让你花太多在项目环境搭建上,在本教程中我们将使用上面的 script 标签来将 Web3.js 引入。

实战演习

我们为你建立了一个HTML 项目空壳 —— index.html。假设在和 index.html 同个文件夹里有一份 web3.min.js

  1. 使用上面的 script 标签代码把 web3.js 添加进去以备接下来使用。

页面创建

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CryptoZombies front-end</title>
<script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<!-- Include web3.js here -->
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
</head>
<body>

</body>
</html>

Web3 提供者

太棒了。现在我们的项目中有了Web3.js, 来初始化它然后和区块链对话吧。

首先我们需要 Web3 Provider\.

要记住,以太坊是由共享同一份数据的相同拷贝的 节点 构成的。 在 Web3.js 里设置 Web3 的 Provider(提供者) 告诉我们的代码应该和 哪个节点 交互来处理我们的读写。这就好像在传统的 Web 应用程序中为你的 API 调用设置远程 Web 服务器的网址。

你可以运行你自己的以太坊节点来作为 Provider。 不过,有一个第三方的服务,可以让你的生活变得轻松点,让你不必为了给你的用户提供DApp而维护一个以太坊节点— Infura\.

Infura

Infura 是一个服务,它维护了很多以太坊节点并提供了一个缓存层来实现高速读取。你可以用他们的 API 来免费访问这个服务。 用 Infura 作为节点提供者,你可以不用自己运营节点就能很可靠地向以太坊发送、接收信息。

你可以通过这样把 Infura 作为你的 Web3 节点提供者:

1
var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));

不过,因为我们的 DApp 将被很多人使用,这些用户不单会从区块链读取信息,还会向区块链 入信息,我们需要用一个方法让用户可以用他们的私钥给事务签名。

注意: 以太坊 (以及通常意义上的 blockchains )使用一个公钥/私钥对来对给事务做数字签名。把它想成一个数字签名的异常安全的密码。这样当我修改区块链上的数据的时候,我可以用我的公钥来 证明 我就是签名的那个。但是因为没人知道我的私钥,所以没人能伪造我的事务。

加密学非常复杂,所以除非你是个专家并且的确知道自己在做什么,你最好不要在你应用的前端中管理你用户的私钥。

不过幸运的是,你并不需要,已经有可以帮你处理这件事的服务了: Metamask\.

Metamask

Metamask 是 Chrome 和 Firefox 的浏览器扩展, 它能让用户安全地维护他们的以太坊账户和私钥, 并用他们的账户和使用 Web3.js 的网站互动(如果你还没用过它,你肯定会想去安装的——这样你的浏览器就能使用 Web3.js 了,然后你就可以和任何与以太坊区块链通信的网站交互了)

作为开发者,如果你想让用户从他们的浏览器里通过网站和你的DApp交互(就像我们在 CryptoZombies 游戏里一样),你肯定会想要兼容 Metamask 的。

注意: Metamask 默认使用 Infura 的服务器做为 web3 提供者。 就像我们上面做的那样。不过它还为用户提供了选择他们自己 Web3 提供者的选项。所以使用 Metamask 的 web3 提供者,你就给了用户选择权,而自己无需操心这一块。

使用 Metamask 的 web3 提供者

Metamask 把它的 web3 提供者注入到浏览器的全局 JavaScript对象web3中。所以你的应用可以检查 web3 是否存在。若存在就使用 web3.currentProvider 作为它的提供者。

这里是一些 Metamask 提供的示例代码,用来检查用户是否安装了MetaMask,如果没有安装就告诉用户需要安装MetaMask来使用我们的应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
window.addEventListener('load', function() {

// 检查web3是否已经注入到(Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// 使用 Mist/MetaMask 的提供者
web3js = new Web3(web3.currentProvider);
} else {
// 处理用户没安装的情况, 比如显示一个消息
// 告诉他们要安装 MetaMask 来使用我们的应用
}

// 现在你可以启动你的应用并自由访问 Web3.js:
startApp()

})

你可以在你所有的应用中使用这段样板代码,好检查用户是否安装以及告诉用户安装 MetaMask。

注意: 除了MetaMask,你的用户也可能在使用其他他的私钥管理应用,比如 Mist 浏览器。不过,它们都实现了相同的模式来注入 web3 变量。所以我这里描述的方法对两者是通用的。

实战演习

我们在HTML文件中的 </body> 标签前面放置了一个空的 script 标签。可以把这节课的 JavaScript 代码写在里面。

  1. 把上面用来检测 MetaMask 是否安装的模板代码粘贴进来。请粘贴到以 window.addEventListener 开头的代码块中。

页面修改

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CryptoZombies front-end</title>
<script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
</head>
<body>

<script>
// Start here
window.addEventListener('load', function() {

// 检查web3是否已经注入到(Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// 使用 Mist/MetaMask 的提供者
web3js = new Web3(web3.currentProvider);
} else {
// 处理用户没安装的情况, 比如显示一个消息
// 告诉他们要安装 MetaMask 来使用我们的应用
}

// 现在你可以启动你的应用并自由访问 Web3.js:
startApp()

})
</script>
</body>
</html>

和合约对话

现在,我们已经用 MetaMask 的 Web3 提供者初始化了 Web3.js。接下来就让它和我们的智能合约对话吧。

Web3.js 需要两个东西来和你的合约对话: 它的 地址 和它的 ABI\

合约地址

在你写完了你的智能合约后,你需要编译它并把它部署到以太坊。我们将在下一课中详述部署,因为它和写代码是截然不同的过程,所以我们决定打乱顺序,先来讲 Web3.js。

在你部署智能合约以后,它将获得一个以太坊上的永久地址。如果你还记得第二课,CryptoKitties 在以太坊上的地址是 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d

你需要在部署后复制这个地址以来和你的智能合约对话。

合约 ABI

另一个 Web3.js 为了要和你的智能合约对话而需要的东西是 ABI\

ABI 意为应用二进制接口(Application Binary Interface)。 基本上,它是以 JSON 格式表示合约的方法,告诉 Web3.js 如何以合同理解的方式格式化函数调用。

当你编译你的合约向以太坊部署时(我们将在第七课详述), Solidity 编译器会给你 ABI,所以除了合约地址,你还需要把这个也复制下来。

因为我们这一课不会讲述部署,所以现在我们已经帮你编译了 ABI 并放在了名为cryptozombies_abi.js,文件中,保存在一个名为 cryptoZombiesABI 的变量中。

如果我们将cryptozombies_abi.js 包含进我们的项目,我们就能通过那个变量访问 CryptoZombies ABI 。

实例化 Web3.js

一旦你有了合约的地址和 ABI,你可以像这样来实例化 Web3.js。

1
2
// 实例化 myContract
var myContract = new web3js.eth.Contract(myABI, myContractAddress);

实战演习

  1. 在文件的 <head> 标签块中,用 script 标签引入cryptozombies_abi.js,好把 ABI 的定义引入项目。
  2. <body> 里的 <script> 开头 , 定义一个var,取名 cryptoZombies, 不过不要对其赋值,稍后我们将用这个这个变量来存储我们实例化合约。
  3. 接下来,创建一个名为 startApp()function。 接下来两步来完成这个方法。
  4. startApp() 里应该做的第一件事是定义一个名为cryptoZombiesAddress 的变量并赋值为"你的合约地址" (这是你的合约在以太坊主网上的地址)。
  5. 最后,来实例化我们的合约。模仿我们上面的代码,将 cryptoZombies 赋值为 new web3js.eth.Contract (使用我们上面代码中通过 script 引入的 cryptoZombiesABIcryptoZombiesAddress)。

页面修改

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CryptoZombies front-end</title>
<script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
<!-- Include cryptozombies_abi.js here -->
<script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
</head>
<body>

<script>
// 2. Start code here
var cryptoZombies;
function startApp() {
var cryptoZombiesAddress = "你的合约地址";
cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI,cryptoZombiesAddress);
}

window.addEventListener('load', function() {

// Checking if Web3 has been injected by the browser (Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// Use Mist/MetaMask's provider
web3js = new Web3(web3.currentProvider);
} else {
// Handle the case where the user doesn't have Metamask installed
// Probably show them a message prompting them to install Metamask
}

// Now you can start your app & access web3 freely:
startApp()

})
</script>
</body>
</html>

调用和合约函数

我们的合约配置好了!现在来用 Web3.js 和它对话。

Web3.js 有两个方法来调用我们合约的函数: call and send.

Call

call 用来调用 viewpure 函数。它只运行在本地节点,不会在区块链上创建事务。

复习: viewpure 函数是只读的并不会改变区块链的状态。它们也不会消耗任何gas。用户也不会被要求用MetaMask对事务签名。

使用 Web3.js,你可以如下 call 一个名为myMethod的方法并传入一个 123 作为参数:

1
myContract.methods.myMethod(123).call()

Send

send 将创建一个事务并改变区块链上的数据。你需要用 send 来调用任何非 view 或者 pure 的函数。

注意: send 一个事务将要求用户支付gas,并会要求弹出对话框请求用户使用 Metamask 对事务签名。在我们使用 Metamask 作为我们的 web3 提供者的时候,所有这一切都会在我们调用 send() 的时候自动发生。而我们自己无需在代码中操心这一切,挺爽的吧。

使用 Web3.js, 你可以像这样 send 一个事务调用myMethod 并传入 123 作为参数:

1
myContract.methods.myMethod(123).send()

语法几乎 call()一模一样。

获取僵尸数据

来看一个使用 call 读取我们合约数据的真实例子

回忆一下,我们定义我们的僵尸数组为 公开(public):

1
Zombie[] public zombies;

在 Solidity 里,当你定义一个 public变量的时候, 它将自动定义一个公开的 “getter” 同名方法, 所以如果你像要查看 id 为 15 的僵尸,你可以像一个函数一样调用它: zombies(15).

这是如何在外面的前端界面中写一个 JavaScript 方法来传入一个僵尸 id,在我们的合同中查询那个僵尸并返回结果

注意: 本课中所有的示例代码都使用 Web3.js 的 1.0 版,此版本使用的是 Promises 而不是回调函数。你在线上看到的其他教程可能还在使用老版的 Web3.js。在1.0版中,语法改变了不少。如果你从其他教程中复制代码,先确保你们使用的是相同版本的Web3.js。

1
2
3
4
5
6
7
8
9
function getZombieDetails(id) {
return cryptoZombies.methods.zombies(id).call()
}

// 调用函数并做一些其他事情
getZombieDetails(15)
.then(function(result) {
console.log("Zombie 15: " + JSON.stringify(result));
});

我们来看看这里都做了什么

cryptoZombies.methods.zombies(id).call() 将和 Web3 提供者节点通信,告诉它返回从我们的合约中的 Zombie[] public zombiesid为传入参数的僵尸信息。

注意这是 异步的,就像从外部服务器中调用API。所以 Web3 在这里返回了一个 Promises. (如果你对 JavaScript的 Promises 不了解,最好先去学习一下这方面知识再继续)。

一旦那个 promiseresolve, (意味着我们从 Web3 提供者那里获得了响应),我们的例子代码将执行 then 语句中的代码,在控制台打出 result

result 是一个像这样的 JavaScript 对象:

1
2
3
4
5
6
7
8
{
"name": "H4XF13LD MORRIS'S COOLER OLDER BROTHER",
"dna": "1337133713371337",
"level": "9999",
"readyTime": "1522498671",
"winCount": "999999999",
"lossCount": "0" // Obviously.
}

我们可以用一些前端逻辑代码来解析这个对象并在前端界面友好展示。

实战演习

我们已经帮你把 getZombieDetails 复制进了代码。

  1. 先为zombieToOwner 创建一个类似的函数。如果你还记得 ZombieFactory.sol,我们有一个长这样的映射:

    1
    mapping (uint => address) public zombieToOwner;

    定义一个 JavaScript 方法,起名为 zombieToOwner。和上面的 getZombieDetails 类似, 它将接收一个id 作为参数,并返回一个 Web3.js call 我们合约里的zombieToOwner

  2. 之后在下面,为 getZombiesByOwner 定义一个方法。如果你还能记起 ZombieHelper.sol,这个方法定义像这样:

    1
    function getZombiesByOwner(address _owner)

    我们的 getZombiesByOwner 方法将接收 owner 作为参数,并返回一个对我们函数 getZombiesByOwner的 Web3.js call

页面修改

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
50
51
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CryptoZombies front-end</title>
<script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
<script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
</head>
<body>

<script>
var cryptoZombies;

function startApp() {
var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
}

function getZombieDetails(id) {
return cryptoZombies.methods.zombies(id).call()
}

// 1. Define `zombieToOwner` here
function zombieToOwner(id) {
return cryptoZombies.methods.zombieToOwner(id).call()
}

// 2. Define `getZombiesByOwner` here
function getZombiesByOwner(owner){
return cryptoZombies.methods.getZombiesByOwner(owner).call()
}

window.addEventListener('load', function() {

// Checking if Web3 has been injected by the browser (Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// Use Mist/MetaMask's provider
web3js = new Web3(web3.currentProvider);
} else {
// Handle the case where the user doesn't have Metamask installed
// Probably show them a message prompting them to install Metamask
}

// Now you can start your app & access web3 freely:
startApp()

})
</script>
</body>
</html>

MetaMask 和账户

太棒了!你成功地写了一些前端代码来和你的第一个智能合约交互。

接下来我们综合一下——比如我们想让我们应用的首页显示用户的整个僵尸大军。

毫无疑问我们首先需要用 getZombiesByOwner(owner) 来查询当前用户的所有僵尸ID。

但是我们的 Solidity 合约需要 owner 作为 Solidity address。我们如何能知道应用用户的地址呢?

获得 MetaMask中的用户账户

MetaMask 允许用户在扩展中管理多个账户。

我们可以通过这样来获取 web3 变量中激活的当前账户:

1
var userAccount = web3.eth.accounts[0]

因为用户可以随时在 MetaMask 中切换账户,我们的应用需要监控这个变量,一旦改变就要相应更新界面。例如,若用户的首页展示它们的僵尸大军,当他们在 MetaMask 中切换了账号,我们就需要更新页面来展示新选择的账户的僵尸大军。

我们可以通过 setInterval 方法来做:

1
2
3
4
5
6
7
8
var accountInterval = setInterval(function() {
// 检查账户是否切换
if (web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
// 调用一些方法来更新界面
updateInterface();
}
}, 100);

这段代码做的是,每100毫秒检查一次 userAccount 是否还等于 web3.eth.accounts[0] (比如:用户是否还激活了那个账户)。若不等,则将 当前激活用户赋值给 userAccount,然后调用一个函数来更新界面。

实战演习

我们来让应用在页面第一次加载的时候显示用户的僵尸大军,监控当前 MetaMask 中的激活账户,并在账户发生改变的时候刷新显示。

  1. 定义一个名为userAccount的变量,不给任何初始值。

  2. startApp()函数的最后,复制粘贴上面样板代码中的 accountInterval 方法进去。

  3. updateInterface();替换成一个 getZombiesByOwnercall 函数,并传入 userAccount

  4. getZombiesByOwner 后面链式调用then 语句,并将返回的结果传入名为 displayZombies 的函数。 (语句像这样: .then(displayZombies);).

    我们还没有 displayZombies 函数,将于下一章实现。

页面修改

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CryptoZombies front-end</title>
<script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
<script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
</head>
<body>

<script>
var cryptoZombies;
// 1. declare `userAccount` here
var userAccount;

function startApp() {
var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

// 2. Create `setInterval` code here
var accountInterval = setInterval(function() {
// 检查账户是否切换
if (web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
// 调用一些方法来更新界面
getZombiesByOwner(userAccount).then(displayZombies);
}
}, 100);

}

function getZombieDetails(id) {
return cryptoZombies.methods.zombies(id).call()
}

function zombieToOwner(id) {
return cryptoZombies.methods.zombieToOwner(id).call()
}

function getZombiesByOwner(owner) {
return cryptoZombies.methods.getZombiesByOwner(owner).call()
}

window.addEventListener('load', function() {

// Checking if Web3 has been injected by the browser (Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// Use Mist/MetaMask's provider
web3js = new Web3(web3.currentProvider);
} else {
// Handle the case where the user doesn't have Metamask installed
// Probably show them a message prompting them to install Metamask
}

// Now you can start your app & access web3 freely:
startApp()


})
</script>
</body>
</html>

显示僵尸大军

如果我们不向你展示如何显示你从合约获取的数据,那这个教程就太不完整了。

在实际应用中,你肯定想要在应用中使用诸如 React 或 Vue.js 这样的前端框架来让你的前端开发变得轻松一些。不过要教授 React 或者 Vue.js 知识的话,就大大超出了本教程的范畴——它们本身就需要几节课甚至一整个教程来教学。

所以为了让 CryptoZombies.io 专注于以太坊和智能合约,我们将使用 JQuery 来做一个快速示例,展示如何解析和展示从智能合约中拿到的数据。

显示僵尸数据 — 一个粗略的例子

我们已经在代码中添加了一个空的代码块 <div id="zombies"></div>, 在 displayZombies 方法中也同样有一个。

回忆一下在之前章节中我们在 startApp() 方法内部调用了 displayZombies 并传入了 call getZombiesByOwner 获得的结果,它将被传入一个僵尸ID数组,像这样:

1
[0, 13, 47]

因为我们想让我们的 displayZombies 方法做这些事:

  1. 首先清除 #zombies 的内容以防里面已经有什么内容(这样当用户切换账号的时候,之前账号的僵尸大军数据就会被清除)
  2. 循环遍历 id,对每一个id调用 getZombieDetails(id), 从我们的合约中获得这个僵尸的数据。
  3. 将获得的僵尸数据放进一个HTML模板中以格式化显示,追加进 #zombies 里面。

再次声明,我们只用了 JQuery,没有任何模板引擎,所以会非常丑。不过这只是一个如何展示僵尸数据的示例而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 在合约中查找僵尸数据,返回一个对象
getZombieDetails(id)
.then(function(zombie) {
// 用 ES6 的模板语法来向HTML中注入变量
// 把每一个都追加进 #zombies div
$("#zombies").append(`<div class="zombie">
<ul>
<li>Name: ${zombie.name}</li>
<li>DNA: ${zombie.dna}</li>
<li>Level: ${zombie.level}</li>
<li>Wins: ${zombie.winCount}</li>
<li>Losses: ${zombie.lossCount}</li>
<li>Ready Time: ${zombie.readyTime}</li>
</ul>
</div>`);
});

如何来展示僵尸元素呢?

在上面的例子中,我们只是简单地用字符串来显示 DNA。不过在你的 DApp 中,你将需要把 DNA 转换成图片来显示你的僵尸。

我们通过把 DNA 字符串分割成小的字符串来做到这一点,每2位数字代表一个图片,类似这样:

1
2
3
4
5
// 得到一个 1-7 的数字来表示僵尸的头:
var head = parseInt(zombie.dna.substring(0, 2)) % 7 + 1

// 我们有7张头部图片:
var headSrc = "../assets/zombieparts/head-" + i + ".png"

每一个模块都用 CSS 绝对定位来显示,在一个上面叠加另外一个。

如果你想看我们的具体实现,我们将用来展示僵尸形象的 Vue.js 模块开源了: 点击这里.

不过,因为那个文件中有太多行代码, 超出了本教程的讨论范围。我们依然还是使用上面超级简单的 JQuery 实现,把美化僵尸的工作作为家庭作业留给你了😉

实战演习

我们为你创建了一个空的 displayZombies 方法。来一起实现它。

  1. 首先我们需要清空 #zombies 的内容。 用JQuery,你可以这样做: $("#zombies").empty();
  2. 接下来,我们要循环遍历所有的 id,循环这样用: for (id of ids) {
  3. 在循环内部,复制粘贴上面的代码,对每一个id调用 getZombieDetails(id),然后用 $("#zombies").append(...) 把内容追加进我们的 HTML 里面。

页面修改

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
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CryptoZombies front-end</title>
<script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
<script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
</head>
<body>
<div id="zombies"></div>

<script>
var cryptoZombies;
var userAccount;

function startApp() {
var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

var accountInterval = setInterval(function() {
// Check if account has changed
if (web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
// Call a function to update the UI with the new account
getZombiesByOwner(userAccount)
.then(displayZombies);
}
}, 100);
}

function displayZombies(ids) {
// Start here
$("#zombies").empty();
for (id of ids) {
getZombieDetails(id).then(function(zombie) {
$("#zombies").append(`<div class="zombie">
<ul>
<li>Name: ${zombie.name}</li>
<li>DNA: ${zombie.dna}</li>
<li>Level: ${zombie.level}</li>
<li>Wins: ${zombie.winCount}</li>
<li>Losses: ${zombie.lossCount}</li>
<li>Ready Time: ${zombie.readyTime}</li>
</ul>
</div>`);
});
}
}

function getZombieDetails(id) {
return cryptoZombies.methods.zombies(id).call()
}

function zombieToOwner(id) {
return cryptoZombies.methods.zombieToOwner(id).call()
}

function getZombiesByOwner(owner) {
return cryptoZombies.methods.getZombiesByOwner(owner).call()
}

window.addEventListener('load', function() {

// Checking if Web3 has been injected by the browser (Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// Use Mist/MetaMask's provider
web3js = new Web3(web3.currentProvider);
} else {
// Handle the case where the user doesn't have Metamask installed
// Probably show them a message prompting them to install Metamask
}

// Now you can start your app & access web3 freely:
startApp()

})
</script>
</body>
</html>

发送事务

这下我们的界面能检测用户的 MetaMask 账户,并自动在首页显示它们的僵尸大军了,有没有很棒?

现在我们来看看用 send 函数来修改我们智能合约里面的数据。

相对 call 函数,send 函数有如下主要区别:

  1. send 一个事务需要一个 from 地址来表明谁在调用这个函数(也就是你 Solidity 代码里的 msg.sender )。 我们需要这是我们 DApp 的用户,这样一来 MetaMask 才会弹出提示让他们对事务签名。

  2. send 一个事务将花费 gas

  3. 在用户 send 一个事务到该事务对区块链产生实际影响之间有一个不可忽略的延迟。这是因为我们必须等待事务被包含进一个区块里,以太坊上一个区块的时间平均下来是15秒左右。如果当前在以太坊上有大量挂起事务或者用户发送了过低的 gas 价格,我们的事务可能需要等待数个区块才能被包含进去,往往可能花费数分钟。

    所以在我们的代码中我们需要编写逻辑来处理这部分异步特性。

生成一个僵尸

我们来看一个合约中一个新用户将要调用的第一个函数: createRandomZombie.

作为复习,这里是合约中的 Solidity 代码:

1
2
3
4
5
6
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
randDna = randDna - randDna % 100;
_createZombie(_name, randDna);
}

这是如何在用 MetaMask 在 Web3.js 中调用这个函数的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createRandomZombie(name) {
// 这将需要一段时间,所以在界面中告诉用户这一点
// 事务被发送出去了
$("#txStatus").text("正在区块链上创建僵尸,这将需要一会儿...");
// 把事务发送到我们的合约:
return cryptoZombies.methods.createRandomZombie(name)
.send({ from: userAccount })
.on("receipt", function(receipt) {
$("#txStatus").text("成功生成了 " + name + "!");
// 事务被区块链接受了,重新渲染界面
getZombiesByOwner(userAccount).then(displayZombies);
})
.on("error", function(error) {
// 告诉用户合约失败了
$("#txStatus").text(error);
});
}

我们的函数 send 一个事务到我们的 Web3 提供者,然后链式添加一些事件监听:

  • receipt 将在合约被包含进以太坊区块上以后被触发,这意味着僵尸被创建并保存进我们的合约了。
  • error 将在事务未被成功包含进区块后触发,比如用户未支付足够的 gas。我们需要在界面中通知用户事务失败以便他们可以再次尝试。

注意:你可以在调用 send 时选择指定 gasgasPrice, 例如: .send({ from: userAccount, gas: 3000000 })。如果你不指定,MetaMask 将让用户自己选择数值。

实战演习

我们添加了一个div, 指定 ID 为 txStatus — 这样我们可以通过更新这个 div 来通知用户事务的状态。

  1. displayZombies下面, 复制粘贴上面 createRandomZombie 的代码。

  2. 我们来实现另外一个函数 feedOnKitty

    调用 feedOnKitty 的逻辑几乎一样 — 我们将发送一个事务来调用这个函数,并且成功的事务会为我们创建一个僵尸,所以我们希望在成功后重新绘制界面。

    createRandomZombie 下面复制粘贴它的代码,改动这些地方:

    a) 给其命名为 feedOnKitty, 它将接收两个参数 zombieIdkittyId

    b) #txStatus 的文本内容将更新为: "正在吃猫咪,这将需要一会儿..."

    c) 让其调用我们合约里面的 feedOnKitty 函数并传入相同的参数

    d) #txStatus 里面的的成功信息应该是 "吃了一只猫咪并生成了一只新僵尸!"

页面修改

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
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CryptoZombies front-end</title>
<script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
<script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
</head>
<body>
<div id="txStatus"></div>
<div id="zombies"></div>

<script>
var cryptoZombies;
var userAccount;

function startApp() {
var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

var accountInterval = setInterval(function() {
// Check if account has changed
if (web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
// Call a function to update the UI with the new account
getZombiesByOwner(userAccount)
.then(displayZombies);
}
}, 100);
}

function displayZombies(ids) {
$("#zombies").empty();
for (id of ids) {
// Look up zombie details from our contract. Returns a `zombie` object
getZombieDetails(id)
.then(function(zombie) {
// Using ES6's "template literals" to inject variables into the HTML.
// Append each one to our #zombies div
$("#zombies").append(`<div class="zombie">
<ul>
<li>Name: ${zombie.name}</li>
<li>DNA: ${zombie.dna}</li>
<li>Level: ${zombie.level}</li>
<li>Wins: ${zombie.winCount}</li>
<li>Losses: ${zombie.lossCount}</li>
<li>Ready Time: ${zombie.readyTime}</li>
</ul>
</div>`);
});
}
}

// Start here
function createRandomZombie(name) {
// 这将需要一段时间,所以在界面中告诉用户这一点
// 事务被发送出去了
$("#txStatus").text("正在区块链上创建僵尸,这将需要一会儿...");
// 把事务发送到我们的合约:
return cryptoZombies.methods.createRandomZombie(name)
.send({ from: userAccount })
.on("receipt", function(receipt) {
$("#txStatus").text("成功生成了 " + name + "!");
// 事务被区块链接受了,重新渲染界面
getZombiesByOwner(userAccount).then(displayZombies);
})
.on("error", function(error) {
// 告诉用户合约失败了
$("#txStatus").text(error);
});
}
function feedOnKitty(zombieId, kittyId) {
$("#txStatus").text("正在吃猫咪,这将需要一会儿...");
return cryptoZombies.methods.feedOnKitty(zombieId, kittyId)
.send({ from: userAccount })
.on("receipt", function(receipt) {
$("#txStatus").text("吃了一只猫咪并生成了一只新僵尸!");

// 事务被区块链接受了,重新渲染界面
getZombiesByOwner(userAccount).then(displayZombies);
})
.on("error", function(error) {
// 告诉用户合约失败了
$("#txStatus").text(error);
});
}

function getZombieDetails(id) {
return cryptoZombies.methods.zombies(id).call()
}

function zombieToOwner(id) {
return cryptoZombies.methods.zombieToOwner(id).call()
}

function getZombiesByOwner(owner) {
return cryptoZombies.methods.getZombiesByOwner(owner).call()
}

window.addEventListener('load', function() {

// Checking if Web3 has been injected by the browser (Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// Use Mist/MetaMask's provider
web3js = new Web3(web3.currentProvider);
} else {
// Handle the case where the user doesn't have Metamask installed
// Probably show them a message prompting them to install Metamask
}

// Now you can start your app & access web3 freely:
startApp()

})
</script>
</body>
</html>

调用 Payable 函数

attack, changeName, 以及 changeDna 的逻辑将非常雷同,所以本课将不会花时间在上面。

实际上,在调用这些函数的时候已经有了非常多的重复逻辑。所以最好是重构代码把相同的代码写成一个函数。(并对txStatus使用模板系统——我们已经看到用类似 Vue.js 类的框架是多么整洁)

我们来看看另外一种 Web3.js 中需要特殊对待的函数 — payable 函数。

升级!

回忆一下在 ZombieHelper 里面,我们添加了一个 payable 函数,用户可以用来升级:

1
2
3
4
function levelUp(uint _zombieId) external payable {
require(msg.value == levelUpFee);
zombies[_zombieId].level++;
}

和函数一起发送以太非常简单,只有一点需要注意: 我们需要指定发送多少 wei,而不是以太。

啥是 Wei?

一个 wei 是以太的最小单位 — 1 ether 等于 10^18 wei

太多0要数了,不过幸运的是 Web3.js 有一个转换工具来帮我们做这件事:

1
2
// 把 1 ETH 转换成 Wei
web3js.utils.toWei("1", "ether");

在我们的 DApp 里, 我们设置了 levelUpFee = 0.001 ether,所以调用 levelUp 方法的时候,我们可以让用户用以下的代码同时发送 0.001 以太:

1
2
cryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001","ether") })

实战演习

feedOnKitty 下面添加一个 levelUp 方法。代码和 feedOnKitty 将非常相似。不过:

  1. 函数将接收一个参数, zombieId
  2. 在发送事务之前,txStatus 的文本应该是 "正在升级您的僵尸..."
  3. 当它调用合约里的levelUp时,它应该发送"0.001" ETH,并用 toWei 转换,如同上面例子里那样。
  4. 成功之后应该显示 "不得了了!僵尸成功升级啦!"
  5. 我们 需要在调用 getZombiesByOwner 后重新绘制界面 — 因为在这里我们只是修改了僵尸的级别而已。

页面修改

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
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CryptoZombies front-end</title>
<script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
<script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
</head>
<body>
<div id="txStatus"></div>
<div id="zombies"></div>

<script>
var cryptoZombies;
var userAccount;

function startApp() {
var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

var accountInterval = setInterval(function() {
// Check if account has changed
if (web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
// Call a function to update the UI with the new account
getZombiesByOwner(userAccount)
.then(displayZombies);
}
}, 100);
}

function displayZombies(ids) {
$("#zombies").empty();
for (id of ids) {
// Look up zombie details from our contract. Returns a `zombie` object
getZombieDetails(id)
.then(function(zombie) {
// Using ES6's "template literals" to inject variables into the HTML.
// Append each one to our #zombies div
$("#zombies").append(`<div class="zombie">
<ul>
<li>Name: ${zombie.name}</li>
<li>DNA: ${zombie.dna}</li>
<li>Level: ${zombie.level}</li>
<li>Wins: ${zombie.winCount}</li>
<li>Losses: ${zombie.lossCount}</li>
<li>Ready Time: ${zombie.readyTime}</li>
</ul>
</div>`);
});
}
}

function createRandomZombie(name) {
// This is going to take a while, so update the UI to let the user know
// the transaction has been sent
$("#txStatus").text("Creating new zombie on the blockchain. This may take a while...");
// Send the tx to our contract:
return cryptoZombies.methods.createRandomZombie(name)
.send({ from: userAccount })
.on("receipt", function(receipt) {
$("#txStatus").text("Successfully created " + name + "!");
// Transaction was accepted into the blockchain, let's redraw the UI
getZombiesByOwner(userAccount).then(displayZombies);
})
.on("error", function(error) {
// Do something to alert the user their transaction has failed
$("#txStatus").text(error);
});
}

function feedOnKitty(zombieId, kittyId) {
$("#txStatus").text("Eating a kitty. This may take a while...");
return cryptoZombies.methods.feedOnKitty(zombieId, kittyId)
.send({ from: userAccount })
.on("receipt", function(receipt) {
$("#txStatus").text("Ate a kitty and spawned a new Zombie!");
getZombiesByOwner(userAccount).then(displayZombies);
})
.on("error", function(error) {
$("#txStatus").text(error);
});
}

// Start here
function levelUp(zombieId) {
$("#txStatus").text("正在升级您的僵尸...");
return cryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001","ether") })
.on("receipt", function(receipt) {
$("#txStatus").text("不得了了!僵尸成功升级啦!");
})
.on("error",function(error){
$("#txStatus").text(error);
});
}

function getZombieDetails(id) {
return cryptoZombies.methods.zombies(id).call()
}

function zombieToOwner(id) {
return cryptoZombies.methods.zombieToOwner(id).call()
}

function getZombiesByOwner(owner) {
return cryptoZombies.methods.getZombiesByOwner(owner).call()
}

window.addEventListener('load', function() {

// Checking if Web3 has been injected by the browser (Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// Use Mist/MetaMask's provider
web3js = new Web3(web3.currentProvider);
} else {
// Handle the case where the user doesn't have Metamask installed
// Probably show them a message prompting them to install Metamask
}

// Now you can start your app & access web3 freely:
startApp()

})
</script>
</body>
</html>

订阅事件

如你所见,通过 Web3.js 和合约交互非常简单直接——一旦你的环境建立起来, call 函数和 send 事务和普通的网络API并没有多少不同。

还有一点东西我们想要讲到——订阅合约事件

监听新僵尸事件

如果你还记得 zombiefactory.sol,每次新建一个僵尸后,我们会触发一个 NewZombie 事件:

1
event NewZombie(uint zombieId, string name, uint dna);

在 Web3.js里, 你可以 订阅 一个事件,这样你的 Web3 提供者可以在每次事件发生后触发你的一些代码逻辑:

1
2
3
4
5
cryptoZombies.events.NewZombie()
.on("data", function(event) {
let zombie = event.returnValues;
console.log("一个新僵尸诞生了!", zombie.zombieId, zombie.name, zombie.dna);
}).on('error', console.error);

注意这段代码将在 任何 僵尸生成的时候激发一个警告信息——而不仅仅是当前用用户的僵尸。如果我们只想对当前用户发出提醒呢?

使用 indexed

为了筛选仅和当前用户相关的事件,我们的 Solidity 合约将必须使用 indexed 关键字,就像我们在 ERC721 实现中的Transfer 事件中那样:

1
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);

在这种情况下, 因为_from_to 都是 indexed,这就意味着我们可以在前端事件监听中过滤事件

1
2
3
4
5
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
let data = event.returnValues;
// 当前用户更新了一个僵尸!更新界面来显示
}).on('error', console.error);

看到了吧, 使用 eventindexed 字段对于监听合约中的更改并将其反映到 DApp 的前端界面中是非常有用的做法。

查询过去的事件

我们甚至可以用 getPastEvents 查询过去的事件,并用过滤器 fromBlocktoBlock 给 Solidity 一个事件日志的时间范围(“block” 在这里代表以太坊区块编号):

1
2
3
4
5
cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: 'latest' })
.then(function(events) {
// events 是可以用来遍历的 `event` 对象
// 这段代码将返回给我们从开始以来创建的僵尸列表
});

因为你可以用这个方法来查询从最开始起的事件日志,这就有了一个非常有趣的用例: 用事件来作为一种更便宜的存储

若你还能记得,在区块链上保存数据是 Solidity 中最贵的操作之一。但是用事件就便宜太多太多了。

这里的短板是,事件不能从智能合约本身读取。但是,如果你有一些数据需要永久性地记录在区块链中以便可以在应用的前端中读取,这将是一个很好的用例。这些数据不会影响智能合约向前的状态。

举个栗子,我们可以用事件来作为僵尸战斗的历史纪录——我们可以在每次僵尸攻击别人以及有一方胜出的时候产生一个事件。智能合约不需要这些数据来计算任何接下来的事情,但是这对我们在前端向用户展示来说是非常有用的东西。

Web3.js 事件 和 MetaMask

上面的示例代码是针对 Web3.js 最新版1.0的,此版本使用了 WebSockets\ 来订阅事件。

但是,MetaMask 尚且不支持最新的事件 API (尽管如此,他们已经在实现这部分功能了, 点击这里 查看进度)

所以现在我们必须使用一个单独 Web3 提供者,它针对事件提供了WebSockets支持。 我们可以用 Infura 来像实例化第二份拷贝:

1
2
var web3Infura = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));
var czEvents = new web3Infura.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

然后我们将使用 czEvents.events.Transfer 来监听事件,而不再使用 cryptoZombies.events.Transfer。我们将继续在课程的其他部分使用 cryptoZombies.methods

将来,在 MetaMask 升级了 API 支持 Web3.js 后,我们就不用这么做了。但是现在我们还是要这么做,以使用 Web3.js 更好的最新语法来监听事件。

放在一起

来添加一些代码监听 Transfer 事件,并在当前用户获得一个新僵尸的时候为他更新界面。

我们将需要在 startApp 底部添加代码,以保证在添加事件监听器之前 cryptoZombies 已经初始化了。

  1. startApp()底部,为 cryptoZombies.events.Transfer 复制粘贴上面的2行事件监听代码块
  2. 复制监听 Transfer 事件的代码块,并用 _to: userAccount 过滤。要记得把 cryptoZombies 换成 czEvents 好在这 里使用 Infura 而不是 MetaMask 来作为提供者。
  3. getZombiesByOwner(userAccount).then(displayZombies); 来更新界面

页面修改

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
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CryptoZombies front-end</title>
<script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
<script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
</head>
<body>
<div id="txStatus"></div>
<div id="zombies"></div>

<script>
var cryptoZombies;
var userAccount;

function startApp() {
var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

var accountInterval = setInterval(function() {
// Check if account has changed
if (web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
// Call a function to update the UI with the new account
getZombiesByOwner(userAccount)
.then(displayZombies);
}
}, 100);

// Start here
var web3Infura = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));")
var czEvents = new web3Infura.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

czEvents.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
let data = event.returnValues;
getZombiesByOwner(userAccount).then(displayZombies);
}).on('error', console.error);
}

function displayZombies(ids) {
$("#zombies").empty();
for (id of ids) {
// Look up zombie details from our contract. Returns a `zombie` object
getZombieDetails(id)
.then(function(zombie) {
// Using ES6's "template literals" to inject variables into the HTML.
// Append each one to our #zombies div
$("#zombies").append(`<div class="zombie">
<ul>
<li>Name: ${zombie.name}</li>
<li>DNA: ${zombie.dna}</li>
<li>Level: ${zombie.level}</li>
<li>Wins: ${zombie.winCount}</li>
<li>Losses: ${zombie.lossCount}</li>
<li>Ready Time: ${zombie.readyTime}</li>
</ul>
</div>`);
});
}
}

function createRandomZombie(name) {
// This is going to take a while, so update the UI to let the user know
// the transaction has been sent
$("#txStatus").text("Creating new zombie on the blockchain. This may take a while...");
// Send the tx to our contract:
return cryptoZombies.methods.createRandomZombie(name)
.send({ from: userAccount })
.on("receipt", function(receipt) {
$("#txStatus").text("Successfully created " + name + "!");
// Transaction was accepted into the blockchain, let's redraw the UI
getZombiesByOwner(userAccount).then(displayZombies);
})
.on("error", function(error) {
// Do something to alert the user their transaction has failed
$("#txStatus").text(error);
});
}

function feedOnKitty(zombieId, kittyId) {
$("#txStatus").text("Eating a kitty. This may take a while...");
return cryptoZombies.methods.feedOnKitty(zombieId, kittyId)
.send({ from: userAccount })
.on("receipt", function(receipt) {
$("#txStatus").text("Ate a kitty and spawned a new Zombie!");
getZombiesByOwner(userAccount).then(displayZombies);
})
.on("error", function(error) {
$("#txStatus").text(error);
});
}

function levelUp(zombieId) {
$("#txStatus").text("Leveling up your zombie...");
return cryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3.utils.toWei("0.001", "ether") })
.on("receipt", function(receipt) {
$("#txStatus").text("Power overwhelming! Zombie successfully leveled up");
})
.on("error", function(error) {
$("#txStatus").text(error);
});
}

function getZombieDetails(id) {
return cryptoZombies.methods.zombies(id).call()
}

function zombieToOwner(id) {
return cryptoZombies.methods.zombieToOwner(id).call()
}

function getZombiesByOwner(owner) {
return cryptoZombies.methods.getZombiesByOwner(owner).call()
}

window.addEventListener('load', function() {

// Checking if Web3 has been injected by the browser (Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// Use Mist/MetaMask's provider
web3js = new Web3(web3.currentProvider);
} else {
// Handle the case where the user doesn't have Metamask installed
// Probably show them a message prompting them to install Metamask
}

// Now you can start your app & access web3 freely:
startApp()

})
</script>
</body>
</html>

放在一起

恭喜啊少年,你已经成功编写了一个 Web3.js 前端界面来和你的智能合约交互

接下来的步骤

这节课的内容非常基础。我们想要给你展示和智能合约交互的核心内容,而并不想用太多的时间来教你完整实现。我们也不想花太多时间在HTML/CSS上,因为大部分人都已经知道了。

所以我们把一些实现略去了。这里是你要完整实现所需要完成的基本事项列表:

  1. attack, changeName, changeDna 以及 ERC721 函数 transfer, ownerOf, balanceOf 等实现前端函数。这些函数的实现将和我们讲过的 send事务的函数非常相似。

  2. 实现一个“管理界面”,在那里你可以调用 setKittyContractAddress, setLevelUpFee, 以及 withdraw。再次,在前端这块没有什么特别的代码——这些实现之间将非常相似。你应该保证从部署合同时候相同的以太坊地址调用这些函数,因为他们都有onlyOwner 修饰符。

  3. 在应用里我们还应该实现一些其他的界面:

    a. 一个僵尸页面,在那里你可以查看一个特定僵尸的信息并可以分享它的链接。这个页面应该渲染僵尸的外形,展示它的名字,它的所有者(以及用户主页的链接),它的输赢次数,它的战斗记录等等。

    b. 一个用户界面,在那里你可以查看用户的僵尸大军并分享它的链接。

    c. 一个主页,就是用户页面的变体,可以展示当前用户的僵尸大军(正如我们在index.html)里面实现的那样。

  4. 界面中的一些方法允许用户用 CryptoKitties 喂食僵尸。我们可以给每一个僵尸添加一个按钮,叫做“给我投食”,再给一个输入框让用户输入一个猫咪的ID(或者一个猫咪的网址,比如https://www.cryptokitties.co/kitty/578397),它将触发我们的 feedOnKitty 函数。

  5. 界面中的一些方法将让用户用来攻击其他用户的僵尸

    实现这点的一个方法是,当用户浏览其他用户的页面的时候,可以在对方僵尸旁边显示一个按钮,叫做“攻击这头僵尸”。当用户点击的时候,可以弹出一个模块,展示当前用户的僵尸大军并询问用户“你想用哪头僵尸出战?”

    在用户的主页,也可以在每个僵尸旁边显示一个按钮,叫做“攻击一个僵尸”。当用户点击的时候,可以弹出一个模块,展示一个搜索框,可以让用户输入僵尸ID或者网址来搜索,或者也可以有一个按钮叫做“随机攻击一头僵尸”,将随机搜索一头僵尸来。

    我们也建议你将在冷却期的僵尸用特殊的颜色显示,比如使其变成灰色。这样界面就能告诉用户不能用冷却期的僵尸来进攻。

  6. 在用户的主页,每一个僵尸也应该有选项可以更改名字、DNA、以及升级(通过付费)。若用户等级不到,无法使用的选项应该标灰。

  7. 对于新用户,我们应该显示一个欢迎信息,并让其确认用 createRandomZombie()创建一个新僵尸。

  8. 也可以为我们的智能合约添加一个包含indexed 的用户地址属性的 Attack 事件。这样就可以创建实时通知了——我们可以在用户的僵尸遭受攻击的时候弹出一条通知,这样他们可以看到谁在用什么僵尸攻击他们并做出报复。

  9. 我们也许还想实现一些前端缓存层,这样就不用总是为了相同的数据去访问Infura。(在我们当前实现中,displayZombies 将在每次页面刷新的时候为每一个僵尸调用 getZombieDetails,但是实际中我们将只需要为新加入的僵尸调用这个函数)

  10. 一个实时聊天室,这样你就可以在你击溃别人的僵尸大军的同时嘲讽他们?

因为这将需要大量的前端代码来实现全部的界面(HTML, CSS, JavaScript 以及诸如 React 和 Vue.js 这样的框架)。光实现一个这样的前端界面也许会花费多达10节课,所以我们将这个光荣的任务交给你自己去完成。

注意:尽管智能合约是去中心化的。这个用来和DApp交互的前端界面依然需要放在我们中心化的网络服务器上。不过,有了我们正在内测的Loom Network SDK,很快你就可以在应用自己的DApp链上运行前端界面而不是中心化的网络服务器。这样在以太坊和 Loom DApp 链上,你的整个应用都100%运行在区块链上了。

总结

学完第六课。你现在有了编写智能合约和前端界面来让用户交互的所需技能了。

在下一课。 我们将涉及这个历程中最后缺失的一部分——将你的智能合约部署到以太坊。

知道你已经急不可耐了,点击“下一章”领取你的奖励吧。

image-20200630142837304

第六课打卡

image-20200630143006394

我的僵尸大军6

您的支持是我继续创作的动力