如何在以太坊上搭建一個Dapp?


原創: 前哨小兵甲 區塊鏈前哨 昨天

策划|Tina作者|Mahesh Murthy俗話說,實踐出真知!對於開發人員來說,最好的學習辦法就是親自動手做一個小項目。所以,接下來我們將會以一個投票程序為例,帶着你在以太坊平台上搭建一個 dapp。

這個程序的功能很簡單,只是設定一組候選項,讓所有人都可以給這些候選項投票,以及顯示每個候選項收到的總票數。當然,我們的目的並不是要開發一個投票程序,而是想借助這樣一個例子介紹 Dapp的編譯、部署及交互過程。

更多干貨內容請關注微信公眾號“區塊鏈前哨”,(ID:blockchain-666)

事先說明,因為所有 dapp框架都會隱藏掉一些底層細節,對初學者來說,貿然使用框架可能會形成對系統認識上的障礙,所以本文不會介紹如何借助框架搭建 dapp。這樣等將來需要甄選框架時,你也能清楚地看到框架到底幫你做了什么。

如果之前沒接觸過以太坊 dapp開發,建議先閱讀那篇《給 Web開發人員的以太坊入坑指南》。

該交代的都交代了,接下來是我們要講的干貨:

准備開發環境

學習在開發環境中的合約編寫、編譯和部署流程

通過 node.js控制台與區塊鏈上的合約交互

通過一個簡單的網頁與合約交互,在頁面上提供投票功能並顯示候選項及相應的票數。

整個程序的開發都是在一台干凈的 ubuntu 16.04 xenial上完成的。除此之外,我還在一台 macos上重復了一遍搭建和測試過程。

下面是我們這個程序的架構圖:

准備開發環境

按 web開發的說法,真實區塊鏈(live blockchain)相當於生產環境,我們自然不應該在生產環境上做開發,因此本文用了一個名為 ganache的內存區塊鏈(相當於區塊鏈模擬器)。本教程的第二篇文章才會跟真正的區塊鏈交互。

下面是在 linux操作系統上安裝 ganache和 web3js,以及啟動測試區塊鏈的步驟。在 macos上可以用同樣的命令。windows系統可以參照這里的命令(感謝 Prateesh!)。

注意:ganache-cli會創建 10個自動參與交易的測試賬號,每個賬號里都預存了 100個以太幣(當然,只能用於測試)。

簡單的投票合約

接下來我們要用 Solidity編程語言編寫合約。如果你熟悉面向對象編程,就會覺得這個學起來很輕松。

我們要編寫一個名為 Voting的合約(相當於 OOP語言中的類)。這個合約中會有個構造器,負責初始化一個包含候選項的數組;還會有兩個方法,一個用於返回指定候選項的總票數,另一個給候選項的得票數加一。

注意:在將合約部署到區塊鏈上時,構造器會執行,並且只會執行這一次。在做 web應用時,每次重新部署都會覆蓋掉原來的代碼,但部署到區塊鏈上的代碼是不可變的。也就是說,即便你更新了合約,又重新部署了一次,之前的合約仍然會原封不動地留在區塊鏈上,並且其中存儲的數據也不會受到絲毫影響,新部署的代碼會創建一個全新的合約實例。

下面是帶有注釋的投票合約代碼:

pragma solidity ^0.4.18;
// 必須指明編譯這段代碼的編譯器版本
contract Voting {
  /* 下面這個 mapping域相當於一個關聯數組或哈希。
      mapping的鍵是候選項的名字,類型為 bytes32;
      值的類型是無符號整型,用於存儲得票數。
  */
  mapping (bytes32 => uint8) public votesReceived;
  /* Solidity(還)不允許給構造器傳入字符串數組。
  所以我們用 bytes32數組存儲候選項
  */
  bytes32[] public candidateList;
  /* 這就是把合約部署到區塊鏈上時會執行一次的構造器。
  在部署合約時,我們會傳入一個包含候選項的數組。
  */
  function Voting(bytes32[] candidateNames) public {
    candidateList = candidateNames;
  }
  // 這個函數用於返回指定候選項的總票數,其參數即為指定候選項
  function totalVotesFor(bytes32 candidate) view public returns (uint8) {
    require(validCandidate(candidate));
    return votesReceived[candidate];
  }
  // 這個函數用於將指定候選項的票數加一
  // 這相當於實現了投票功能
  function voteForCandidate(bytes32 candidate) public {
    require(validCandidate(candidate));
    votesReceived[candidate] += 1;
  }
  function validCandidate(bytes32 candidate) view public returns (bool) {
    for(uint i = 0; i < candidateList.length; i++) {
      if (candidateList[i] == candidate) {
        return true;
      }
    }
    return false;
  }
}

將上面的代碼保存到 Voting.sol文件中,放在 hello_world_voting目錄下。接下來我們要編譯這段代碼,並將它部署到 ganache區塊鏈上。

