以太坊開發實戰學習-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>
相關文章
相關標籤/搜索