以太坊開發實戰學習-Web3.js(九)

Corwien發表於2018-06-17

通過前邊的學習,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 引入的 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>

四、呼叫和合約函式

我們的合約配置好了!現在來用 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 複製進了程式碼。

  • 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();替換成一個 getZombiesByOwnercall 函式,並傳入 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>

相關文章