前端通過web3調(diào)用智能合約進行邏輯交互

本例子通過crytozombie的例子,講解前端界面與智能合約交互的邏輯。界面一般 用HTML, JavaScript(包括 ES6 promises)虎锚,以及 JQuery 寫網(wǎng)站了,JavaScript 來寫,并不是 Solidity并不能直接與前端界面進行數(shù)據(jù)交互犀盟。

第1章: 介紹 Web3.js
完成第五課以后,我們的僵尸 DApp 的 Solidity 合約部分就完成了∮牵現(xiàn)在我們來做一個基本的網(wǎng)頁好讓你的用戶能玩它阅畴。 要做到這一點,我們將使用以太坊基金發(fā)布的 JavaScript 庫 —— Web3.js.

什么是 Web3.js?
還記得么迅耘?以太坊網(wǎng)絡是由節(jié)點組成的贱枣,每一個節(jié)點都包含了區(qū)塊鏈的一份拷貝。當你想要調(diào)用一份智能合約的一個方法颤专,你需要從其中一個節(jié)點中查找并告訴它:

智能合約的地址
你想調(diào)用的方法纽哥,以及
你想傳入那個方法的參數(shù)
以太坊節(jié)點只能識別一種叫做 JSON-RPC 的語言。這種語言直接讀起來并不好懂栖秕。當你你想調(diào)用一個合約的方法的時候春塌,需要發(fā)送的查詢語句將會是這樣的:

{
    "jsonrpc":"2.0",
    "method":"eth_sendTransaction",
    "params":[
        {
            "from":"0xb60e8dd61c5d32be8058bb8eb970870f07233155",
            "to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567",
            "gas":"0x76c0",
            "gasPrice":"0x9184e72a000",
            "value":"0x9184e72a",
            "data":"0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"
        }
    ],
    "id":1
}

幸運的是 Web3.js 把這些令人討厭的查詢語句都隱藏起來了, 所以你只需要與方便易懂的 JavaScript 界面進行交互即可簇捍。

你不需要構(gòu)建上面的查詢語句只壳,在你的代碼中調(diào)用一個函數(shù)看起來將是這樣:

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

我們將在接下來的幾章詳細解釋這些語句,不過首先我們來把 Web3.js 環(huán)境搭建起來暑塑。

準備好了么吼句?
取決于你的項目工作流程和你的愛好,你可以用一些常用工具把 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>

因為我們不想讓你花太多在項目環(huán)境搭建上搞隐,在本教程中我們將使用上面的 script 標簽來將 Web3.js 引入。

實戰(zhàn)演習
我們?yōu)槟憬⒘艘粋€HTML 項目空殼 —— index.html远搪。假設在和 index.html 同個文件夾里有一份 web3.min.js

使用上面的 script 標簽代碼把 web3.js 添加進去以備接下來使用劣纲。

<!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 提供者
太棒了。現(xiàn)在我們的項目中有了Web3.js, 來初始化它然后和區(qū)塊鏈對話吧终娃。

首先我們需要 Web3 Provider .

要記住味廊,以太坊是由共享同一份數(shù)據(jù)的相同拷貝的 節(jié)點 構(gòu)成的。 在 Web3.js 里設置 Web3 的 Provider(提供者) 告訴我們的代碼應該和 哪個節(jié)點 交互來處理我們的讀寫棠耕。這就好像在傳統(tǒng)的 Web 應用程序中為你的 API 調(diào)用設置遠程 Web 服務器的網(wǎng)址余佛。

你可以運行你自己的以太坊節(jié)點來作為 Provider。 不過窍荧,有一個第三方的服務辉巡,可以讓你的生活變得輕松點,讓你不必為了給你的用戶提供DApp而維護一個以太坊節(jié)點— Infura .

Infura
Infura 是一個服務蕊退,它維護了很多以太坊節(jié)點并提供了一個緩存層來實現(xiàn)高速讀取郊楣。你可以用他們的 API 來免費訪問這個服務。 用 Infura 作為節(jié)點提供者瓤荔,你可以不用自己運營節(jié)點就能很可靠地向以太坊發(fā)送净蚤、接收信息。

你可以通過這樣把 Infura 作為你的 Web3 節(jié)點提供者:

var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));

不過输硝,因為我們的 DApp 將被很多人使用今瀑,這些用戶不單會從區(qū)塊鏈讀取信息,還會向區(qū)塊鏈 寫 入信息点把,我們需要用一個方法讓用戶可以用他們的私鑰給事務簽名橘荠。

注意: 以太坊 (以及通常意義上的 blockchains )使用一個公鑰/私鑰對來對給事務做數(shù)字簽名。把它想成一個數(shù)字簽名的異常安全的密碼郎逃。這樣當我修改區(qū)塊鏈上的數(shù)據(jù)的時候哥童,我可以用我的公鑰來 證明 我就是簽名的那個。但是因為沒人知道我的私鑰褒翰,所以沒人能偽造我的事務贮懈。

加密學非常復雜,所以除非你是個專家并且的確知道自己在做什么优训,你最好不要在你應用的前端中管理你用戶的私鑰朵你。

不過幸運的是,你并不需要型宙,已經(jīng)有可以幫你處理這件事的服務了: Metamask .

Metamask
Metamask 是 Chrome 和 Firefox 的瀏覽器擴展, 它能讓用戶安全地維護他們的以太坊賬戶和私鑰伦吠, 并用他們的賬戶和使用 Web3.js 的網(wǎng)站互動(如果你還沒用過它妆兑,你肯定會想去安裝的——這樣你的瀏覽器就能使用 Web3.js 了魂拦,然后你就可以和任何與以太坊區(qū)塊鏈通信的網(wǎng)站交互了)

作為開發(fā)者,如果你想讓用戶從他們的瀏覽器里通過網(wǎng)站和你的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是否已經(jīng)注入到(Mist/MetaMask)
  if (typeof web3 !== 'undefined') {
    // 使用 Mist/MetaMask 的提供者
    web3js = new Web3(web3.currentProvider);
  } else {
    // 處理用戶沒安裝的情況, 比如顯示一個消息
    // 告訴他們要安裝 MetaMask 來使用我們的應用
  }

  // 現(xiàn)在你可以啟動你的應用并自由訪問 Web3.js:
  startApp()

})

你可以在你所有的應用中使用這段樣板代碼潘拨,好檢查用戶是否安裝以及告訴用戶安裝 MetaMask吊输。

注意: 除了MetaMask,你的用戶也可能在使用其他他的私鑰管理應用铁追,比如 Mist 瀏覽器季蚂。不過,它們都實現(xiàn)了相同的模式來注入 web3 變量脂信。所以我這里描述的方法對兩者是通用的癣蟋。

實戰(zhàn)演習
我們在HTML文件中的 </body> 標簽前面放置了一個空的 script 標簽≌粒可以把這節(jié)課的 JavaScript 代碼寫在里面疯搅。

