本文內(nèi)容來(lái)自熊麗兵老師在登鏈學(xué)院上的《以太坊Dapp開(kāi)發(fā)實(shí)戰(zhàn)》課程
簡(jiǎn)介
Dapp VS App
去中心化的應(yīng)用的后端是一個(gè)分布式的網(wǎng)絡(luò)(中心化應(yīng)用后端是一個(gè)中心化的節(jié)點(diǎn))劲阎。前端連接到網(wǎng)絡(luò)中的任意一個(gè)節(jié)點(diǎn)都一樣匠题。
去中心化應(yīng)用給節(jié)點(diǎn)發(fā)送的請(qǐng)求叫交易颜懊,節(jié)點(diǎn)不單自己處理請(qǐng)求采够,還把請(qǐng)求轉(zhuǎn)發(fā)到網(wǎng)絡(luò)的其他節(jié)點(diǎn)黄虱,請(qǐng)求的最終執(zhí)行需要網(wǎng)絡(luò)的共識(shí)旷坦,所以應(yīng)用不能直接拿到結(jié)果袭艺,是異步的搀崭,前端想要知道狀態(tài)的改變需要監(jiān)聽(tīng)事件的方法。
第一個(gè)去中心化應(yīng)用
本應(yīng)用使用Truffle來(lái)開(kāi)發(fā)Dapp。功能與 第一個(gè)去中心化應(yīng)用 一樣瘤睹。
應(yīng)用效果圖如下
應(yīng)用的功能很簡(jiǎn)單
淺藍(lán)色部分用來(lái)顯示區(qū)塊鏈上的姓名和年齡信息
淺綠色部分用來(lái)修改區(qū)塊鏈上的姓名和年齡信息
準(zhǔn)備
全局安裝truffle框架
sudo npm install -g truffle //安裝truffle升敲。-g代表全局
Truffle是DApps開(kāi)發(fā)用到的一個(gè)最流行的開(kāi)發(fā)框架。本身基于Javascript轰传。
Truffle也可以理解為是一個(gè)腳手架:集成了合約的編譯驴党、鏈接、測(cè)試获茬、部署
Truffle官網(wǎng):https://truffleframework.com/truffle
Truffle文檔:https://truffleframework.com/docs/truffle
Truffle命令:https://truffleframework.com/docs/truffle/reference/truffle-commands
Truffle里有個(gè)box的概念港庄,相當(dāng)于把一些代碼框架給打包好了,想用哪個(gè)box直接下載就可以使用恕曲,在此基礎(chǔ)上編寫自己的Dapp鹏氧。比如有一個(gè)react的box,整合了react的代碼佩谣,不用專門下載react了把还,非常方便。
Truffle box: https://github.com/truffle-box
本文為了梳理開(kāi)發(fā)流程茸俭,沒(méi)有使用box吊履。
全局安裝ganache節(jié)點(diǎn)
sudo npm install -g ganache-cli //安裝ganache-cli節(jié)點(diǎn)
ganache-cli //啟動(dòng)ganache-cli節(jié)點(diǎn),后續(xù)會(huì)用到调鬓⊥а祝可單獨(dú)開(kāi)一個(gè)終端
以太坊節(jié)點(diǎn)也叫以太坊客戶端。智能合約必須部署到以太坊節(jié)點(diǎn)上來(lái)運(yùn)行袖迎。
以太坊節(jié)點(diǎn)有Ganache虛擬節(jié)點(diǎn)冕臭、Geth節(jié)點(diǎn)、以太坊測(cè)試節(jié)點(diǎn)(Ropsten燕锥、kovan辜贵、Rinkeby)、以太坊主網(wǎng)(Main Ethereum Network)
開(kāi)發(fā)過(guò)程中需要不斷的修改代碼和測(cè)試归形,需要得到及時(shí)的反饋托慨。Truffle官方推薦使用適合開(kāi)發(fā)的客戶端Ganache(前身為EtherumJS TestRPC)
。
Ganache是一個(gè)完整的在內(nèi)存中的區(qū)塊鏈(因?yàn)樵趦?nèi)存中暇榴,所以重啟后數(shù)據(jù)會(huì)丟失)厚棵,僅僅存在于你開(kāi)發(fā)的設(shè)備上。它在執(zhí)行交易時(shí)是實(shí)時(shí)返回蔼紧,而不等待默認(rèn)的出塊時(shí)間婆硬,可以快速驗(yàn)證新寫的代碼,當(dāng)出現(xiàn)錯(cuò)誤時(shí)能夠即時(shí)反饋奸例。它同時(shí)還是一個(gè)支持自動(dòng)化測(cè)試的功能強(qiáng)大的客戶端彬犯。Truffle充分利用它的特性向楼,能將測(cè)試運(yùn)行時(shí)間提速近90%。
ganache有命令行版和界面版兩個(gè)版本谐区,使用哪個(gè)都可以湖蜕。本文使用的是命令行版
ganache命令行版:https://github.com/trufflesuite/ganache-cli/tree/master
ganache界面版:https://github.com/trufflesuite/ganache/releases
安裝文檔:https://truffleframework.com/docs/ganache/quickstart
節(jié)點(diǎn)選擇
開(kāi)發(fā) | 部署 | 正式部署前測(cè)試 | 正式部署 |
---|---|---|---|
ganache | Geth | 以太坊測(cè)試節(jié)點(diǎn) | 以太坊主網(wǎng) |
MetaMask連接ganache節(jié)點(diǎn)
MetaMask是一款瀏覽器插件錢包,不需下載安裝客戶端宋列,只需添加至瀏覽器擴(kuò)展程序即可使用昭抒。它不僅是一個(gè)簡(jiǎn)單的錢包,還可以很方便的調(diào)試和測(cè)試以太坊的智能合約炼杖。
官網(wǎng):https://metamask.io/
Github地址:https://github.com/MetaMask/metamask-extension
文檔:https://metamask.github.io/metamask-docs/
使用教程參考:http://www.reibang.com/p/4a28566c425d
將MetaMask連接ganache節(jié)點(diǎn)并導(dǎo)入一個(gè)ganache自動(dòng)創(chuàng)建的賬戶灭返。用此賬戶部署合約以及發(fā)送交易。
連接節(jié)點(diǎn)
MetaMask的network選擇ganache-cli的節(jié)點(diǎn)url(localhost 8545)嘹叫,連接到ganache-cli節(jié)點(diǎn)
MetaMask導(dǎo)入賬戶
在MetaMask的import Account里導(dǎo)入Private Key婆殿。用此賬戶部署合約以及發(fā)送交易诈乒。
ganache-cli開(kāi)啟時(shí)會(huì)默認(rèn)創(chuàng)建十個(gè)賬戶罩扇,每個(gè)賬戶都有一個(gè)Private Key
創(chuàng)建項(xiàng)目
mkdir Dapp //創(chuàng)建項(xiàng)目目錄
cd Dapp
truffle init //truffle創(chuàng)建項(xiàng)目,得到項(xiàng)目框架(當(dāng)然也可以用truffle提供的box來(lái)創(chuàng)建項(xiàng)目怕磨,在box的基礎(chǔ)上修改)
npm init //將項(xiàng)目轉(zhuǎn)換為npm項(xiàng)目(提示時(shí)都使用默認(rèn)值即可)
本文采用truffle init創(chuàng)建空項(xiàng)目喂饥,當(dāng)然還可以使用truffle的box創(chuàng)建帶有其他框架的項(xiàng)目。
npm init是將項(xiàng)目轉(zhuǎn)換為一個(gè)npm的項(xiàng)目肠鲫,方便管理员帮。
項(xiàng)目根目錄下安裝lite-server服務(wù)器
npm install lite-server //安裝項(xiàng)目服務(wù)器
應(yīng)用需要一個(gè)web服務(wù)器,而像nginx导饲、apache等web服務(wù)器需要單獨(dú)安裝,在項(xiàng)目外。而lite-server服務(wù)器本身就是項(xiàng)目的一部分荣挨,環(huán)境容易統(tǒng)一飞傀,方便開(kāi)發(fā)和維護(hù)。
項(xiàng)目根目錄下安裝truffle-contract
npm install truffle-contract //安裝truffle框架提供的contract袋毙,對(duì)web3進(jìn)行了封裝型檀,方便與合約進(jìn)行交互
truffle-contract是web3的一個(gè)封裝,在應(yīng)用與智能合約交互時(shí)用听盖。
truffle-contract文檔:https://truffleframework.com/docs/truffle/getting-started/interacting-with-your-contracts
web3中文文檔:https://web3.learnblockchain.cn/
初始框架結(jié)構(gòu)
|-- Dapp
|-- contracts //Solidity合約代碼
-- Migrations.sol //此文件為必須
|-- migrations //合約部署腳本
-- 1_initial_migration.js //用來(lái)部署Migrations.sol的
|-- test //測(cè)試代碼
|-- node_modules //npm模塊胀溺,lite-server和truffle-contract在里面
-- truffle-config.js //windows下的truffle配置文件
-- truffle.js //linux、mac下的truffle配置文件
-- package.json //npm init后的配置文件
后端(智能合約)
開(kāi)發(fā)合約
Solidity文檔:https://solidity.readthedocs.io/en/v0.5.1/
直接將項(xiàng)目導(dǎo)入自己喜歡的編輯器皆看,在contracts目錄下新建 Simple.sol 文件仓坞,或者終端中使用命令來(lái)創(chuàng)建Simple.sol文件
truffle create contract Simple
結(jié)構(gòu)如下
|-- contracts
-- Migrations.sol
-- Simple.sol
內(nèi)容如下
pragma solidity ^0.4.24;
contract Simple{
string name;
uint age;
//定義事件
event Instructor(string name, uint age);
function set(string _name, uint _age) public {
name = _name;
age = _age;
//觸發(fā)事件
emit Instructor(name, age);
}
function get() public view returns(string, uint) {
return (name, age);
}
}
編譯合約
truffle compile
編譯完成后,根目錄下會(huì)生成一個(gè)build目錄腰吟,其下的contracts目錄下是合約的json文件
|-- build
|-- contracts
-- Migrations.json
-- Simple.json //包含abi无埃、地址等信息
對(duì)合約部署的時(shí)候,truffle會(huì)更新此文件,寫入truffle.js里的網(wǎng)絡(luò)網(wǎng)絡(luò)信息
測(cè)試合約
測(cè)試合約有solidity和js兩種方式
solidity方式文檔:https://truffleframework.com/docs/truffle/testing/writing-tests-in-solidity
js方式文檔:https://truffleframework.com/docs/truffle/testing/writing-tests-in-javascript
test目錄下創(chuàng)建測(cè)試文件
|-- Dapp
|-- test
-- TestSimple.sol
-- Simple.js
TestSimple.sol
pragma solidity ^0.4.24;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Simple.sol";
contract TestSimple {
Simple info = Simple(DeployedAddresses.Simple());
string name;
uint age;
function testInfo() public {
info.set("中本聰",18);
(name, age) = info.get();
Assert.equal(name, "中本聰", "設(shè)置姓名出錯(cuò)");
Assert.equal(age, 18, "設(shè)置年齡出錯(cuò)");
}
}
Simple.js
var Simple = artifacts.require("Simple");
contract('Simple', function(accounts){
it("set() should be equal set", function(){
return Simple.deployed().then(function(instance){
instance.set("中本聰", 18);
return instance.get().then(function(data){
assert.equal(data[0], "中本聰");
assert.equal(data[1], 18);
});
});
});
});
執(zhí)行測(cè)試命令
truffle test //執(zhí)行所有測(cè)試文件
truffle test ./test/TestSimple.sol //指定測(cè)試哪個(gè)文件
測(cè)試需要用到網(wǎng)絡(luò)录语,配置部分參考部署合約時(shí)的truffle.js的網(wǎng)絡(luò)配置
結(jié)果如下倍啥,說(shuō)明測(cè)試通過(guò)
TestSimple
? testInfo (90ms)
Contract: Simple
? set() should be equal set (46ms)
2 passing (865ms)
部署合約
配置truffle.js文件
可參考truffle官網(wǎng)配置部分:http://truffleframework.com/docs/advanced/configuration
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 8545,
network_id: "*" //watch any network id
}
}
};
創(chuàng)建并配置部署的腳本
直接在contracts目錄下新建 Simple.js 文件,或者終端中使用命令來(lái)創(chuàng)建Simple.js文件
truffle create migration simple
在migrations目錄下就會(huì)創(chuàng)建一個(gè)部署腳本
|-- migrations
-- 1_initial_migration.js
-- 1544177454_simple.js //命令創(chuàng)建的澎埠。前面數(shù)字應(yīng)該是隨機(jī)的
配置腳本
參考:https://truffleframework.com/docs/truffle/getting-started/running-migrations
var Simple = artifacts.require("./Simple.sol");
module.exports = function(deployer) {
// deployment steps
deployer.deploy(Simple);
};
部署
truffle migrate
truffle migrate --reset //如果修改了合約重新部署虽缕,需要加reset參數(shù)
運(yùn)行上面指令發(fā)生的事情
運(yùn)行了1_initial_migration.js、2_deploy_contracts.js文件蒲稳,ganache客戶端中生成了transaction信息氮趋。
contracts目錄里的Migrations.json文件里增加了networks內(nèi)容,最主要的是一個(gè)address信息江耀。
前端
|-- Dapp
|-- src //項(xiàng)目根目錄下創(chuàng)建src目錄剩胁,存放前端文件
-- index.html
-- index.css
|-- js
-- index.js
-- truffle-contract.min.js //從node_modules/truffle-contract/dist/truffle-contract.min.js復(fù)制過(guò)來(lái)的
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>第一個(gè)去中心化應(yīng)用</title>
<link rel="stylesheet" href="./index.css">
</head>
<body>
<div id="content">
<h1 id="title">第一個(gè)去中心化應(yīng)用</h1>
<h2 id="info"></h2>
<div id="loader"></div>
<div id="update">
<div>
<label for="name">姓名:</label>
<input type="text" id="name">
</div>
<div>
<label for="age">年齡:</label>
<input type="text" id="age">
</div>
<button id="button">更新</button>
</div>
</div>
</body>
<!-- 引入jquery、web3.js祥国、truffle-contract.min.js昵观、index.js -->
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<!-- 這里的web3.js為0.2版本。1.0版本目前還是beta階段舌稀,等穩(wěn)定了可更換為1.0版本 -->
<script src="https://cdn.jsdelivr.net/gh/ethereum/web3.js/dist/web3.min.js"></script>
<script src="./js/truffle-contract.min.js"></script>
<script src="./js/index.js"></script>
</html>
index.css
body{background:#f0f0f0;}
#content{width:350px;margin:0 auto;}
#title{text-align: center;}
#info{padding:0.5em;background: lightblue;margin:1em 0;}
#update{padding:1em;background: lightgreen}
#name{border:none;}#age{border:none;}
button{margin:1em 0;}
index.js
這部分是應(yīng)用與智能合約交互的部分啊犬,跟智能合約一樣,是Dapp的關(guān)鍵部分
在第一個(gè)去中心化應(yīng)用中壁查,獲取智能合約對(duì)象是通過(guò)在js代碼里寫入合約abi和合約地址的硬編碼方式觉至,每次變更合約都要重新修改為新的合約abi和合約地址,非常繁瑣睡腿,不容易維護(hù)语御。
truffle提供了一個(gè)truffle-contract的抽象,對(duì)web3進(jìn)行了封裝席怪,并且引入了promise的用法应闯,方便與合約進(jìn)行交互。合約編譯的時(shí)候會(huì)在build/contracts目錄下生成合約對(duì)應(yīng)的json文件(重新編譯部署會(huì)自動(dòng)更新json文件)何恶,truffle-contract會(huì)利用這些信息生成合約對(duì)象孽锥,避免了硬編碼方式。
兩者對(duì)比
web3硬編碼方式
var infoContract = web3.eth.contract(合約ABI);
var Simple = infoContract.at('合約地址');
truffle-contract方式
TruffleContract('合約的json文件').deployed()
.then(function(instance) {
});
完整的js文件
App = {
web3Provider: null,
contracts: {},
//初始化
init: function(){
return App.initweb3();
},
//初始化web3
initweb3: function(){
//獲取web3對(duì)象
if (typeof web3 !== 'undefined') {
App.web3Provider = web3.currentProvider;
web3 = new Web3(App.web3Provider);
} else {
// Set the provider you want from Web3.providers
App.web3Provider = new Web3.providers.HttpProvider("http://localhost:8545");
web3 = new Web3(App.web3Provider);
}
return App.initContract();
},
//初始化合約
initContract: function(){
//拿到Simple.json的內(nèi)容细层,回調(diào)函數(shù)的參數(shù)data即為拿到的內(nèi)容
$.getJSON("Simple.json",function(data){
//得到TruffleContract對(duì)象惜辑,并賦值給App.contracts.Simple
App.contracts.Simple = TruffleContract(data);
//設(shè)置Provider
App.contracts.Simple.setProvider(App.web3Provider);
//調(diào)用合約的get方法
App.get();
//事件監(jiān)聽(tīng),更新顯示的內(nèi)容
App.watchChanged();
});
//調(diào)用事件
App.bindEvents();
},
//合約的get方法
get: function(){
//deployed得到合約的實(shí)例疫赎,通過(guò)then的方式回調(diào)拿到實(shí)例
App.contracts.Simple.deployed().then(function(instance){
return instance.get.call();
}).then(function(result){ //異步執(zhí)行盛撑,get方法執(zhí)行完后回調(diào)執(zhí)行then方法,result為get方法的返回值
$("#loader").hide();
$("#info").html(`我叫` + result[0] + `捧搞, 我今年` + result[1] + `歲了抵卫。`);
}).catch(function(err){ //get方法執(zhí)行失敗打印錯(cuò)誤
console.log(err);
})
},
//事件狮荔,更新操作
bindEvents: function(){
$("#button").click(function(){
$("#loader").show();
App.contracts.Simple.deployed().then(function(instance){
return instance.set.sendTransaction($("#name").val(),$("#age").val());
}).then(function(result){
App.get(); //set方法執(zhí)行完后調(diào)用get方法,獲取最新值(可沒(méi)有介粘,通常使用事件監(jiān)聽(tīng)的方式)
}).catch(function(err){
console.log(err);
});
});
},
//事件監(jiān)聽(tīng)
watchChanged: function(){
App.contracts.Simple.deployed().then(function(instance){
var infoEvent = instance.Instructor();
infoEvent.watch(function(err, result){
$("#loader").hide();
$("#info").html(`我叫` + result.args.name + `殖氏, 我今年` + result.args.age + `歲了。`);
})
});
}
}
//加載應(yīng)用
$(function(){
$(window).load(function(){
App.init();
});
});
開(kāi)啟服務(wù)
原始方式如下
根目錄下輸入
node_modules/lite-server/bin/lite-server
彈出來(lái)的頁(yè)面中定位到index.html文件姻采,就可以看到應(yīng)用了
localhost:3000/src/index.html
這種方式 index.js里
$.getJSON
第一個(gè)參數(shù)路徑為../build/contracts/Simple.json
雅采。上面index.js文件是配置bs-config.json后的寫法,如果使用此方式需要修改路徑慨亲。
但這種方式顯然是太麻煩了婚瓜。可以通過(guò)配置lite-server的baseDir來(lái)自動(dòng)定位刑棵,通過(guò)配置服務(wù)腳本的方式來(lái)簡(jiǎn)化輸入命令
配置bs-config.json
bs-config.json
或bs-config.js
為lite-server的配置文件
在項(xiàng)目根目錄下創(chuàng)建bs-config.json
巴刻,然后寫入配置
{
"server":
{"baseDir": ["./src", "./build/contracts"] }
}
配置./src
,直接localhost:3000 即可打開(kāi)index.html
配置./build/contracts
蛉签,index.js里$.getJSON
第一個(gè)參數(shù)路徑就可以省略胡陪,直接寫Simple.json即可
修改package.json,加入腳本
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "lite-server" //新加入的腳本
},
在項(xiàng)目根目錄下直接輸入
npm run dev
即可打開(kāi)項(xiàng)目(自動(dòng)定位到src下)
交互
與應(yīng)用交互
應(yīng)用中在淡綠色區(qū)域?yàn)榻换^(qū)域正蛙,修改姓名督弓、年齡营曼,點(diǎn)擊更新按鈕乒验,會(huì)彈出MetaMask的對(duì)話框,顯示的是交易的信息蒂阱,點(diǎn)擊確定锻全,應(yīng)用頁(yè)面淡藍(lán)色區(qū)域的姓名、年齡就會(huì)更新了录煤。
與ganache節(jié)點(diǎn)交互
truffle console
通過(guò)上面指令進(jìn)入控制臺(tái)鳄厌,可以運(yùn)行web3.js的指令。web3.js指令具體查看:以太坊 web3.js 中文版文檔
比如
truffle(development)> web3.eth.accounts
truffle(development)> web3.currentProvider //web3所鏈接的ethereum網(wǎng)絡(luò)的相關(guān)信息
附最終的文件目錄結(jié)構(gòu)
|-- Dapp
|-- build //編譯合約后生成的json文件目錄
|-- contracts
-- Migrations.json
-- Simple.json
|-- contracts //Solidity合約代碼
-- Migrations.sol //此文件為必須
-- Simple.sol //合約文件
|-- migrations //合約部署腳本
-- 1_initial_migration.js //用來(lái)部署Migrations.sol的
-- 1544177454_simple.js //合約遷移文件
|-- test //測(cè)試文件目錄
-- TestSimple.sol
-- Simple.js
|-- node_modules //npm模塊妈踊,lite-server和truffle-contract在里面
|-- src //資源文件目錄
-- index.html
-- index.css
|-- js
-- index.js
-- truffle-contract.min.js
-- truffle-config.js //windows下的truffle配置文件
-- truffle.js //linux了嚎、mac下的truffle配置文件
-- package.json //npm init后的配置文件
-- bs-config.json //lite-server的配置文件