事先說明,由於全部 dapp框架都會隱藏掉一些底層細節,對初學者來講,貿然使用框架可能會造成對系統認識上的障礙,因此本文不會介紹如何藉助框架搭建 dapp。這樣等未來須要甄選框架時,你也能清楚地看到框架到底幫你作了什麼。css
若是以前沒接觸過以太坊 dapp開發,建議先閱讀那篇《給 Web開發人員的以太坊入坑指南》。html
該交代的都交代了,接下來是咱們要講的乾貨:node
準備開發環境學習在開發環境中的合約編寫、編譯和部署流程jquery
經過 node.js控制檯與區塊鏈上的合約交互linux
經過一個簡單的網頁與合約交互,在頁面上提供投票功能並顯示候選項及相應的票數。git
整個程序的開發都是在一臺乾淨的 ubuntu 16.04 xenial上完成的。除此以外,我還在一臺 macos上重複了一遍搭建和測試過程。web
下面是咱們這個程序的架構圖:macos
按 web開發的說法,真實區塊鏈(live blockchain)至關於生產環境,咱們天然不該該在生產環境上作開發,所以本文用了一個名爲 ganache的內存區塊鏈(至關於區塊鏈模擬器)。本教程的第二篇文章纔會跟真正的區塊鏈交互。npm
下面是在 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