把上面用來檢測 MetaMask 是否安裝的模板代碼粘貼進來。請粘貼到以 window.addEventListener 開頭的代碼塊中埋泵。

<!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是否已經(jīng)注入到(Mist/MetaMask)
      if (typeof web3 !== 'undefined') {
        // 使用 Mist/MetaMask 的提供者
        web3js = new Web3(web3.currentProvider);
      } else {
        // 處理用戶沒安裝的情況幔欧, 比如顯示一個消息
        // 告訴他們要安裝 MetaMask 來使用我們的應用
      }

      // 現(xiàn)在你可以啟動你的應用并自由訪問 Web3.js:
      startApp()

    })
    </script>
  </body>
</html>

第3章: 和合約對話
現(xiàn)在,我們已經(jīng)用 MetaMask 的 Web3 提供者初始化了 Web3.js丽声。接下來就讓它和我們的智能合約對話吧礁蔗。

Web3.js 需要兩個東西來和你的合約對話: 它的 地址 和它的 ABI。

合約地址
在你寫完了你的智能合約后雁社,你需要編譯它并把它部署到以太坊浴井。我們將在下一課中詳述部署,因為它和寫代碼是截然不同的過程霉撵,所以我們決定打亂順序磺浙,先來講 Web3.js洪囤。

在你部署智能合約以后,它將獲得一個以太坊上的永久地址撕氧。如果你還記得第二課瘤缩,CryptoKitties 在以太坊上的地址是 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d。

你需要在部署后復制這個地址以來和你的智能合約對話伦泥。

合約 ABI
另一個 Web3.js 為了要和你的智能合約對話而需要的東西是 ABI剥啤。

ABI 意為應用二進制接口(Application Binary Interface)。 基本上不脯,它是以 JSON 格式表示合約的方法府怯,告訴 Web3.js 如何以合同理解的方式格式化函數(shù)調(diào)用。

當你編譯你的合約向以太坊部署時(我們將在第七課詳述)跨新, Solidity 編譯器會給你 ABI富腊,所以除了合約地址,你還需要把這個也復制下來域帐。

因為我們這一課不會講述部署赘被,所以現(xiàn)在我們已經(jīng)幫你編譯了 ABI 并放在了名為cryptozombies_abi.js,文件中肖揣,保存在一個名為 cryptoZombiesABI 的變量中民假。

如果我們將cryptozombies_abi.js 包含進我們的項目,我們就能通過那個變量訪問 CryptoZombies ABI 龙优。

實例化 Web3.js
一旦你有了合約的地址和 ABI羊异,你可以像這樣來實例化 Web3.js。

// 實例化 myContract
var myContract = new web3js.eth.Contract(myABI, myContractAddress);

實戰(zhàn)演習
在文件的 <head> 標簽塊中彤断,用 script 標簽引入cryptozombies_abi.js野舶,好把 ABI 的定義引入項目。

在 <body> 里的 <script> 開頭 , 定義一個var宰衙,取名 cryptoZombies平道, 不過不要對其賦值,稍后我們將用這個這個變量來存儲我們實例化合約供炼。

接下來一屋,創(chuàng)建一個名為 startApp() 的 function。 接下來兩步來完成這個方法袋哼。

startApp() 里應該做的第一件事是定義一個名為cryptoZombiesAddress 的變量并賦值為"你的合約地址" (這是你的合約在以太坊主網(wǎng)上的地址)冀墨。

最后,來實例化我們的合約涛贯。模仿我們上面的代碼诽嘉,將 cryptoZombies 賦值為 new web3js.eth.Contract (使用我們上面代碼中通過 script 引入的 cryptoZombiesABI 和 cryptoZombiesAddress)。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>CryptoZombies front-end</title>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
    <!-- 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章: 調(diào)用和合約函數(shù)
我們的合約配置好了!現(xiàn)在來用 Web3.js 和它對話虫腋。

Web3.js 有兩個方法來調(diào)用我們合約的函數(shù): call and send.

Call
call 用來調(diào)用 view 和 pure 函數(shù)身冬。它只運行在本地節(jié)點,不會在區(qū)塊鏈上創(chuàng)建事務岔乔。

復習: view 和 pure 函數(shù)是只讀的并不會改變區(qū)塊鏈的狀態(tài)。它們也不會消耗任何gas滚躯。用戶也不會被要求用MetaMask對事務簽名雏门。

使用 Web3.js,你可以如下 call 一個名為myMethod的方法并傳入一個 123 作為參數(shù):

myContract.methods.myMethod(123).call()

Send
send 將創(chuàng)建一個事務并改變區(qū)塊鏈上的數(shù)據(jù)掸掏。你需要用 send 來調(diào)用任何非 view 或者 pure 的函數(shù)茁影。

注意: send 一個事務將要求用戶支付gas,并會要求彈出對話框請求用戶使用 Metamask 對事務簽名丧凤。在我們使用 Metamask 作為我們的 web3 提供者的時候,所有這一切都會在我們調(diào)用 send() 的時候自動發(fā)生。而我們自己無需在代碼中操心這一切钥平,挺爽的吧拉盾。

使用 Web3.js, 你可以像這樣 send 一個事務調(diào)用myMethod 并傳入 123 作為參數(shù):

myContract.methods.myMethod(123).send()

語法幾乎 call()一模一樣。

獲取僵尸數(shù)據(jù)
來看一個使用 call 讀取我們合約數(shù)據(jù)的真實例子

回憶一下仍侥,我們定義我們的僵尸數(shù)組為 公開(public):

Zombie[] public zombies;

在 Solidity 里要出,當你定義一個 public變量的時候, 它將自動定義一個公開的 “getter” 同名方法农渊, 所以如果你像要查看 id 為 15 的僵尸患蹂,你可以像一個函數(shù)一樣調(diào)用它: zombies(15).

這是如何在外面的前端界面中寫一個 JavaScript 方法來傳入一個僵尸 id,在我們的合同中查詢那個僵尸并返回結(jié)果

注意: 本課中所有的示例代碼都使用 Web3.js 的 1.0 版砸紊,此版本使用的是 Promises 而不是回調(diào)函數(shù)传于。你在線上看到的其他教程可能還在使用老版的 Web3.js。在1.0版中醉顽,語法改變了不少沼溜。如果你從其他教程中復制代碼,先確保你們使用的是相同版本的Web3.js徽鼎。

function getZombieDetails(id) {
  return cryptoZombies.methods.zombies(id).call()
}

// 調(diào)用函數(shù)并做一些其他事情
getZombieDetails(15)
.then(function(result) {
  console.log("Zombie 15: " + JSON.stringify(result));
});

我們來看看這里都做了什么

