通过前边的学习,DApp 的 Solidity 合约部分就完成了。现在我们来做一个基本的网页好让你的用户能玩它。 要做到这一点,我们将使用以太坊基金发布的 JavaScript 库 —— Web3.js.
一、Web3.js简介
什么是 Web3.js?
还记得么?以太坊网络是由节点组成的,每一个节点都包含了区块链的一份拷贝
。当你想要调用一份智能合约的一个方法,你需要从其中一个节点中查找并告诉它:
- 1、智能合约的地址
- 2、你想调用的方法,以及
- 3、你想传入那个方法的参数
以太坊节点只能识别一种叫做 JSON-RPC
的语言。这种语言直接读起来并不好懂。当你你想调用一个合约的方法的时候,需要发送的查询语句将会是这样的:
// 哈……祝你写所有这样的函数调用的时候都一次通过
// 往右边拉…… ==>
{"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 添加进来:
// 用 NPM
npm install web3
// 用 Yarn
yarn add web3
// 用 Bower
bower install web3
// ...或者其他。
甚至,你可以从 github直接下载压缩后的 .js
文件 然后包含到你的项目文件中:
<script language="javascript" type="text/javascript" src="web3.min.js">
因为我们不想让你花太多在项目环境搭建上,在本教程中我们将使用上面的 script 标签来将 Web3.js 引入。
实战演练
新建一个HTML 项目空壳 —— index.html
。假设在和 index.html
同个文件夹里有一份 web3.min.js
使用上面的 script 标签代码把 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>
<!-- 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 节点提供者:
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来使用我们的应用。
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 代码写在里面。
把上面用来检测 MetaMask 是否安装的模板代码粘贴进来。请粘贴到以 window.addEventListener
开头的代码块中。
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>
// 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 在以太坊上的地址是 YOUR_CONTRACT_ADDRESS
。
你需要在部署后复制这个地址以来和你的智能合约对话。
合约ABI
另一个 Web3.js 为了要和你的智能合约对话而需要的东西是 ABI。
ABI
意为应用二进制接口(Application Binary Interface)。 基本上,它是以 JSON 格式表示合约的方法,告诉 Web3.js 如何以合同理解的方式格式化函数调用。
当你编译你的合约向以太坊部署时(我们将后边详述), Solidity 编译器会给你 ABI,所以除了合约地址,你还需要把这个也复制下来。
因为我们这一课不会讲述部署,所以现在我们已经帮你编译了 ABI 并放在了名为cryptozombies_abi.js
,文件中,保存在一个名为 cryptozombiesABI
的变量中。
如果我们将cryptozombies_abi.js
包含进我们的项目,我们就能通过那个变量访问 CryptoZombies ABI 。
cryptozombies_abi.js
文件:
var cryptozombiesABI = [
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_tokenId",
"type": "uint256"
}
],
"name": "approve",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_zombieId",
"type": "uint256"
}
],
"name": "levelUp",
"outputs": [],
"payable": true,
"stateMutability": "payable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_zombieId",
"type": "uint256"
},
{
"name": "_kittyId",
"type": "uint256"
}
],
"name": "feedOnKitty",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
}
],
"name": "zombies",
"outputs": [
{
"name": "name",
"type": "string"
},
{
"name": "dna",
"type": "uint256"
},
{
"name": "level",
"type": "uint32"
},
{
"name": "readyTime",
"type": "uint32"
},
{
"name": "winCount",
"type": "uint16"
},
{
"name": "lossCount",
"type": "uint16"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "withdraw",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"name": "getZombiesByOwner",
"outputs": [
{
"name": "",
"type": "uint256[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
}
],
"name": "zombieToOwner",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_address",
"type": "address"
}
],
"name": "setKittyContractAddress",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_zombieId",
"type": "uint256"
},
{
"name": "_newDna",
"type": "uint256"
}
],
"name": "changeDna",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_tokenId",
"type": "uint256"
}
],
"name": "ownerOf",
"outputs": [
{
"name": "_owner",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "_balance",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_name",
"type": "string"
}
],
"name": "createRandomZombie",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "owner",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_tokenId",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getAllZombies",
"outputs": [
{
"name": "",
"type": "uint256[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_tokenId",
"type": "uint256"
}
],
"name": "takeOwnership",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_zombieId",
"type": "uint256"
},
{
"name": "_newName",
"type": "string"
}
],
"name": "changeName",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_fee",
"type": "uint256"
}
],
"name": "setLevelUpFee",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_zombieId",
"type": "uint256"
},
{
"name": "_targetId",
"type": "uint256"
}
],
"name": "attack",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "_from",
"type": "address"
},
{
"indexed": true,
"name": "_to",
"type": "address"
},
{
"indexed": false,
"name": "_tokenId",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "_owner",
"type": "address"
},
{
"indexed": true,
"name": "_approved",
"type": "address"
},
{
"indexed": false,
"name": "_tokenId",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "attackResult",
"type": "bool"
},
{
"indexed": false,
"name": "winCount",
"type": "uint16"
},
{
"indexed": false,
"name": "lossCount",
"type": "uint16"
}
],
"name": "AttackResult",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "zombieId",
"type": "uint256"
},
{
"indexed": false,
"name": "name",
"type": "string"
},
{
"indexed": false,
"name": "dna",
"type": "uint256"
}
],
"name": "NewZombie",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
}
]
实例化Web3.js
一旦你有了合约的地址和 ABI,你可以像这样来实例化 Web3.js。
// 实例化 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 引入的cryptoZombiesABI
和cryptoZombiesAddress
)。
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>
<!-- 1\. 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
用来调用 view
和 pure
函数。它只运行在本地节点,不会在区块链上创建事务。
复习:
view
和pure
函数是只读的并不会改变区块链的状态。它们也不会消耗任何gas。用户也不会被要求用MetaMask对事务签名。
使用 Web3.js,你可以如下 call
一个名为myMethod
的方法并传入一个 123 作为参数:
myContract.methods.myMethod(123).call()
Send
send
将创建一个事务并改变区块链上的数据。你需要用 send
来调用任何非 view
或者 pure
的函数。
注意:
send
一个事务将要求用户支付gas,并会要求弹出对话框请求用户使用 Metamask 对事务签名。在我们使用 Metamask 作为我们的 web3 提供者的时候,所有这一切都会在我们调用send()
的时候自动发生。而我们自己无需在代码中操心这一切,挺爽的吧。
使用 Web3.js, 你可以像这样 send 一个事务调用myMethod 并传入 123 作为参数:
myContract.methods.myMethod(123).send()
语法几乎 call()一模一样。
获取僵尸数据
来看一个使用 call
读取我们合约数据的真实例子
回忆一下,我们定义我们的僵尸数组为 公开(public):
Zombie[] public zombies;
在 Solidity 里,当你定义一个 public
变量的时候, 它将自动定义一个公开的 "getter" 同名方法, 所以如果你像要查看 id 为 15 的僵尸,你可以像一个函数一样调用它: zombies(15)
.
这是如何在外面的前端界面中写一个 JavaScript 方法来传入一个僵尸 id,在我们的合同中查询那个僵尸并返回结果
注意: 本课中所有的示例代码都使用 Web3.js 的 1.0 版,此版本使用的是 Promises 而不是回调函数。你在线上看到的其他教程可能还在使用老版的 Web3.js。在1.0版中,语法改变了不少。如果你从其他教程中复制代码,先确保你们使用的是相同版本的Web3.js。
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 zombies
,id为传入参数的僵尸信息。
注意这是 异步的,就像从外部服务器中调用API。所以 Web3 在这里返回了一个 Promises. (如果你对 JavaScript的 Promises 不了解,最好先去学习一下这方面知识再继续)。
一旦那个 promise
被 resolve
, (意味着我们从 Web3 提供者那里获得了响应),我们的例子代码将执行 then 语句中的代码,在控制台打出 result。
result 是一个像这样的 JavaScript 对象:
{
"name": "H4XF13LD MORRIS'S COOLER OLDER BROTHER",
"dna": "1337133713371337",
"level": "9999",
"readyTime": "1522498671",
"winCount": "999999999",
"lossCount": "0" // Obviously.
}
我们可以用一些前端逻辑代码来解析这个对象并在前端界面友好展示。
实战演练
我们已经把 getZombieDetails
复制进了代码。
- 1、先为zombieToOwner 创建一个类似的函数。如果你还记得 ZombieFactory.sol,我们有一个长这样的映射:
- `mapping (uint => address) public zombieToOwner;
- 定义一个 JavaScript 方法,起名为 zombieToOwner。和上面的 getZombieDetails 类似, 它将接收一个id 作为参数,并返回一个 Web3.js call 我们合约里的zombieToOwner 。
- 2、之后在下面,为
getZombiesByOwner
定义一个方法。如果你还能记起 ZombieHelper.sol,这个方法定义像这样: function getZombiesByOwner(address _owner)
- 我们的 getZombiesByOwner 方法将接收 owner 作为参数,并返回一个对我们函数 getZombiesByOwner的 Web3.js call
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>
<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>
Promise学习
promise
是异步编程
的一种解决方案,比传统的解决方案–回调函数和事件--更合理和更强大。它由社区最早提出和实现,ES6将其写进了语言标准,统一了语法,原生提供了Promise
所谓Promise ,简单说就是一个容器,里面保存着某个未来才回结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。
Promise 对象的状态不受外界影响
三种状态:
- pending:进行中
- fulfilled :已经成功
- rejected 已经失败
状态改变:
Promise对象的状态改变,只有两种可能:
- 从pending变为fulfilled
- 从pending变为rejected。
这两种情况只要发生,状态就凝固
了,不会再变了,这时就称为resolved
(已定型)
基本用法
ES6规定,Promise对象是一个构造函数
,用来生成Promise实例:
const promist = new Promise(function(resolve,reject){
if(/*异步操作成功*/){
resolve(value);
}else{
reject(error);
}
})
-
resolve
函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去; -
reject
函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise 实例生成以后,可以用then
方法分别指定resolved
状态和rejected
状态的回调函数。
promise.then(function(value){
//success
},function(error){
//failure
});
示例:
function timeout(ms){
return new Promise((resolve,reject)=>{
setTimeout(resolve,ms,'done');
});
}
timeout(100).then((value)=>{
console.log(value);
});
let promise = new Promise(function(resolve,reject){
console.log('Promise');
resolve();
});
promise.then(function(){
console.log('resolved');
});
console.log('Hi!');
//Promise
//Hi!
//resolved
//异步加载图片
function loadImageAsync(url){
return new Promise(function(resolve,reject){
const image = new Image();
image.onload = function(){
resolve(image);
};
image.onerror = function(){
reject(new Error('error');
};
image.src = url;
});
}
下面是一个用Promise对象实现的 Ajax 操作的例子。
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
五、MetaMask和账户
接下来我们综合一下——比如我们想让我们应用的首页显示用户的整个僵尸大军。
毫无疑问我们首先需要用 getZombiesByOwner(owner)
来查询当前用户的所有僵尸ID。
但是我们的 Solidity 合约需要 owner
作为 Solidity address
。我们如何能知道应用用户的地址呢?
获得MetaMask中的用户账户
MetaMask 允许用户在扩展中管理多个账户。
我们可以通过这样来获取 web3 变量中激活的当前账户:
var userAccount = web3.eth.accounts[0]
因为用户可以随时在 MetaMask 中切换账户,我们的应用需要监控这个变量,一旦改变就要相应更新界面。例如,若用户的首页展示它们的僵尸大军,当他们在 MetaMask 中切换了账号,我们就需要更新页面来展示新选择的账户的僵尸大军。
我们可以通过 setInterval
方法来做:
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();
替换成一个getZombiesByOwner
的call
函数,并传入userAccount
。 - 4、在 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];
// 调用一些方法来更新界面
// updateInterface();
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数组,像这样:
[0, 13, 47]
因为我们想让我们的 displayZombies 方法做这些事:
- 1、首先清除 #zombies 的内容以防里面已经有什么内容(这样当用户切换账号的时候,之前账号的僵尸大军数据就会被清除)
- 2、循环遍历 id,对每一个id调用 getZombieDetails(id), 从我们的合约中获得这个僵尸的数据。
- 3、将获得的僵尸数据放进一个HTML模板中以格式化显示,追加进 #zombies 里面。
再次声明,我们只用了 JQuery,没有任何模板引擎,所以会非常丑。不过这只是一个如何展示僵尸数据的示例而已。
// 在合约中查找僵尸数据,返回一个对象
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-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 里面。
<!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) {
var ele = getZombieDetails(id);
$("#zombies").append(ele);
}
*/
for (id of ids) {
// 获取到的结果通过then之后传给闭包函数做参数
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>