以太坊开发实战学习-Web3.js(九)

经过前边的学习,DApp 的 Solidity 合约部分就完成了。如今咱们来作一个基本的网页好让你的用户能玩它。 要作到这一点,咱们将使用以太坊基金发布的 JavaScript 库 —— Web3.js.

1、Web3.js简介

什么是 Web3.js?

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

  • 一、智能合约的地址
  • 二、你想调用的方法,以及
  • 三、你想传入那个方法的参数

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

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

幸运的是 Web3.js 把这些使人讨厌的查询语句都隐藏起来了, 因此你只须要与方便易懂的 JavaScript 界面进行交互便可。前端

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

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

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

准备工做

取决于你的项目工做流程和你的爱好,你能够用一些经常使用工具把 Web3.js 添加进来:git

// 用 NPM
npm install web3

// 用 Yarn
yarn add web3

// 用 Bower
bower install web3

// ...或者其余。

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

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

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

实战演练

新建一个HTML 项目空壳 —— index.html。假设在和 index.html 同个文件夹里有一份 web3.min.jsajax

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

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>

2、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>

3、和合约对话

如今,咱们已经用 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);

实战演练

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

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>

4、调用和合约函数

咱们的合约配置好了!如今来用 Web3.js 和它对话。

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

call

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

复习: viewpure 函数是 只读的并不会改变区块链的状态。它们也不会消耗任何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 不了解,最好先去学习一下这方面知识再继续)。

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

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;
  • 定义一个 JavaScript 方法,起名为 zombieToOwner。和上面的 getZombieDetails 相似, 它将接收一个id 做为参数,并返回一个 Web3.js call 咱们合约里的zombieToOwner 。
  • 二、以后在下面,为 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);
});

5、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 中的激活帐户,并在帐户发生改变的时候刷新显示。

  • 一、定义一个名为userAccount的变量,不给任何初始值。
  • 二、在 startApp()函数的最后,复制粘贴上面样板代码中的 accountInterval 方法进去。
  • 三、将 updateInterface();替换成一个 getZombiesByOwnercall 函数,并传入 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];
          // 调用一些方法来更新界面
          // 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>

6、显示合约数据

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

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

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

显示僵尸数据

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

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

[0, 13, 47]

由于咱们想让咱们的 displayZombies 方法作这些事:

  • 一、首先清除 #zombies 的内容以防里面已经有什么内容(这样当用户切换帐号的时候,以前帐号的僵尸大军数据就会被清除)
  • 二、循环遍历 id,对每个id调用 getZombieDetails(id), 从咱们的合约中得到这个僵尸的数据。
  • 三、将得到的僵尸数据放进一个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 方法。来一块儿实现它。

  • 一、首先咱们须要清空 #zombies 的内容。 用JQuery,你能够这样作: $("#zombies").empty();。
  • 二、接下来,咱们要循环遍历全部的 id,循环这样用: for (id of ids) {
  • 三、在循环内部,复制粘贴上面的代码,对每个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>
相关文章
相关标签/搜索