cryptoZombies.methods.zombies(id).call() 將和 Web3 提供者節(jié)點通信盛末,告訴它返回從我們的合約中的 Zombie[] public zombies,id為傳入?yún)?shù)的僵尸信息否淤。

注意這是 異步的悄但,就像從外部服務器中調(diào)用API。所以 Web3 在這里返回了一個 Promises. (如果你對 JavaScript的 Promises 不了解石抡,最好先去學習一下這方面知識再繼續(xù))檐嚣。

一旦那個 promise 被 resolve, (意味著我們從 Web3 提供者那里獲得了響應),我們的例子代碼將執(zhí)行 then 語句中的代碼,在控制臺打出 result嚎京。

result 是一個像這樣的 JavaScript 對象:

{
  "name": "H4XF13LD MORRIS'S COOLER OLDER BROTHER",
  "dna": "1337133713371337",
  "level": "9999",
  "readyTime": "1522498671",
  "winCount": "999999999",
  "lossCount": "0" // Obviously.
}

我們可以用一些前端邏輯代碼來解析這個對象并在前端界面友好展示嗡贺。

實戰(zhàn)演習
我們已經(jīng)幫你把 getZombieDetails 復制進了代碼。

先為zombieToOwner 創(chuàng)建一個類似的函數(shù)鞍帝。如果你還記得 ZombieFactory.sol诫睬,我們有一個長這樣的映射:

mapping (uint => address) public zombieToOwner;

定義一個 JavaScript 方法,起名為 zombieToOwner帕涌。和上面的 getZombieDetails 類似摄凡, 它將接收一個id 作為參數(shù),并返回一個 Web3.js call 我們合約里的zombieToOwner 蚓曼。

之后在下面亲澡,為 getZombiesByOwner 定義一個方法。如果你還能記起 ZombieHelper.sol纫版,這個方法定義像這樣:

function getZombiesByOwner(address _owner)

我們的 getZombiesByOwner 方法將接收 owner 作為參數(shù)床绪,并返回一個對我們函數(shù) getZombiesByOwner的 Web3.js call

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>CryptoZombies front-end</title>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
    <script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
  </head>
  <body>

    <script>
      var cryptoZombies;

      function startApp() {
        var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
      }

   

      // 1. Define `zombieToOwner` here
      function getZombieDetails(id) {
        return cryptoZombies.methods.zombies(id).call()
      }
      // 2. Define `getZombiesByOwner` here
      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>

第5章: MetaMask 和賬戶
太棒了!你成功地寫了一些前端代碼來和你的第一個智能合約交互其弊。

接下來我們綜合一下——比如我們想讓我們應用的首頁顯示用戶的整個僵尸大軍癞己。

毫無疑問我們首先需要用 getZombiesByOwner(owner) 來查詢當前用戶的所有僵尸ID。

但是我們的 Solidity 合約需要 owner 作為 Solidity address梭伐。我們?nèi)绾文苤缿糜脩舻牡刂纺兀?/p>

獲得 MetaMask中的用戶賬戶
MetaMask 允許用戶在擴展中管理多個賬戶末秃。

我們可以通過這樣來獲取 web3 變量中激活的當前賬戶:

var userAccount = web3.eth.accounts[0]

因為用戶可以隨時在 MetaMask 中切換賬戶,我們的應用需要監(jiān)控這個變量籽御,一旦改變就要相應更新界面练慕。例如,若用戶的首頁展示它們的僵尸大軍技掏,當他們在 MetaMask 中切換了賬號铃将,我們就需要更新頁面來展示新選擇的賬戶的僵尸大軍。

我們可以通過 setInterval 方法來做:

var accountInterval = setInterval(function() {
  // 檢查賬戶是否切換
  if (web3.eth.accounts[0] !== userAccount) {
    userAccount = web3.eth.accounts[0];
    // 調(diào)用一些方法來更新界面
    updateInterface();
  }
}, 100);

這段代碼做的是哑梳,每100毫秒檢查一次 userAccount 是否還等于 web3.eth.accounts[0] (比如:用戶是否還激活了那個賬戶)劲阎。若不等,則將 當前激活用戶賦值給 userAccount鸠真,然后調(diào)用一個函數(shù)來更新界面悯仙。

實戰(zhàn)演習
我們來讓應用在頁面第一次加載的時候顯示用戶的僵尸大軍,監(jiān)控當前 MetaMask 中的激活賬戶吠卷,并在賬戶發(fā)生改變的時候刷新顯示锡垄。

定義一個名為 userAccount的變量,不給任何初始值祭隔。

在 startApp()函數(shù)的最后货岭,復制粘貼上面樣板代碼中的 accountInterval 方法進去。

將 updateInterface();替換成一個 getZombiesByOwner 的 call 函數(shù),并傳入 userAccount千贯。

在 getZombiesByOwner 后面鏈式調(diào)用 then 語句屯仗,并將返回的結(jié)果傳入名為 displayZombies 的函數(shù)。 (語句像這樣: .then(displayZombies);).

我們還沒有 displayZombies 函數(shù)搔谴,將于下一章實現(xiàn)魁袜。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>CryptoZombies front-end</title>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
    <script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
  </head>
  <body>

    <script>
      var cryptoZombies;
      // 1. declare `userAccount` here
      var userAccount;
      function startApp() {
        var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

        // 2. Create `setInterval` code here
        var accountInterval = setInterval(function() {
          
          if (web3.eth.accounts[0] !== userAccount) {
            userAccount = web3.eth.accounts[0];
            
            getZombiesByOwner(userAccount)
            .then(displayZombies);
          }
        }, 100);
      }

      function getZombieDetails(id) {
        return cryptoZombies.methods.zombies(id).call()
      }

      function zombieToOwner(id) {
        return cryptoZombies.methods.zombieToOwner(id).call()
      }

      function getZombiesByOwner(owner) {
        return cryptoZombies.methods.getZombiesByOwner(owner).call()
      }

      window.addEventListener('load', function() {

        // Checking if Web3 has been injected by the browser (Mist/MetaMask)
        if (typeof web3 !== 'undefined') {
          // Use Mist/MetaMask's provider
          web3js = new Web3(web3.currentProvider);
        } else {
          // Handle the case where the user doesn't have Metamask installed
          // Probably show them a message prompting them to install Metamask
        }

        // Now you can start your app & access web3 freely:
        startApp()

      })
    </script>
  </body>
</html>

第6章: 顯示僵尸大軍
如果我們不向你展示如何顯示你從合約獲取的數(shù)據(jù),那這個教程就太不完整了敦第。

在實際應用中慌核,你肯定想要在應用中使用諸如 React 或 Vue.js 這樣的前端框架來讓你的前端開發(fā)變得輕松一些。不過要教授 React 或者 Vue.js 知識的話申尼,就大大超出了本教程的范疇——它們本身就需要幾節(jié)課甚至一整個教程來教學。

所以為了讓 CryptoZombies.io 專注于以太坊和智能合約垫桂,我們將使用 JQuery 來做一個快速示例师幕,展示如何解析和展示從智能合約中拿到的數(shù)據(jù)。

顯示僵尸數(shù)據(jù) — 一個粗略的例子
我們已經(jīng)在代碼中添加了一個空的代碼塊 <div id="zombies"></div>诬滩, 在 displayZombies 方法中也同樣有一個霹粥。

回憶一下在之前章節(jié)中我們在 startApp() 方法內(nèi)部調(diào)用了 displayZombies 并傳入了 call getZombiesByOwner 獲得的結(jié)果,它將被傳入一個僵尸ID數(shù)組疼鸟,像這樣:
[0, 13, 47]

因為我們想讓我們的 displayZombies 方法做這些事:

首先清除 #zombies 的內(nèi)容以防里面已經(jīng)有什么內(nèi)容(這樣當用戶切換賬號的時候后控,之前賬號的僵尸大軍數(shù)據(jù)就會被清除)

循環(huán)遍歷 id,對每一個id調(diào)用 getZombieDetails(id)空镜, 從我們的合約中獲得這個僵尸的數(shù)據(jù)浩淘。

將獲得的僵尸數(shù)據(jù)放進一個HTML模板中以格式化顯示,追加進 #zombies 里面吴攒。

再次聲明张抄,我們只用了 JQuery,沒有任何模板引擎洼怔,所以會非常丑署惯。不過這只是一個如何展示僵尸數(shù)據(jù)的示例而已。

// 在合約中查找僵尸數(shù)據(jù)镣隶,返回一個對象
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 轉(zhuǎn)換成圖片來顯示你的僵尸。

我們通過把 DNA 字符串分割成小的字符串來做到這一點域那,每2位數(shù)字代表一個圖片蜕依,類似這樣:

// 得到一個 1-7 的數(shù)字來表示僵尸的頭:
var head = parseInt(zombie.dna.substring(0, 2)) % 7 + 1

// 我們有7張頭部圖片:
var headSrc = "../assets/zombieparts/head-" + i + ".png"

每一個模塊都用 CSS 絕對定位來顯示,在一個上面疊加另外一個。

如果你想看我們的具體實現(xiàn)样眠,我們將用來展示僵尸形象的 Vue.js 模塊開源了: 點擊這里.

不過友瘤,因為那個文件中有太多行代碼, 超出了本教程的討論范圍檐束。我們依然還是使用上面超級簡單的 JQuery 實現(xiàn)辫秧,把美化僵尸的工作作為家庭作業(yè)留給你了?

實戰(zhàn)演習
我們?yōu)槟銊?chuàng)建了一個空的 displayZombies 方法。來一起實現(xiàn)它被丧。

首先我們需要清空 #zombies 的內(nèi)容盟戏。 用JQuery,你可以這樣做: $("#zombies").empty();甥桂。

接下來柿究,我們要循環(huán)遍歷所有的 id,循環(huán)這樣用: for (id of ids) {

在循環(huán)內(nèi)部黄选,復制粘貼上面的代碼蝇摸,對每一個id調(diào)用 getZombieDetails(id),然后用 $("#zombies").append(...) 把內(nèi)容追加進我們的 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) {
          
          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>

第7章: 發(fā)送事務
這下我們的界面能檢測用戶的 MetaMask 賬戶貌夕,并自動在首頁顯示它們的僵尸大軍了,有沒有很棒民镜?

現(xiàn)在我們來看看用 send 函數(shù)來修改我們智能合約里面的數(shù)據(jù)啡专。

相對 call 函數(shù), send 函數(shù)有如下主要區(qū)別:

send 一個事務需要一個 from 地址來表明誰在調(diào)用這個函數(shù)(也就是你 Solidity 代碼里的 msg.sender )制圈。 我們需要這是我們 DApp 的用戶们童,這樣一來 MetaMask 才會彈出提示讓他們對事務簽名。

send 一個事務將花費 gas

在用戶 send 一個事務到該事務對區(qū)塊鏈產(chǎn)生實際影響之間有一個不可忽略的延遲鲸鹦。這是因為我們必須等待事務被包含進一個區(qū)塊里病附,以太坊上一個區(qū)塊的時間平均下來是15秒左右。如果當前在以太坊上有大量掛起事務或者用戶發(fā)送了過低的 gas 價格亥鬓,我們的事務可能需要等待數(shù)個區(qū)塊才能被包含進去完沪,往往可能花費數(shù)分鐘。

所以在我們的代碼中我們需要編寫邏輯來處理這部分異步特性嵌戈。

生成一個僵尸
我們來看一個合約中一個新用戶將要調(diào)用的第一個函數(shù): createRandomZombie.

作為復習覆积,這里是合約中的 Solidity 代碼:

function createRandomZombie(string _name) public {
  require(ownerZombieCount[msg.sender] == 0);
  uint randDna = _generateRandomDna(_name);
  randDna = randDna - randDna % 100;
  _createZombie(_name, randDna);
}

這是如何在用 MetaMask 在 Web3.js 中調(diào)用這個函數(shù)的示例:

function createRandomZombie(name) {
  // 這將需要一段時間,所以在界面中告訴用戶這一點
  // 事務被發(fā)送出去了
  $("#txStatus").text("正在區(qū)塊鏈上創(chuàng)建僵尸熟呛,這將需要一會兒...");
  // 把事務發(fā)送到我們的合約:
  return cryptoZombies.methods.createRandomZombie(name)
  .send({ from: userAccount })
  .on("receipt", function(receipt) {
    $("#txStatus").text("成功生成了 " + name + "!");
    // 事務被區(qū)塊鏈接受了宽档,重新渲染界面
    getZombiesByOwner(userAccount).then(displayZombies);
  })
  .on("error", function(error) {
    // 告訴用戶合約失敗了
    $("#txStatus").text(error);
  });
}

我們的函數(shù) send 一個事務到我們的 Web3 提供者,然后鏈式添加一些事件監(jiān)聽:

receipt 將在合約被包含進以太坊區(qū)塊上以后被觸發(fā)庵朝,這意味著僵尸被創(chuàng)建并保存進我們的合約了吗冤。
error 將在事務未被成功包含進區(qū)塊后觸發(fā)又厉,比如用戶未支付足夠的 gas。我們需要在界面中通知用戶事務失敗以便他們可以再次嘗試椎瘟。
注意:你可以在調(diào)用 send 時選擇指定 gas 和 gasPrice覆致, 例如: .send({ from: userAccount, gas: 3000000 })。如果你不指定肺蔚,MetaMask 將讓用戶自己選擇數(shù)值煌妈。

實戰(zhàn)演習
我們添加了一個 div, 指定 ID 為 txStatus — 這樣我們可以通過更新這個 div 來通知用戶事務的狀態(tài)宣羊。

在 displayZombies下面璧诵, 復制粘貼上面 createRandomZombie 的代碼。

我們來實現(xiàn)另外一個函數(shù) feedOnKitty:

調(diào)用 feedOnKitty 的邏輯幾乎一樣 — 我們將發(fā)送一個事務來調(diào)用這個函數(shù)仇冯,并且成功的事務會為我們創(chuàng)建一個僵尸之宿,所以我們希望在成功后重新繪制界面。

在 createRandomZombie 下面復制粘貼它的代碼苛坚,改動這些地方:

a) 給其命名為 feedOnKitty比被, 它將接收兩個參數(shù) zombieId 和 kittyId