在編譯 Solidity代碼之前,需要先安裝 npm模塊 solc。

mahesh@projectblockchain:~/hello_world_voting$ npm install solc

我們會在 node控制台中用這個庫編譯合約。在上一篇文章中,我們說過 web3js庫提供了通過 RPC跟區塊鏈交互的功能。應用的部署和交互都是通過這個庫完成的。

首先,在終端中運行 node命令進入 node控制台,初始化 solc和 web3對象。下面是需要在 node控制台中輸入的代碼:

mahesh@projectblockchain:~/hello_world_voting$ node
> Web3 = require('web3')
> web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));

為了確保 web3對象初始化成功,可以跟區塊鏈通訊,我們可以查詢一下區塊鏈上的所有賬號。查詢結果應該如下所示:

> web3.eth.accounts
['0x9c02f5c68e02390a3ab81f63341edc1ba5dbb39e',
'0x7d920be073e92a590dc47e4ccea2f28db3f218cc',
'0xf8a9c7c65c4d1c0c21b06c06ee5da80bd8f074a9',
'0x9d8ee8c3d4f8b1e08803da274bdaff80c2204fc6',
'0x26bb5d139aa7bdb1380af0e1e8f98147ef4c406a',
'0x622e557aad13c36459fac83240f25ae91882127c',
'0xbf8b1630d5640e272f33653e83092ce33d302fd2',
'0xe37a3157cb3081ea7a96ba9f9e942c72cf7ad87b',
'0x175dae81345f36775db285d368f0b1d49f61b2f8',
'0xc26bda5f3370bdd46e7c84bdb909aead4d8f35f3']

為了編譯合約,需要先加載文件 Voting.sol中的代碼,並將其賦值給一個字符串變量,然后再編譯這個字符串。

> code = fs.readFileSync('Voting.sol').toString()
> solc = require('solc')
> compiledCode = solc.compile(code)

代碼編譯成功后,可以在 node終端中輸入 compiledCode命令查看 contract對象,有兩個域非常重要,一定要搞明白:

compiledCode.contracts[‘:Voting’].bytecode: 這是 Voting.sol中的代碼編譯而成的字節碼,也是要部署到區塊鏈上的代碼。

compiledCode.contracts[‘:Voting’].interface: 這是合約的接口或者說模板(稱為 abi),告訴合約的用戶有哪些方法可用。將來不管什么時候要跟合約交互,都需要這個 abi定義。這里有關於 ABI的詳細介紹。

接下來部署合約。先創建一個在區塊鏈中部署和初始化合約的合約對象(即下面的 VotingContract)。

> abiDefinition = JSON.parse(compiledCode.contracts[':Voting'].interface)
> VotingContract = web3.eth.contract(abiDefinition)
> byteCode = compiledCode.contracts[':Voting'].bytecode
> deployedContract = VotingContract.new(['Rama','Nick','Jose'],{data: byteCode, from: web3.eth.accounts[0], gas: 4700000})
> deployedContract.address
> contractInstance = VotingContract.at(deployedContract.address)

上面代碼中的 VotingContract.new 將合約部署到區塊鏈上。它的第一個參數是包含候選項的數組,一看就能明白。第二個參數中各數據項的含義分別為:

data: 這是已編譯好要部署到區塊鏈上的字節碼。

from: 區塊鏈必須追蹤是誰部署的合約。在這個例子中,我們只是調用了 web3.eth.accounts,然后將返回結果的第一個賬號作為這個合約的所有者(即將合約部署到區塊鏈上的賬號)。

記住,web3.eth.accounts返回的是 ganche在啟動測試區塊鏈時創建的 10個測試賬號組成的數組。然而在真實的區塊鏈中,不能隨便指定一個賬號。那必須是你擁有的賬號,並且在交易之前要解鎖那個賬號。在創建賬號時,系統會要求你提供一個口令,這個口令就是用來證明你對賬號的所有權的。為了用起來方便,Ganache默認把 10個賬號全解鎖了。

gas: 跟區塊鏈交互是要花錢的。為了把你的代碼放到區塊鏈上,是需要讓礦機干活的,這筆錢就是給那些付出計算力的礦機的。你必須明確願意為此支付多少錢,即給‘gas’一個值。購買燃料的以太幣是從你的 from賬號中出的。燃料的價格是由網絡設定的。合約部署好之后,我們就可以跟合約的實例(即上面的變量 contractInstance)交互了。區塊鏈上有成百上千個合約,怎么確定哪個是你的呢?答案是用 deployedContract.address。在你必須跟合約交互時,需要這個部署地址和之前說過的那個 abi定義。

