应用程序前端和Web3.js
cryptozombies - Lesson 6原文地址
你已经走到这一步了?
你不是普通的CryptoZombie……
通过完成第5课,你已经证明了你对Solidity有一个相当牢固的掌握。
但是,如果没有用户与之交互的方式,DApp是不完整的……
在本课中,我们将学习如何与智能合约交互,并使用Web3.js库为DApp构建基本前端。
请注意,应用程序的前端是用JavaScript编写的,而不是Solidity。但由于本课程的重点是以太坊/Solidity,我们假设你已经习惯用HTML, javascript构建网站
一、Web3.js简介
完成第5课后,我们的僵尸DApp就完成了。现在我们要创建一个基本的网页,你的用户可以与之交互。
为此,我们将使用以太坊基金会的一个名为Web3.js的JavaScript库。
Web3.js是什么?
请记住,以太坊网络由节点组成,每个节点都包含区块链的副本。当你想在智能合约上调用一个函数时,你需要查询其中一个节点并告诉它:
- 智能合约的地址
- 要调用的函数,和
- 你想传递给这个函数的变量
以太坊节点只使用一种称为JSON-RPC的语言,这种语言不太适合人类阅读。告诉节点你想在合约上调用一个查询函数:
// Yeah... Good luck writing all your function calls this way!
// Scroll right ==>
{"jsonrpc":"2.0","method":"eth_sendTransaction","params":[{"from":"0xb60e8dd61c5d32be8058bb8eb970870f07233155","to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","gas":"0x76c0","gasPrice":"0x9184e72a000","value":"0x9184e72a","data":"0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"}],"id":1}
幸运的是,Web3.js隐藏了这些令人讨厌的查询,因此您只需要与方便且易于阅读的JavaScript接口进行交互。
不需要构造上面的查询,在代码中调用函数将看起来像这样:
CryptoZombies.methods.createRandomZombie("Vitalik Nakamoto 🤔")
.send({ from: "0xb60e8dd61c5d32be8058bb8eb970870f07233155", gas: "3000000" })
我们将在接下来的几章中详细解释语法,但首先让我们用Web3.js设置你的项目。
开始
据项目的工作流程,你可以使用大多数包工具将Web3.js添加到项目中:
// Using NPM
npm install web3
// Using Yarn
yarn add web3
// Using Bower
bower install web3
// ...etc.
或者你可以简单地从github下载压缩后的.js文件,并将其包含在你的项目中:
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
由于我们不想对您的开发环境和您使用的包管理器做太多的假设,因此在本教程中,我们将像上面那样使用脚本标记在项目中包含web3
实战演习
我们已经为您创建了一个HTML项目文件的index.html脚本。假设在index.html所在的文件夹中有一个web3.min.js的副本。
将上面的脚本标签复制/粘贴到我们的项目中,这样我们就可以使用web3.js了
合约修改
index.html
<!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>
</body>
</html>
三、 Web3 Providers
太棒了!现在我们的项目中有了Web3.js,让我们初始化它并与区块链对话。
我们需要的第一件事是Web3提供商
请记住,以太坊是由所有共享相同数据副本的node
组成的。在Web3.js中设置Web3 Provider告诉我们的代码应该与哪个节点对话来处理我们的读写操作。这有点像在传统web应用中为API调用设置远程web服务器的URL。
您可以作为提供商托管自己的以太坊节点。然而,有一个第三方服务可以让你的生活更轻松,所以你不需要维护自己的以太坊节点,就可以为你的用户提供一个DApp——Infura
。
Infura
Infura
是一项服务,它维护了一组以太坊节点,其中包含用于快速读取的缓存层,您可以通过其API免费访问。使用Infura作为提供者,您可以可靠地向以太坊区块链发送和接收消息,而无需设置和维护自己的节点。
您可以将Web3设置为使用Infura
作为您的Web3提供商,如下所示
var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));
然而,由于我们的DApp将被许多用户使用——这些用户将写入区块链,而不仅仅是从区块链中读取——我们需要一种方法让这些用户用他们的私钥签署交易。
注意:以太坊(以及一般的区块链)使用公钥/私钥对对交易进行数字签名。你可以把它想象成一个极其安全的数字签名密码。这样,如果我改变了区块链上的一些数据,我可以通过我的公钥证明我是签署它的人——但是由于没有人知道我的私钥,没有人可以为我伪造交易。
密码学是复杂的,所以除非你是一个安全专家,你真的知道你在做什么,否则在我们的应用程序前端自己管理用户的私钥可能不是一个好主意。
但幸运的是,你不需要这样做——已经有服务可以为你处理这个问题。其中最受欢迎的是Metamask
。
Metamask
Metamask是Chrome和Firefox的浏览器扩展,允许用户安全地管理他们的以太坊帐户和私钥,并使用这些帐户与使用Web3.js的网站进行交互。(如果你以前没有使用过它,你肯定想去安装它——然后你的浏览器是Web3启用的,你现在可以与任何与以太坊区块链通信的网站进行交互!)
作为开发者,如果你想让用户通过网页浏览器中的网站与你的DApp进行互动(就像我们在我们的CryptoZombies游戏中所做的那样),你肯定想让它与metamask兼容。
注意:Metamask在底层使用Infura的服务器作为web3提供商,就像我们上面所做的那样——但它也给了用户选择自己的web3提供商的选项。通过使用Metamask的web3提供商,你给了用户一个选择,你在应用中少了一件需要担心的事情。
Using Metamask’s web3 provider
Metamask将web3提供程序注入到浏览器的全局JavaScript对象web3
中。所以你的应用可以检查web3
是否存在,以及是否使用web3.currentProvider
作为它的提供者。
下面是Metamask提供的一些模板代码,用来检测用户是否安装了Metamask,如果没有,告诉他们需要安装Metamask才能使用我们的应用:
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 web3. Probably
// show them a message telling them to install Metamask in
// order to use our app.
}
// Now you can start your app & access web3js freely:
startApp()
})
你可以在你创建的所有应用中使用这个样板代码,以要求用户拥有Metamask才能使用你的DApp。
注意:除了MetaMask之外,您的用户可能还会使用其他私钥管理程序,例如web浏览器Mist。然而,它们都实现了注入变量web3的共同模式,因此我们在这里描述的用于检测用户的web3提供商的方法也适用于它们。
实战演习
我们在HTML文件的结束</body>
标记之前创建了一些空脚本标记。我们可以在这里编写本课的JavaScript代码。
- 继续并复制/粘贴上面用于检测Metamask的模板代码。它是以
window.addeventlistener
开始的block。
合约修改
index.html
<!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>
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>
三、Talking to Contracts
现在我们已经用MetaMask的Web3提供程序初始化了Web3.js,让我们设置它来与我们的智能合约对话。
Web3.js需要两件事来与你的合约对话:它的address
和它的ABI
。
Contract Address
在您完成编写智能合约后,您将编译它并将其部署到以太坊。我们将在下一课中介绍部署,但由于这与编写代码是完全不同的过程,我们决定不按顺序先介绍Web3.js。
在你部署你的合约后,它会在以太坊上获得一个固定的地址,它将永远存在。如果您回忆一下第2课,以太坊主网上的CryptoKitties合约的地址是0x06012c8cf97BEaD5deAe237070F9587f8E7A266d
。
Contract ABI
Web3.js需要与你的合同对话的另一件事是它的ABI。
ABI代表应用程序二进制接口。基本上,它是以JSON格式表示合约的方法,告诉Web3.js如何以合约能够理解的方式格式化函数调用。
当您编译合约以部署到以太坊(我们将在第7课中介绍)时,Solidity编译器将为您提供ABI,因此除了合约地址外,您还需要复制并保存此内容。
由于我们还没有讨论部署,因此在本课中,我们已经为您编译了ABI,并将其放在名为cryptozombies_abi.js
的文件中,存储在名为cryptoZombiesABI
的变量中。
如果我们在项目中包含cryptozombies_abi.js
,我们将能够使用该变量访问CryptoZombies ABI。
实例化一个Web3.js合约
一旦有了合约的地址和ABI,就可以在Web3中实例化它,如下所示
// Instantiate myContract
var myContract = new web3js.eth.Contract(myABI, myContractAddress);
实战演习
-
在文档的
<head>
中,为cryptozombies_abi.js
包含另一个脚本标记,这样我们就可以将ABI定义导入到项目中 -
在
<body>
中的<script>
标记的开头,声明一个名为cryptoZombies
的变量,但不要将其设置为任何值。稍后,我们将使用该变量来存储实例化的合约约。 -
接下来,创建一个名为
startApp()
的函数。我们将在接下来的两步中填充主体。 -
startApp()
应该做的第一件事是声明一个名为cryptoZombiesAddress
的var
,并将其设置为字符串YOUR_CONTRACT_ADDRESS
(这是主网上CryptoZombies合约的地址)。 -
最后,让我们实例化我们的合约。将cryptoZombies设置为新的
web3js.eth.Contract
。就像我们在上面的示例代码中所做的那样。(使用cryptoZombiesABI
,它与我们的脚本标签一起导入,以及上面的cryptoZombiesAddress
)。
合约修改
<!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();
}
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
和send
。
Call
call
用于view
和pure
函数。它只在本地节点上运行,不会在区块链上创建交易。
回顾:
view
和pure
函数是只读的,不会改变区块链上的状态。它们也不消耗任何天然气,用户也不会被提示与MetaMask签署交易。
使用Web3.js,你可以用参数123
调用一个名为myMethod
的函数,如下所示:
myContract.methods.myMethod(123).call()
Send
Send
将创建一个交易并更改区块链上的数据。对于任何不是view
或pure
的函数,都需要使用send
。
注意:发送交易将需要用户支付gas,并且会弹出他们的Metamask来提示他们签署交易。当我们使用Metamask作为web3提供者时,这一切都会在我们调用
send()
时自动发生,我们不需要在代码中做任何特别的事情。很酷!
使用Web3.js,你·调用一个名为myMethod
的函数发送交易,参数为123
,如下所示:
myContract.methods.myMethod(123).send()
语法几乎与call()
相同。
获取僵尸数据
现在让我们看一个使用call
访问合约数据的真实示例。
回想一下,我们将僵尸数组设置为public
:
Zombie[] public zombies;
在Solidity中,当你声明一个变量public
时,它会自动创建一个同名的public getter函数。因此,如果您想查找id为15
的僵尸,您可以像调用函数一样调用它:zombies(15)
。
下面是我们如何在前端编写一个JavaScript函数,它将获取一个僵尸id,查询该僵尸的合约,并返回结果:
注意:我们在本课中使用的所有代码示例都使用Web3.js的1.0版本,它使用承诺而不是回调。你在网上看到的许多其他教程都是使用旧版本的Web3.js。1.0版本的语法改变了很多,所以如果你从其他教程中复制代码,确保他们使用与你相同的版本!
function getZombieDetails(id) {
return cryptoZombies.methods.zombies(id).call()
}
// Call the function and do something with the result:
getZombieDetails(15)
.then(function(result) {
console.log("Zombie 15: " + JSON.stringify(result));
});
让我们来看看这里发生了什么。
cryptoZombies.methods.zombies(id).call()
将与Web3提供商节点通信,并告诉它从我们的合约上的zombie [] public zombies
中返回索引id
的僵尸。
注意,这是异步的,就像对外部服务器的API调用一样。所以Web3在这里返回一个promise。(如果你不熟悉JavaScript promise…在继续之前,是时候做一些额外的功课了!)
一旦promise被解析(这意味着我们从web3提供者那里得到了一个答案),我们的示例代码继续使用then
语句,它将结果记录到控制台。
result
将是一个JavaScript对象,看起来像这样:
{
"name": "H4XF13LD MORRIS'S COOLER OLDER BROTHER",
"dna": "1337133713371337",
"level": "9999",
"readyTime": "1522498671",
"winCount": "999999999",
"lossCount": "0" // Obviously.
}
然后,我们可以使用一些前端逻辑来解析该对象,并以有意义的方式在前端显示它。
实战演习
我们已经为您将getZombieDetails
复制到代码中。
- 我们为
zombieToOwner
创建一个类似的函数。如果你还记得ZombieFactory.sol
的话。我们有一个像这样的映射:
mapping (uint => address) public zombieToOwner;
定义一个名为zombieToOwner
的JavaScript函数。与上面的getZombieDetails
类似,它将以id
作为参数,并将在我们的合约上对zombietooowner的调用 返回一个Web3.js call
。
- 在此下面,为
getZombiesByOwner
创建第三个函数。如果你还记得ZombieHelper.sol
的话。函数定义是这样的:
function getZombiesByOwner(address _owner)
我们的函数getZombiesByOwner
将把owner
作为参数,并返回一个Web3.js对getZombiesByOwner
的call
调用。
合约修改
<!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()
}
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 & Accounts
太棒了!您已经成功编写了与第一个智能合约交互的前端代码。
现在让我们把一些片段放在一起——假设我们希望应用程序的主页显示用户的整个僵尸大军。
显然,我们首先需要使用函数getZombiesByOwner(owner)
来查找当前用户拥有的所有僵尸id。
但是我们的Solidity合约要求owner
是一个Solidity address
。我们如何知道使用我们的应用程序的用户的地址?
在MetaMask中获取用户的帐户
MetaMask允许用户在其扩展中管理多个帐户。
我们可以通过以下方式查看当前在注入的web3变量上哪个帐户是活跃的:
var userAccount = web3.eth.accounts[0]
因为用户可以在MetaMask中随时切换活动帐户,我们的应用程序需要监控这个变量,看看它是否已经更改并相应地更新UI。例如,如果用户的主页显示他们的僵尸军队,当他们在MetaMask中更改帐户时,我们将希望更新页面以显示他们选择的新帐户的僵尸军队。
我们可以用setInterval
循环这样做:
var accountInterval = setInterval(function() {
// Check if account has changed
if (web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
// Call some function to update the UI with the new account
updateInterface();
}
}, 100);
它所做的是每100毫秒检查一次userAccount
是否仍然等于web3.eth.ccounts[0]
(即用户是否仍然拥有该帐户活动)。如果不是,它将userAccount
重新分配给当前活动的帐户,并调用一个函数来更新显示。
实战演习
让我们这样做,当页面第一次加载时,我们的应用程序将显示用户的僵尸军队,并监控MetaMask中的活动帐户,以便在页面发生变化时刷新显示。
- 声明一个名为
userAccount
的变量,但不将其赋值给任何东西。 - 在
startApp()
的末尾,复制/粘贴上面的样板文件accountInterval
代码 - 用
getZombiesByOwner
替换行updateInterface();
,并传递给userAccount
- 在
getZombiesByOwner
之后链接一个then
语句,并将结果传递给一个名为displayZombies
的函数。(语法是:.then(displayZombies)😉。 - 我们还没有一个叫做
displayZombies
的函数,但是我们将在下一章实现它。
合约修改
<!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
,并调用getZombiesByOwner
的结果。它将被传递一个僵尸id数组,看起来像这样:
[0, 13, 47]
因此我们希望displayZombies
函数:
- 首先清除
#zombies
div的内容,如果其中已经有任何内容的话。(这样,如果用户改变他们的活跃MetaMask帐户,它将清除他们的旧僵尸军队之前加载新的)。 - 循环遍历每个
id
,然后调用getZombieDetails(id)
从我们的智能合约中查找该僵尸的所有信息 - 将关于僵尸的信息放入HTML模板中以格式化显示,并将该模板附加到
#zombies
这里我们只使用JQuery,默认情况下它没有模板引擎,所以会很难看。但这里有一个简单的例子,说明我们如何为每个僵尸输出这些数据:
// 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>`);
});
如何展示僵尸精灵?
在上面的示例中,我们只是将DNA显示为字符串。但在DApp中,您可能希望将其转换为图像以显示僵尸。
我们把DNA串分成子串,每两个数字对应一个图像。喜欢的东西:
// Get an integer 1-7 that represents our zombie head:
var head = parseInt(zombie.dna.substring(0, 2)) % 7 + 1
// We have 7 head images with sequential filenames:
var headSrc = "../assets/zombieparts/head-" + head + ".png"
每个组件都使用CSS进行绝对定位,以便将其覆盖在其他图像上。
如果你想看到我们的确切实现,我们已经开源了我们用于僵尸外观的Vue.js组件,你可以在这里查看。
但是,由于该文件中有大量代码,因此超出了本教程的范围。对于本课,我们将坚持使用上面非常简单的JQuery实现,并将其留给您作为作业😉深入研究更漂亮的实现
实战演习
我们为您创建了一个空的 displayZombies
函数。我们把它填上。
- 我们要做的第一件事是清空
#zombies
div。在JQuery中,您可以使用$("#zombies").empty();
- 接下来,我们要循环遍历所有id,使用for循环:
for (const id of ids) {}
- 在for循环中,复制/粘贴上面的代码块,为每个id调用
getZombieDetails(id)
,然后使用$("#zombies").append(…)
将其添加到HTML中。
合约修改
<!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 (const 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>
七、发送交易
太棒了!现在我们的UI将检测用户的metamask帐户,并自动在主页上显示他们的僵尸军队。
现在让我们看看如何使用send
函数来更改智能合约中的数据。
与call
函数有几个主要区别:
send
发送交易需要调用函数的人的from address
(solidity代码里的msg.sender)。我们希望其作为我们的DApp的用户,所以MetaMask将弹出提示他们签署交易。send
发送交易需要gas- 从用户
send
发送交易到该交易在区块链上实际生效会有很大的延迟。这是因为我们必须等待交易被打包在区块中,而以太坊的区块时间平均为15秒。如果以太坊上有很多待处理的交易,或者如果用户发送的gas价格过低,我们的交易可能需要等待几个区块才能被包含在内,这可能需要几分钟。
因此,我们需要应用程序中的逻辑来处理这段代码的异步特性。
Creating zombies
让我们看一个例子,其中有一个新用户将调用的合约中的第一个函数:createRandomZombie
。
回顾一下,这是我们合约的solidity代码:
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
randDna = randDna - randDna % 100;
_createZombie(_name, randDna);
}
下面是我们如何在Web3.js中使用MetaMask调用这个函数的例子:
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);
});
}
我们的函数向Web3提供程序发送一个transaction,并链接一些事件监听器:
- 当交易被包含到以太坊的一个区块中时,
receipt
将被触发,这意味着我们的僵尸已经被创建并保存在我们的合约中 - 如果存在阻止交易被包含在块中的问题,例如用户没有发送足够的gas,则会触发
error
。我们希望在UI中通知用户transaction没有通过,以便他们可以再次尝试。
注意:当你调用
send
时,你可以选择指定gas
和gasPrice
,例如。send({from: userAccount, gas: 3000000})
。如果你不指定这个,MetaMask会让用户选择这些值。
实战演习
我们添加了一个ID为txStatus
的div
——这样我们就可以使用这个div向用户更新带有事务状态的消息。
- 在
displayZombies
下面,复制/粘贴上面的createRandomZombie
代码。 - 让我们实现另一个函数:
feedOnKitty
。
调用feedOnKitty
的逻辑几乎是相同的——我们将发送一个调用该函数的事务,一个成功的事务将为我们创建一个新的僵尸,因此我们将在它成功后重新绘制UI。
在它下面复制一个createRandomZombie
,但做以下修改:
a)调用第二个函数feedOnKitty
,它有两个参数:zombiid
和kittyId
b)#txStatus
文本应该更新为:“吃小猫。这可能需要一段时间……”
c)在合约中调用feedOnKitty
,并传递两个参数
d)#txStatus
上的成功消息应该是"Ate a kitty and spawned a new Zombie!"
合约修改
index.html
<!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 (const 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 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, and changeDna
的逻辑将非常相似,这一课我们不会花时间在编码实现它们上。
事实上,在每个函数调用中已经有很多重复的逻辑,因此重构并将公共代码放入自己的函数中可能是有意义的。(并且为txStatus
消息使用一个模板系统——我们已经看到,使用Vue.js这样的框架,事情会变得多么简洁!)
让我们来看看Web3.js中另一种需要特殊处理的函数——payable
functions。
Level Up!
回想一下在ZombieHelper
中,我们添加了一个支付功能,用户可以在其中升级:
function levelUp(uint _zombieId) external payable {
require(msg.value == levelUpFee);
zombies[_zombieId].level++;
}
将Ether用函数发送的方法很简单,但有一点需要注意:我们需要指定以 wei
发送的数量,而不是Ether
。
What’s a Wei?
Wei
是 以太 最小的单位——一个以太有10^18个Wei
。
这有很多零要数——但幸运的是,Web3.js有一个转换实用程序可以为我们做这件事。
// This will convert 1 ETH to Wei
web3js.utils.toWei("1");
在我们的DApp中,我们设置了levelUpFee = 0.001 ether
,所以当我们调用levelUp
函数时,我们可以使用以下代码让用户发送0.001 ether
:
cryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001", "ether") })
实战演习
让我们在feedOnKitty
下面添加一个levelUp
函数。代码将与feedOnKitty
非常相似,但是:
- 该函数将接受一个参数
zombieId
- 在交易前,它应该显示
txStatus
文本"Leveling up your zombie..."
- 当它在合约上调用
levelUp
时,它应该发送“0.001”ETH
转换为wei
,如上面的示例所示 - 一旦成功,它应该显示
"Power overwhelming! Zombie successfully leveled up"
- 我们不需要通过查询
getZombiesByOwner
的智能合约来重新绘制UI——因为在这种情况下,我们知道唯一改变的是一个僵尸的level
合约修改
index.html
<!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 (const 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("Leveling up your zombie...");
return cryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.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与你的合约交互是非常简单的——一旦你设置好了你的环境,调用函数和发送交易与普通的web API并没有什么不同。
我们还想介绍另一个方面——从你的合同中订阅事件。
监听新僵尸
如果你还记得zombiefactory.sol
, 我们有一个叫做 NewZombie
的事件,每当一个新的僵尸被创造出来时,我们就会触发这个事件:
event NewZombie(uint zombieId, string name, uint dna);
在web3 .js中,你可以订阅一个事件,这样你的web3提供商每次触发时都会在你的代码中触发一些逻辑:
cryptoZombies.events.NewZombie()
.on("data", function(event) {
let zombie = event.returnValues;
// We can access this event's 3 return values on the `event.returnValues` object:
console.log("A new zombie was born!", zombie.zombieId, zombie.name, zombie.dna);
}).on("error", console.error);
请注意,每次在我们的DApp中创建任何僵尸时都会触发警报-不仅仅是针对当前用户。如果我们只想要当前用户的警报呢?
Using indexed
为了过滤事件,只监听与当前用户相关的变化,我们的Solidity合约必须使用indexed
关键字,就像我们在ERC721实现的Transfer
事件中所做的那样:
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
在这种情况下,因为_from
和_to
是indexed
的,这意味着我们可以在前端的事件监听器中过滤它们:
// Use `filter` to only fire this code when `_to` equals `userAccount`
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
let data = event.returnValues;
// The current user just received a zombie!
// Do something here to update the UI to show it
}).on("error", console.error);
正如你所看到的,使用event
和indexed
字段是一个非常有用的实践,它可以监听合约的变化,并将它们反映在应用程序的前端。
查询过去事件
我们甚至可以使用 getPastEvents
查询过去的事件,并使用fromBlock
和toBlock
过滤器为Solidity提供事件日志的时间范围(在这种情况下,“block”指的是以太坊区块号):
cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: "latest" })
.then(function(events) {
// `events` is an array of `event` objects that we can iterate, like we did above
// This code will get us a list of every zombie that was ever created
});
因为您可以使用此方法从一开始就查询事件日志,这就提出了一个有趣的用例:使用事件作为一种更便宜的存储形式。
如果你还记得的话,将数据保存到区块链是Solidity中最昂贵的操作之一。但是从gas的角度来看,使用events要便宜得多。
这里的权衡是,events无法从智能合约本身内部读取。但这是一个重要的用例,如果你有一些数据想要记录在区块链上,这样你就可以从应用程序的前端读取它。
例如,我们可以用它作为僵尸战斗的历史记录——我们可以为每次僵尸攻击另一个僵尸和谁赢了创造一个事件。智能合约不需要这些数据来计算任何未来的结果,但它是用户能够从应用程序前端浏览的有用数据。
实战演习
让我们添加一些代码来监听Transfer
事件,并在当前用户接收到新僵尸时更新应用程序的UI。
我们需要在startApp
函数的末尾添加这段代码,以确保在添加事件侦听器之前已经初始化了cryptoZombies
合约。
- 在
startApp()
的末尾, 复制/粘贴上面监听的代码块cryptoZombies.events.Transfer
- 对于更新UI的行,使用
getZombiesByOwner(userAccount).then(displayZombies);
合约修改
<!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);
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
let data = event.returnValues;
getZombiesByOwner(userAccount).then(displayZombies);
// The current user just received a zombie!
// Do something here to update the UI to show it
}).on("error", console.error);
}
function displayZombies(ids) {
$("#zombies").empty();
for (const 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前端。
作为奖励,你会得到你自己的Web3僵尸幽灵!3.0级(Web 3.0😉),完成狐狸面具
Next Steps
这堂课故意讲得很基础。我们想向您展示与智能合约交互所需的核心逻辑,但不想花费太多时间来完成完整的实现,因为Web3.js部分的代码非常重复,并且我们不会通过使本课程更长时间来介绍任何新概念。
所以我们留下了这个实现的骨架。为了让我们的前端成为僵尸游戏的完整执行,这里有一个我们想要执行的想法清单,如果你想要运行并自己构建它
- 实现
attack, changeName, changeDna
,以及ERC721函数transfer, ownerOf, balanceOf
等。这些函数的实现将与我们讨论的所有其他send
事务相同。 - 实现一个“管理页面”,你可以执行
setKittyContractAddress, setLevelUpFee
和withdraw
。同样,这里的前端没有特殊的逻辑——这些实现将与我们已经介绍过的功能相同。你只需要确保你从部署合约的同一个以太坊地址调用它们,因为它们有onlyOwner修饰符 - 在应用程序中,我们想要实现:
a. 一个单独的僵尸页面,您可以在其中查看有关特定僵尸的信息,并提供永久链接。这个页面将呈现僵尸的外观,显示它的名字,它的主人(带有用户个人资料页面的链接),它的胜败计数,它的战斗历史等等。
b.一个用户页面,在那里你可以看到一个用户的僵尸军队与永久链接。你可以点击一个僵尸来查看它的页面,如果你登录了MetaMask并且拥有一支军队,你也可以点击一个僵尸来攻击它。
c. 主页,这是用户页面的变体,显示当前用户的僵尸大军。(这是我们在index.html中开始实现的页面)。 - UI中的一些方法允许用户以CryptoKitties为饲料。我们可以在主页上的每个僵尸旁边放一个按钮,上面写着“喂我”,然后用一个文本框提示用户输入小猫的ID(或者小猫的URL,例如https://www.cryptokitties.co/kitty/578397)。这将触发feedOnKitty函数。
- UI中的一些方法可以让用户攻击另一个用户的僵尸。
实现这一点的一种方法是,当用户浏览另一个用户的页面时,可以有一个按钮,上面写着“攻击这个僵尸”。当用户点击它时,它会弹出一个包含当前用户僵尸军队的模式,并提示他们“你想用哪个僵尸攻击?”
用户的主页也可以在每个僵尸旁边放一个按钮,上面写着“攻击僵尸”。当他们点击它时,它会弹出一个带有搜索字段的模式,他们可以在其中输入僵尸的ID进行搜索。或者一个选项说“攻击随机僵尸”,这将为他们搜索一个随机数。
我们还想让冷却期尚未结束的用户僵尸变灰,这样UI就可以告诉用户他们还不能用僵尸攻击,以及他们需要等待多长时间。 - 用户的主页上还可以为每个僵尸提供更改名称、更改DNA和升级的选项(需要付费)。如果用户级别不够高,选项将显示为灰色。
- 对于新用户,我们应该显示一条欢迎消息,并提示在他们的军队中创建第一个僵尸,该消息调用createRandomZombie()。
- 我们可能希望将攻击事件添加到我们的智能合约中,并将用户的地址作为索引属性,如上一章所述。这将允许我们构建实时通知——当他们的僵尸之一受到攻击时,我们可以向用户显示一个弹出式警报,这样他们就可以查看攻击他们的用户/僵尸并进行报复。
- 我们可能还想实现某种前端缓存层,这样我们就不会总是因为对相同数据的请求而对fura造成冲击。(我们当前的displayZombies实现在每次刷新界面时都会为每一个僵尸调用getZombieDetails——但实际上我们只需要为添加到我们军队中的新僵尸调用这个)。
- 一个实时聊天室,这样你就可以在粉碎其他玩家的僵尸军队时诋毁他们?
这只是一个开始——我相信我们还能想出更多的功能——而且已经是一个庞大的列表了。
因为有很多前端代码可以用来创建这样一个完整的界面(HTML, CSS, JavaScript和像React或Vue.js这样的框架),构建整个前端可能需要整整10节课的课程。因此,我们将把出色的实现留给您。
注意:尽管我们的智能合约是去中心化的,但与我们的DApp交互的前端将完全集中在我们的web服务器上。
然而,随着我们在Loom Network构建的SDK,很快你就可以从自己的DAppChain而不是集中式web服务器上提供这样的前端服务。这样在以太坊和织机DAppChain之间,你的整个应用程序将100%在区块链上运行。
Conclusion
第六课结束了。现在,您已经具备了编写智能合约和允许用户与之交互的前端所需的所有技能!
在下一课中,我们将介绍这个谜题中最后一个缺失的部分——将您的智能合约部署到以太坊。
继续点击“下一章”领取奖励!