b) #txStatus 的文本內(nèi)容將更新為: "正在吃貓咪,這將需要一會兒..."

c) 讓其調(diào)用我們合約里面的 feedOnKitty 函數(shù)并傳入相同的參數(shù)

d) #txStatus 里面的的成功信息應該是 "吃了一只貓咪并生成了一只新僵尸炕婶!"

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>CryptoZombies front-end</title>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
    <script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
  </head>
  <body>
    <div id="txStatus"></div>
    <div id="zombies"></div>

    <script>
      var cryptoZombies;
      var userAccount;

      function startApp() {
        var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

        var accountInterval = setInterval(function() {
          // Check if account has changed
          if (web3.eth.accounts[0] !== userAccount) {
            userAccount = web3.eth.accounts[0];
            // Call a function to update the UI with the new account
            getZombiesByOwner(userAccount)
            .then(displayZombies);
          }
        }, 100);
      }

      function displayZombies(ids) {
        $("#zombies").empty();
        for (id of ids) {
          // Look up zombie details from our contract. Returns a `zombie` object
          getZombieDetails(id)
          .then(function(zombie) {
            // Using ES6's "template literals" to inject variables into the HTML.
            // Append each one to our #zombies div
            $("#zombies").append(`<div class="zombie">
              <ul>
                <li>Name: ${zombie.name}</li>
                <li>DNA: ${zombie.dna}</li>
                <li>Level: ${zombie.level}</li>
                <li>Wins: ${zombie.winCount}</li>
                <li>Losses: ${zombie.lossCount}</li>
                <li>Ready Time: ${zombie.readyTime}</li>
              </ul>
            </div>`);
          });
        }
      }

      // Start here
      function createRandomZombie(name) {
        
        
        $("#txStatus").text("正在區(qū)塊鏈上創(chuàng)建僵尸,這將需要一會兒...");
        
        return cryptoZombies.methods.createRandomZombie(name)
        .send({ from: userAccount })
        .on("receipt", function(receipt) {
          $("#txStatus").text("成功生成了 " + name + "!");
          
          getZombiesByOwner(userAccount).then(displayZombies);
        })
        .on("error", function(error) {
          
          $("#txStatus").text(error);
        });
      }

      function feedOnKitty(zombieId, kittyId) {
        
        
        $("#txStatus").text("正在吃貓咪莱预,這將需要一會兒...");
        
        return cryptoZombies.methods.feedOnKitty(zombieId, kittyId)
        .send({ from: userAccount })
        .on("receipt", function(receipt) {
          $("#txStatus").text("吃了一只貓咪并生成了一只新僵尸柠掂!");
          
          getZombiesByOwner(userAccount).then(displayZombies);
        })
        .on("error", function(error) {
          
          $("#txStatus").text(error);
        });
      }
      function getZombieDetails(id) {
        return cryptoZombies.methods.zombies(id).call()
      }

      function zombieToOwner(id) {
        return cryptoZombies.methods.zombieToOwner(id).call()
      }

      function getZombiesByOwner(owner) {
        return cryptoZombies.methods.getZombiesByOwner(owner).call()
      }

      window.addEventListener('load', function() {

        // Checking if Web3 has been injected by the browser (Mist/MetaMask)
        if (typeof web3 !== 'undefined') {
          // Use Mist/MetaMask's provider
          web3js = new Web3(web3.currentProvider);
        } else {
          // Handle the case where the user doesn't have Metamask installed
          // Probably show them a message prompting them to install Metamask
        }

        // Now you can start your app & access web3 freely:
        startApp()

      })
    </script>
  </body>
</html>

第8章: 調(diào)用 Payable 函數(shù)
attack, changeName, 以及 changeDna 的邏輯將非常雷同,所以本課將不會花時間在上面依沮。

實際上涯贞,在調(diào)用這些函數(shù)的時候已經(jīng)有了非常多的重復邏輯。所以最好是重構(gòu)代碼把相同的代碼寫成一個函數(shù)危喉。(并對txStatus使用模板系統(tǒng)——我們已經(jīng)看到用類似 Vue.js 類的框架是多么整潔)

我們來看看另外一種 Web3.js 中需要特殊對待的函數(shù) — payable 函數(shù)宋渔。

升級!
回憶一下在 ZombieHelper 里面辜限,我們添加了一個 payable 函數(shù)皇拣,用戶可以用來升級:

function levelUp(uint _zombieId) external payable {
  require(msg.value == levelUpFee);
  zombies[_zombieId].level++;
}

和函數(shù)一起發(fā)送以太非常簡單,只有一點需要注意: 我們需要指定發(fā)送多少 wei薄嫡,而不是以太氧急。

啥是 Wei?
一個 wei 是以太的最小單位 — 1 ether 等于 10^18 wei

太多0要數(shù)了,不過幸運的是 Web3.js 有一個轉(zhuǎn)換工具來幫我們做這件事:

// 把 1 ETH 轉(zhuǎn)換成 Wei
web3js.utils.toWei("1", "ether");

在我們的 DApp 里毫深, 我們設置了 levelUpFee = 0.001 ether吩坝,所以調(diào)用 levelUp 方法的時候,我們可以讓用戶用以下的代碼同時發(fā)送 0.001 以太:

cryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001","ether") })

實戰(zhàn)演習
在 feedOnKitty 下面添加一個 levelUp 方法哑蔫。代碼和 feedOnKitty 將非常相似钉寝。不過:

函數(shù)將接收一個參數(shù), zombieId

在發(fā)送事務之前弧呐,txStatus 的文本應該是 "正在升級您的僵尸..."

當它調(diào)用合約里的levelUp時,它應該發(fā)送"0.001" ETH嵌纲,并用 toWei 轉(zhuǎn)換俘枫,如同上面例子里那樣。

成功之后應該顯示 "不得了了疹瘦!僵尸成功升級啦崩哩!"

我們 不 需要在調(diào)用 getZombiesByOwner 后重新繪制界面 — 因為在這里我們只是修改了僵尸的級別而已。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>CryptoZombies front-end</title>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
    <script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
  </head>
  <body>
    <div id="txStatus"></div>
    <div id="zombies"></div>

    <script>
      var cryptoZombies;
      var userAccount;

      function startApp() {
        var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

        var accountInterval = setInterval(function() {
          // Check if account has changed
          if (web3.eth.accounts[0] !== userAccount) {
            userAccount = web3.eth.accounts[0];
            // Call a function to update the UI with the new account
            getZombiesByOwner(userAccount)
            .then(displayZombies);
          }
        }, 100);
      }

      function displayZombies(ids) {
        $("#zombies").empty();
        for (id of ids) {
          // Look up zombie details from our contract. Returns a `zombie` object
          getZombieDetails(id)
          .then(function(zombie) {
            // Using ES6's "template literals" to inject variables into the HTML.
            // Append each one to our #zombies div
            $("#zombies").append(`<div class="zombie">
              <ul>
                <li>Name: ${zombie.name}</li>
                <li>DNA: ${zombie.dna}</li>
                <li>Level: ${zombie.level}</li>
                <li>Wins: ${zombie.winCount}</li>
                <li>Losses: ${zombie.lossCount}</li>
                <li>Ready Time: ${zombie.readyTime}</li>
              </ul>
            </div>`);
          });
        }
      }

      function createRandomZombie(name) {
        // This is going to take a while, so update the UI to let the user know
        // the transaction has been sent
        $("#txStatus").text("Creating new zombie on the blockchain. This may take a while...");
        // Send the tx to our contract:
        return cryptoZombies.methods.createRandomZombie(name)
        .send({ from: userAccount })
        .on("receipt", function(receipt) {
          $("#txStatus").text("Successfully created " + name + "!");
          // Transaction was accepted into the blockchain, let's redraw the UI
          getZombiesByOwner(userAccount).then(displayZombies);
        })
        .on("error", function(error) {
          // Do something to alert the user their transaction has failed
          $("#txStatus").text(error);
        });
      }

      function feedOnKitty(zombieId, kittyId) {
        $("#txStatus").text("Eating a kitty. This may take a while...");
        return cryptoZombies.methods.feedOnKitty(zombieId, kittyId)
        .send({ from: userAccount })
        .on("receipt", function(receipt) {
          $("#txStatus").text("Ate a kitty and spawned a new Zombie!");
          getZombiesByOwner(userAccount).then(displayZombies);
        })
        .on("error", function(error) {
          $("#txStatus").text(error);
        });
      }

      // Start here
      function levelUp(zombieId) {
        $("#txStatus").text("正在升級您的僵尸...");
        return cryptoZombies.methods.levelUp(zombieId)
        .send({ from: userAccount, value: web3js.utils.toWei("0.001", "ether") })
        .on("receipt", function(receipt) {
          $("#txStatus").text("不得了了言沐!僵尸成功升級啦邓嘹!");
        })
        .on("error", function(error) {
          $("#txStatus").text(error);
        });
      }

      function getZombieDetails(id) {
        return cryptoZombies.methods.zombies(id).call()
      }

      function zombieToOwner(id) {
        return cryptoZombies.methods.zombieToOwner(id).call()
      }

      function getZombiesByOwner(owner) {
        return cryptoZombies.methods.getZombiesByOwner(owner).call()
      }

      window.addEventListener('load', function() {

        // Checking if Web3 has been injected by the browser (Mist/MetaMask)
        if (typeof web3 !== 'undefined') {
          // Use Mist/MetaMask's provider
          web3js = new Web3(web3.currentProvider);
        } else {
          // Handle the case where the user doesn't have Metamask installed
          // Probably show them a message prompting them to install Metamask
        }

        // Now you can start your app & access web3 freely:
        startApp()

      })
    </script>
  </body>
</html>

第9章: 訂閱事件
如你所見,通過 Web3.js 和合約交互非常簡單直接——一旦你的環(huán)境建立起來险胰, call 函數(shù)和 send 事務和普通的網(wǎng)絡API并沒有多少不同汹押。

還有一點東西我們想要講到——訂閱合約事件

監(jiān)聽新僵尸事件
如果你還記得 zombiefactory.sol,每次新建一個僵尸后起便,我們會觸發(fā)一個 NewZombie 事件:

event NewZombie(uint zombieId, string name, uint dna);

在 Web3.js里棚贾, 你可以 訂閱 一個事件,這樣你的 Web3 提供者可以在每次事件發(fā)生后觸發(fā)你的一些代碼邏輯:

cryptoZombies.events.NewZombie()
.on("data", function(event) {
  let zombie = event.returnValues;
  console.log("一個新僵尸誕生了榆综!", zombie.zombieId, zombie.name, zombie.dna);
}).on('error', console.error);

注意這段代碼將在 任何 僵尸生成的時候激發(fā)一個警告信息——而不僅僅是當前用用戶的僵尸妙痹。如果我們只想對當前用戶發(fā)出提醒呢?

使用 indexed
為了篩選僅和當前用戶相關的事件鼻疮,我們的 Solidity 合約將必須使用 indexed 關鍵字怯伊,就像我們在 ERC721 實現(xiàn)中的 Transfer 事件中那樣:

event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);

在這種情況下, 因為 _from 和 _to 都是 indexed判沟,這就意味著我們可以在前端事件監(jiān)聽中過濾事件

cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
  let data = event.returnValues;
  // 當前用戶更新了一個僵尸耿芹!更新界面來顯示
}).on('error', console.error);

看到了吧, 使用 event 和 indexed 字段對于監(jiān)聽合約中的更改并將其反映到 DApp 的前端界面中是非常有用的做法挪哄。

查詢過去的事件
我們甚至可以用 getPastEvents 查詢過去的事件吧秕,并用過濾器 fromBlock 和 toBlock 給 Solidity 一個事件日志的時間范圍(“block” 在這里代表以太坊區(qū)塊編號):

cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: 'latest' })
.then(function(events) {
  // events 是可以用來遍歷的 `event` 對象 
  // 這段代碼將返回給我們從開始以來創(chuàng)建的僵尸列表
});

因為你可以用這個方法來查詢從最開始起的事件日志,這就有了一個非常有趣的用例: 用事件來作為一種更便宜的存儲迹炼。

若你還能記得砸彬,在區(qū)塊鏈上保存數(shù)據(jù)是 Solidity 中最貴的操作之一。但是用事件就便宜太多太多了斯入。

這里的短板是拿霉,事件不能從智能合約本身讀取。但是咱扣,如果你有一些數(shù)據(jù)需要永久性地記錄在區(qū)塊鏈中以便可以在應用的前端中讀取绽淘,這將是一個很好的用例。這些數(shù)據(jù)不會影響智能合約向前的狀態(tài)闹伪。

舉個栗子沪铭,我們可以用事件來作為僵尸戰(zhàn)斗的歷史紀錄——我們可以在每次僵尸攻擊別人以及有一方勝出的時候產(chǎn)生一個事件壮池。智能合約不需要這些數(shù)據(jù)來計算任何接下來的事情,但是這對我們在前端向用戶展示來說是非常有用的東西杀怠。

Web3.js 事件 和 MetaMask
上面的示例代碼是針對 Web3.js 最新版1.0的椰憋,此版本使用了 WebSockets 來訂閱事件。

但是赔退,MetaMask 尚且不支持最新的事件 API (盡管如此橙依,他們已經(jīng)在實現(xiàn)這部分功能了, 點擊這里 查看進度)

所以現(xiàn)在我們必須使用一個單獨 Web3 提供者硕旗,它針對事件提供了WebSockets支持窗骑。 我們可以用 Infura 來像實例化第二份拷貝:

var web3Infura = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));
var czEvents = new web3Infura.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

然后我們將使用 czEvents.events.Transfer 來監(jiān)聽事件,而不再使用 cryptoZombies.events.Transfer漆枚。我們將繼續(xù)在課程的其他部分使用 cryptoZombies.methods创译。

將來,在 MetaMask 升級了 API 支持 Web3.js 后墙基,我們就不用這么做了软族。但是現(xiàn)在我們還是要這么做,以使用 Web3.js 更好的最新語法來監(jiān)聽事件残制。

放在一起
來添加一些代碼監(jiān)聽 Transfer 事件立砸,并在當前用戶獲得一個新僵尸的時候為他更新界面。

我們將需要在 startApp 底部添加代碼初茶,以保證在添加事件監(jiān)聽器之前 cryptoZombies 已經(jīng)初始化了颗祝。

在 startApp()底部,為 cryptoZombies.events.Transfer 復制粘貼上面的2行事件監(jiān)聽代碼塊

復制監(jiān)聽 Transfer 事件的代碼塊纺蛆,并用 _to: userAccount 過濾吐葵。要記得把 cryptoZombies 換成 czEvents 好在這 里使用 Infura 而不是 MetaMask 來作為提供者规揪。

用 getZombiesByOwner(userAccount).then(displayZombies); 來更新界面

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>CryptoZombies front-end</title>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
    <script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
  </head>
  <body>
    <div id="txStatus"></div>
    <div id="zombies"></div>

    <script>
      var cryptoZombies;
      var userAccount;

      function startApp() {
        var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

        var accountInterval = setInterval(function() {
          // Check if account has changed
          if (web3.eth.accounts[0] !== userAccount) {
            userAccount = web3.eth.accounts[0];
            // Call a function to update the UI with the new account
            getZombiesByOwner(userAccount)
            .then(displayZombies);
          }
        }, 100);

        // Start here
        var web3Infura = new Web3(new Web3.providers.WebsocketProvider("wss:
        var czEvents = new web3Infura.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
        
        czEvents.events.Transfer({ filter: { _to: userAccount } })
        .on("data", function(event) {
          let data = event.returnValues;
          getZombiesByOwner(userAccount).then(displayZombies);
        }).on('error', console.error);
      }

      function displayZombies(ids) {
        $("#zombies").empty();
        for (id of ids) {
          // Look up zombie details from our contract. Returns a `zombie` object
          getZombieDetails(id)
          .then(function(zombie) {
            // Using ES6's "template literals" to inject variables into the HTML.
            // Append each one to our #zombies div
            $("#zombies").append(`<div class="zombie">
              <ul>
                <li>Name: ${zombie.name}</li>
                <li>DNA: ${zombie.dna}</li>
                <li>Level: ${zombie.level}</li>
                <li>Wins: ${zombie.winCount}</li>
                <li>Losses: ${zombie.lossCount}</li>
                <li>Ready Time: ${zombie.readyTime}</li>
              </ul>
            </div>`);
          });
        }
      }

      function createRandomZombie(name) {
        // This is going to take a while, so update the UI to let the user know
        // the transaction has been sent
        $("#txStatus").text("Creating new zombie on the blockchain. This may take a while...");
        // Send the tx to our contract:
        return cryptoZombies.methods.createRandomZombie(name)
        .send({ from: userAccount })
        .on("receipt", function(receipt) {
          $("#txStatus").text("Successfully created " + name + "!");
          // Transaction was accepted into the blockchain, let's redraw the UI
          getZombiesByOwner(userAccount).then(displayZombies);
        })
        .on("error", function(error) {
          // Do something to alert the user their transaction has failed
          $("#txStatus").text(error);
        });
      }

      function feedOnKitty(zombieId, kittyId) {
        $("#txStatus").text("Eating a kitty. This may take a while...");
        return cryptoZombies.methods.feedOnKitty(zombieId, kittyId)
        .send({ from: userAccount })
        .on("receipt", function(receipt) {
          $("#txStatus").text("Ate a kitty and spawned a new Zombie!");
          getZombiesByOwner(userAccount).then(displayZombies);
        })
        .on("error", function(error) {
          $("#txStatus").text(error);
        });
      }

      function levelUp(zombieId) {
        $("#txStatus").text("Leveling up your zombie...");
        return cryptoZombies.methods.levelUp(zombieId)
        .send({ from: userAccount, value: web3.utils.toWei("0.001", "ether") })
        .on("receipt", function(receipt) {
          $("#txStatus").text("Power overwhelming! Zombie successfully leveled up");
        })
        .on("error", function(error) {
          $("#txStatus").text(error);
        });
      }

      function getZombieDetails(id) {
        return cryptoZombies.methods.zombies(id).call()
      }

      function zombieToOwner(id) {
        return cryptoZombies.methods.zombieToOwner(id).call()
      }

      function getZombiesByOwner(owner) {
        return cryptoZombies.methods.getZombiesByOwner(owner).call()
      }

      window.addEventListener('load', function() {

        // Checking if Web3 has been injected by the browser (Mist/MetaMask)
        if (typeof web3 !== 'undefined') {
          // Use Mist/MetaMask's provider
          web3js = new Web3(web3.currentProvider);
        } else {
          // Handle the case where the user doesn't have Metamask installed
          // Probably show them a message prompting them to install Metamask
        }

        // Now you can start your app & access web3 freely:
        startApp()

      })
    </script>
  </body>
</html>

第10章: 放在一起
恭喜啊少年桥氏,你已經(jīng)成功編寫了一個 Web3.js 前端界面來和你的智能合約交互

接下來的步驟
這節(jié)課的內(nèi)容非常基礎猛铅。我們想要給你展示和智能合約交互的核心內(nèi)容字支,而并不想用太多的時間來教你完整實現(xiàn)。我們也不想花太多時間在HTML/CSS上奸忽,因為大部分人都已經(jīng)知道了堕伪。

所以我們把一些實現(xiàn)略去了。這里是你要完整實現(xiàn)所需要完成的基本事項列表:

為 attack, changeName, changeDna 以及 ERC721 函數(shù) transfer, ownerOf, balanceOf 等實現(xiàn)前端函數(shù)栗菜。這些函數(shù)的實現(xiàn)將和我們講過的 send事務的函數(shù)非常相似欠雌。

實現(xiàn)一個“管理界面”,在那里你可以調(diào)用 setKittyContractAddress, setLevelUpFee, 以及 withdraw疙筹。再次富俄,在前端這塊沒有什么特別的代碼——這些實現(xiàn)之間將非常相似禁炒。你應該保證從部署合同時候相同的以太坊地址調(diào)用這些函數(shù),因為他們都有 onlyOwner 修飾符霍比。

在應用里我們還應該實現(xiàn)一些其他的界面:

a. 一個僵尸頁面幕袱,在那里你可以查看一個特定僵尸的信息并可以分享它的鏈接。這個頁面應該渲染僵尸的外形悠瞬,展示它的名字们豌,它的所有者(以及用戶主頁的鏈接),它的輸贏次數(shù)浅妆,它的戰(zhàn)斗記錄等等望迎。

b. 一個用戶界面,在那里你可以查看用戶的僵尸大軍并分享它的鏈接狂打。

c. 一個主頁擂煞,就是用戶頁面的變體,可以展示當前用戶的僵尸大軍(正如我們在index.html)里面實現(xiàn)的那樣趴乡。

界面中的一些方法允許用戶用 CryptoKitties 喂食僵尸。我們可以給每一個僵尸添加一個按鈕晾捏,叫做“給我投食”蒿涎,再給一個輸入框讓用戶輸入一個貓咪的ID(或者一個貓咪的網(wǎng)址,比如 https://www.cryptokitties.co/kitty/578397)惦辛,它將觸發(fā)我們的 feedOnKitty 函數(shù)劳秋。

界面中的一些方法將讓用戶用來攻擊其他用戶的僵尸

實現(xiàn)這點的一個方法是,當用戶瀏覽其他用戶的頁面的時候胖齐,可以在對方僵尸旁邊顯示一個按鈕玻淑,叫做“攻擊這頭僵尸”。當用戶點擊的時候呀伙,可以彈出一個模塊补履,展示當前用戶的僵尸大軍并詢問用戶“你想用哪頭僵尸出戰(zhàn)?”

在用戶的主頁剿另,也可以在每個僵尸旁邊顯示一個按鈕箫锤,叫做“攻擊一個僵尸”。當用戶點擊的時候雨女,可以彈出一個模塊谚攒,展示一個搜索框,可以讓用戶輸入僵尸ID或者網(wǎng)址來搜索氛堕,或者也可以有一個按鈕叫做“隨機攻擊一頭僵尸”馏臭,將隨機搜索一頭僵尸來。

我們也建議你將在冷卻期的僵尸用特殊的顏色顯示讼稚,比如使其變成灰色括儒。這樣界面就能告訴用戶不能用冷卻期的僵尸來進攻浪耘。

在用戶的主頁,每一個僵尸也應該有選項可以更改名字塑崖、DNA七冲、以及升級(通過付費)。若用戶等級不到规婆,無法使用的選項應該標灰澜躺。

對于新用戶,我們應該顯示一個歡迎信息抒蚜,并讓其確認用 createRandomZombie()創(chuàng)建一個新僵尸掘鄙。

也可以為我們的智能合約添加一個包含 indexed 的用戶地址屬性的 Attack 事件。這樣就可以創(chuàng)建實時通知了——我們可以在用戶的僵尸遭受攻擊的時候彈出一條通知嗡髓,這樣他們可以看到誰在用什么僵尸攻擊他們并做出報復操漠。

我們也許還想實現(xiàn)一些前端緩存層,這樣就不用總是為了相同的數(shù)據(jù)去訪問Infura饿这。(在我們當前實現(xiàn)中浊伙, displayZombies 將在每次頁面刷新的時候為每一個僵尸調(diào)用 getZombieDetails,但是實際中我們將只需要為新加入的僵尸調(diào)用這個函數(shù))

一個實時聊天室长捧,這樣你就可以在你擊潰別人的僵尸大軍的同時嘲諷他們嚣鄙?

因為這將需要大量的前端代碼來實現(xiàn)全部的界面(HTML, CSS串结, JavaScript 以及諸如 React 和 Vue.js 這樣的框架)哑子。光實現(xiàn)一個這樣的前端界面也許會花費多達10節(jié)課,所以我們將這個光榮的任務交給你自己去完成肌割。

注意:盡管智能合約是去中心化的卧蜓。這個用來和DApp交互的前端界面依然需要放在我們中心化的網(wǎng)絡服務器上。不過把敞,有了我們正在內(nèi)測的Loom Network SDK弥奸,很快你就可以在應用自己的DApp鏈上運行前端界面而不是中心化的網(wǎng)絡服務器。這樣在以太坊和 Loom DApp 鏈上先巴,你的整個應用都100%運行在區(qū)塊鏈上了其爵。

總結(jié)
學完第六課冒冬。你現(xiàn)在有了編寫智能合約和前端界面來讓用戶交互的所需技能了伸蚯。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市简烤,隨后出現(xiàn)的幾起案子剂邮,更是在濱河造成了極大的恐慌,老刑警劉巖横侦,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挥萌,死亡現(xiàn)場離奇詭異绰姻,居然都是意外死亡,警方通過查閱死者的電腦和手機引瀑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門狂芋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人憨栽,你說我怎么就攤上這事帜矾。” “怎么了屑柔?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵屡萤,是天一觀的道長。 經(jīng)常有香客問我掸宛,道長死陆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任唧瘾,我火速辦了婚禮措译,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘饰序。我一直安慰自己瞳遍,他們只是感情好,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布菌羽。 她就那樣靜靜地躺著掠械,像睡著了一般。 火紅的嫁衣襯著肌膚如雪注祖。 梳的紋絲不亂的頭發(fā)上猾蒂,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音是晨,去河邊找鬼肚菠。 笑死,一個胖子當著我的面吹牛罩缴,可吹牛的內(nèi)容都是我干的蚊逢。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼箫章,長吁一口氣:“原來是場噩夢啊……” “哼烙荷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起檬寂,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤终抽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體昼伴,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡匾旭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了圃郊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片价涝。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖持舆,靈堂內(nèi)的尸體忽然破棺而出飒泻,到底是詐尸還是另有隱情,我是刑警寧澤吏廉,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布泞遗,位于F島的核電站,受9級特大地震影響席覆,放射性物質(zhì)發(fā)生泄漏史辙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一佩伤、第九天 我趴在偏房一處隱蔽的房頂上張望蕊温。 院中可真熱鬧矿咕,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽隅居。三九已至峡碉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間盐股,已是汗流浹背钱豁。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留疯汁,地道東北人牲尺。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像幌蚊,于是被迫代替她去往敵國和親谤碳。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容