在 nodejs控制台中與合約交互
> contractInstance.totalVotesFor.call('Rama')
{ [String: '0'] s: 1, e: 0, c: [ 0 ] }
> contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]})
'0xdedc7ae544c3dde74ab5a0b07422c5a51b5240603d31074f5b75c0ebc786bf53'
> contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]})
'0x02c054d238038d68b65d55770fabfca592a5cf6590229ab91bbe7cd72da46de9'
> contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]})
'0x3da069a09577514f2baaa11bc3015a16edf26aad28dffbcd126bde2e71f2b76f'
> contractInstance.totalVotesFor.call('Rama').toLocaleString()
'3'

在 node控制台中運行上面的命令,應該可以看到票數的增長。每次投票給候選項,都會得到一個交易 id,比如上面的‘0xdedc7ae544c3dde74ab5a0b07422c5a51b5240603d31074f5b75c0ebc786bf53’。這個 id是交易已經發生的證據,將來隨時可以用這個 id訪問這筆交易。交易是不可變的,而不可變性正是以太坊這樣的區塊鏈的一個顯著優點。后續教程將會介紹如何利用這一優點。

4.連接區塊鏈並且可以投票的網頁

現在基本上算是完工了,只剩下一件事情。接下來我們要創建一個簡單的 html文件,讓它顯示候選項的名稱、票數,還有投票控件,以便調用放在 js文件中的投票命令(剛才在 node控制台上已經測試過了)。下面是 html文件和 js文件中的代碼。把它們存到相應的文件中,放在 hello_world_voting目錄下,然后在瀏覽器中打開 index.html。

index.html文件中的代碼

<!DOCTYPE html>
<html>
<head>
  <title>Hello World DApp</title>
  <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700' rel='stylesheet' type='text/css'>
  <link href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css' rel='stylesheet' type='text/css'>
</head>
<body class="container">
  <h1>A Simple Hello World Voting Application</h1>
  <div class="table-responsive">
    <table class="table table-bordered">
      <thead>
        <tr>
          <th>Candidate</th>
          <th>Votes</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>Rama</td>
          <td id="candidate-1"></td>
        </tr>
        <tr>
          <td>Nick</td>
          <td id="candidate-2"></td>
        </tr>
        <tr>
          <td>Jose</td>
          <td id="candidate-3"></td>
        </tr>
      </tbody>
    </table>
  </div>
  <input type="text" id="candidate" />
  <a href="#" onclick="voteForCandidate()" class="btn btn-primary">Vote</a>
</body>
<script src="https://cdn.rawgit.com/ethereum/web3.js/develop/dist/web3.js"></script>
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js"></script>
<script src="./index.js"></script>
</html>

index.js文件中的代碼

web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
abi = JSON.parse('[{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"totalVotesFor","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"validCandidate","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"votesReceived","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"x","type":"bytes32"}],"name":"bytes32ToString","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"candidateList","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"voteForCandidate","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"contractOwner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"inputs":[{"name":"candidateNames","type":"bytes32[]"}],"payable":false,"type":"constructor"}]')
VotingContract = web3.eth.contract(abi);
// 在你的 node控制台中執行 contractInstance.address以獲取合約的部署地址,並將下面的地址換成你自己的部署地址
contractInstance = VotingContract.at('0x2a9c1d265d06d47e8f7b00ffa987c9185aecf672');
candidates = {"Rama": "candidate-1", "Nick": "candidate-2", "Jose": "candidate-3"}
function voteForCandidate() {
  candidateName = $("#candidate").val();
  contractInstance.voteForCandidate(candidateName, {from: web3.eth.accounts[0]}, function() {
    let div_id = candidates[candidateName];
    $("#" + div_id).html(contractInstance.totalVotesFor.call(candidateName).toString());
  });
}
$(document).ready(function() {
  candidateNames = Object.keys(candidates);
  for (var i = 0; i < candidateNames.length; i++) {
    let name = candidateNames[i];
    let val = contractInstance.totalVotesFor.call(name).toString()
    $("#" + candidates[name]).html(val);
  }
});

我們之前說過,跟合約交互需要 abi和地址。上面的 index.js中有使用它們跟合約交互的代碼。

這是在瀏覽器中打開 index.html之后的頁面:

如果在文本框中輸入候選項的名稱,點擊投票按鈕后能見到票數的增長,說明你已經成功地創建了自己的第一個 dapp!恭喜!

我們簡單回顧一下整個過程:搭建開發環境;編譯合約,部署到區塊鏈上;在 node控制台中跟合約交互;通過網頁跟合約交互。現在你可以讓自己放松一下了:)

以后,我們將會介紹如何將這個合約部署到公共測試網絡中,讓所有人都能看到它,能給你的候選項投票。我們還會做些復雜的事情,介紹如何使用 truffle框架完成開發任務(不再需要用 node控制台管理整個過程)。希望看完這篇文章后,你已經知道如何動手在以太坊平台上開發去中心化應用了。

翻譯:海興。文章來源:

https://medium.com/@mvmurthy/full-stack-hello-world-voting-ethereum-dapp-tutorial-part-1-40d2d0d807c2

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM