本文內(nèi)容來自熊麗兵老師在登鏈學(xué)院上的《以太坊Dapp開發(fā)實戰(zhàn)》課程
簡介
Dapp VS App
去中心化的應(yīng)用的后端是一個分布式的網(wǎng)絡(luò)(中心化應(yīng)用后端是一個中心化的節(jié)點)。前端連接到網(wǎng)絡(luò)中的任意一個節(jié)點都一樣。
去中心化應(yīng)用給節(jié)點發(fā)送的請求叫交易,節(jié)點不單自己處理請求,還把請求轉(zhuǎn)發(fā)到網(wǎng)絡(luò)的其他節(jié)點,請求的最終執(zhí)行需要網(wǎng)絡(luò)的共識,所以應(yīng)用不能直接拿到結(jié)果,是異步的,前端想要知道狀態(tài)的改變需要監(jiān)聽事件的方法。
第一個去中心化應(yīng)用
本應(yīng)用使用Truffle來開發(fā)Dapp。功能與 第一個去中心化應(yīng)用 一樣。
應(yīng)用效果圖如下
應(yīng)用的功能很簡單
淺藍色部分用來顯示區(qū)塊鏈上的姓名和年齡信息
淺綠色部分用來修改區(qū)塊鏈上的姓名和年齡信息
準備
全局安裝truffle框架
sudo npm install -g truffle //安裝truffle。-g代表全局
Truffle是DApps開發(fā)用到的一個最流行的開發(fā)框架。本身基于Javascript。
Truffle也可以理解為是一個腳手架:集成了合約的編譯、鏈接、測試、部署
Truffle官網(wǎng):https://truffleframework.com/truffle
Truffle文檔:https://truffleframework.com/docs/truffle
Truffle命令:https://truffleframework.com/docs/truffle/reference/truffle-commands
Truffle里有個box的概念,相當于把一些代碼框架給打包好了,想用哪個box直接下載就可以使用,在此基礎(chǔ)上編寫自己的Dapp。比如有一個react的box,整合了react的代碼,不用專門下載react了,非常方便。
Truffle box: https://github.com/truffle-box
本文為了梳理開發(fā)流程,沒有使用box。
全局安裝ganache節(jié)點
sudo npm install -g ganache-cli //安裝ganache-cli節(jié)點
ganache-cli //啟動ganache-cli節(jié)點,后續(xù)會用到。可單獨開一個終端
以太坊節(jié)點也叫以太坊客戶端。智能合約必須部署到以太坊節(jié)點上來運行。
以太坊節(jié)點有Ganache虛擬節(jié)點、Geth節(jié)點、以太坊測試節(jié)點(Ropsten、kovan、Rinkeby)、以太坊主網(wǎng)(Main Ethereum Network)
開發(fā)過程中需要不斷的修改代碼和測試,需要得到及時的反饋。Truffle官方推薦使用適合開發(fā)的客戶端Ganache(前身為EtherumJS TestRPC)
。
Ganache是一個完整的在內(nèi)存中的區(qū)塊鏈(因為在內(nèi)存中,所以重啟后數(shù)據(jù)會丟失),僅僅存在于你開發(fā)的設(shè)備上。它在執(zhí)行交易時是實時返回,而不等待默認的出塊時間,可以快速驗證新寫的代碼,當出現(xiàn)錯誤時能夠即時反饋。它同時還是一個支持自動化測試的功能強大的客戶端。Truffle充分利用它的特性,能將測試運行時間提速近90%。
ganache有命令行版和界面版兩個版本,使用哪個都可以。本文使用的是命令行版
ganache命令行版:https://github.com/trufflesuite/ganache-cli/tree/master
ganache界面版:https://github.com/trufflesuite/ganache/releases
安裝文檔:https://truffleframework.com/docs/ganache/quickstart
節(jié)點選擇
開發(fā) | 部署 | 正式部署前測試 | 正式部署 |
---|---|---|---|
ganache | Geth | 以太坊測試節(jié)點 | 以太坊主網(wǎng) |
MetaMask連接ganache節(jié)點
MetaMask是一款瀏覽器插件錢包,不需下載安裝客戶端,只需添加至瀏覽器擴展程序即可使用。它不僅是一個簡單的錢包,還可以很方便的調(diào)試和測試以太坊的智能合約。
官網(wǎng):https://metamask.io/
Github地址:https://github.com/MetaMask/metamask-extension
文檔:https://metamask.github.io/metamask-docs/
使用教程參考:http://www.lxweimin.com/p/4a28566c425d
將MetaMask連接ganache節(jié)點并導(dǎo)入一個ganache自動創(chuàng)建的賬戶。用此賬戶部署合約以及發(fā)送交易。
連接節(jié)點
MetaMask的network選擇ganache-cli的節(jié)點url(localhost 8545),連接到ganache-cli節(jié)點
MetaMask導(dǎo)入賬戶
在MetaMask的import Account里導(dǎo)入Private Key。用此賬戶部署合約以及發(fā)送交易。
ganache-cli開啟時會默認創(chuàng)建十個賬戶,每個賬戶都有一個Private Key
創(chuàng)建項目
mkdir Dapp //創(chuàng)建項目目錄
cd Dapp
truffle init //truffle創(chuàng)建項目,得到項目框架(當然也可以用truffle提供的box來創(chuàng)建項目,在box的基礎(chǔ)上修改)
npm init //將項目轉(zhuǎn)換為npm項目(提示時都使用默認值即可)
本文采用truffle init創(chuàng)建空項目,當然還可以使用truffle的box創(chuàng)建帶有其他框架的項目。
npm init是將項目轉(zhuǎn)換為一個npm的項目,方便管理。
項目根目錄下安裝lite-server服務(wù)器
npm install lite-server //安裝項目服務(wù)器
應(yīng)用需要一個web服務(wù)器,而像nginx、apache等web服務(wù)器需要單獨安裝,在項目外。而lite-server服務(wù)器本身就是項目的一部分,環(huán)境容易統(tǒng)一,方便開發(fā)和維護。
項目根目錄下安裝truffle-contract
npm install truffle-contract //安裝truffle框架提供的contract,對web3進行了封裝,方便與合約進行交互
truffle-contract是web3的一個封裝,在應(yīng)用與智能合約交互時用。
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 //用來部署Migrations.sol的
|-- test //測試代碼
|-- node_modules //npm模塊,lite-server和truffle-contract在里面
-- truffle-config.js //windows下的truffle配置文件
-- truffle.js //linux、mac下的truffle配置文件
-- package.json //npm init后的配置文件
后端(智能合約)
開發(fā)合約
Solidity文檔:https://solidity.readthedocs.io/en/v0.5.1/
直接將項目導(dǎo)入自己喜歡的編輯器,在contracts目錄下新建 Simple.sol 文件,或者終端中使用命令來創(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
編譯完成后,根目錄下會生成一個build目錄,其下的contracts目錄下是合約的json文件
|-- build
|-- contracts
-- Migrations.json
-- Simple.json //包含abi、地址等信息
對合約部署的時候,truffle會更新此文件,寫入truffle.js里的網(wǎng)絡(luò)網(wǎng)絡(luò)信息
測試合約
測試合約有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)建測試文件
|-- 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è)置姓名出錯");
Assert.equal(age, 18, "設(shè)置年齡出錯");
}
}
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í)行測試命令
truffle test //執(zhí)行所有測試文件
truffle test ./test/TestSimple.sol //指定測試哪個文件
測試需要用到網(wǎng)絡(luò),配置部分參考部署合約時的truffle.js的網(wǎng)絡(luò)配置
結(jié)果如下,說明測試通過
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 文件,或者終端中使用命令來創(chuàng)建Simple.js文件
truffle create migration simple
在migrations目錄下就會創(chuàng)建一個部署腳本
|-- migrations
-- 1_initial_migration.js
-- 1544177454_simple.js //命令創(chuàng)建的。前面數(shù)字應(yīng)該是隨機的
配置腳本
參考: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ù)
運行上面指令發(fā)生的事情
運行了1_initial_migration.js、2_deploy_contracts.js文件,ganache客戶端中生成了transaction信息。
contracts目錄里的Migrations.json文件里增加了networks內(nèi)容,最主要的是一個address信息。
前端
|-- Dapp
|-- src //項目根目錄下創(chuàng)建src目錄,存放前端文件
-- index.html
-- index.css
|-- js
-- index.js
-- truffle-contract.min.js //從node_modules/truffle-contract/dist/truffle-contract.min.js復(fù)制過來的
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>第一個去中心化應(yīng)用</title>
<link rel="stylesheet" href="./index.css">
</head>
<body>
<div id="content">
<h1 id="title">第一個去中心化應(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)鍵部分
在第一個去中心化應(yīng)用中,獲取智能合約對象是通過在js代碼里寫入合約abi和合約地址的硬編碼方式,每次變更合約都要重新修改為新的合約abi和合約地址,非常繁瑣,不容易維護。
truffle提供了一個truffle-contract的抽象,對web3進行了封裝,并且引入了promise的用法,方便與合約進行交互。合約編譯的時候會在build/contracts目錄下生成合約對應(yīng)的json文件(重新編譯部署會自動更新json文件),truffle-contract會利用這些信息生成合約對象,避免了硬編碼方式。
兩者對比
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對象
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對象,并賦值給App.contracts.Simple
App.contracts.Simple = TruffleContract(data);
//設(shè)置Provider
App.contracts.Simple.setProvider(App.web3Provider);
//調(diào)用合約的get方法
App.get();
//事件監(jiān)聽,更新顯示的內(nèi)容
App.watchChanged();
});
//調(diào)用事件
App.bindEvents();
},
//合約的get方法
get: function(){
//deployed得到合約的實例,通過then的方式回調(diào)拿到實例
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í)行失敗打印錯誤
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方法,獲取最新值(可沒有,通常使用事件監(jiān)聽的方式)
}).catch(function(err){
console.log(err);
});
});
},
//事件監(jiān)聽
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();
});
});
開啟服務(wù)
原始方式如下
根目錄下輸入
node_modules/lite-server/bin/lite-server
彈出來的頁面中定位到index.html文件,就可以看到應(yīng)用了
localhost:3000/src/index.html
這種方式 index.js里
$.getJSON
第一個參數(shù)路徑為../build/contracts/Simple.json
。上面index.js文件是配置bs-config.json后的寫法,如果使用此方式需要修改路徑。
但這種方式顯然是太麻煩了。可以通過配置lite-server的baseDir來自動定位,通過配置服務(wù)腳本的方式來簡化輸入命令
配置bs-config.json
bs-config.json
或bs-config.js
為lite-server的配置文件
在項目根目錄下創(chuàng)建bs-config.json
,然后寫入配置
{
"server":
{"baseDir": ["./src", "./build/contracts"] }
}
配置./src
,直接localhost:3000 即可打開index.html
配置./build/contracts
,index.js里$.getJSON
第一個參數(shù)路徑就可以省略,直接寫Simple.json即可
修改package.json,加入腳本
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "lite-server" //新加入的腳本
},
在項目根目錄下直接輸入
npm run dev
即可打開項目(自動定位到src下)
交互
與應(yīng)用交互
應(yīng)用中在淡綠色區(qū)域為交互區(qū)域,修改姓名、年齡,點擊更新按鈕,會彈出MetaMask的對話框,顯示的是交易的信息,點擊確定,應(yīng)用頁面淡藍色區(qū)域的姓名、年齡就會更新了。
與ganache節(jié)點交互
truffle console
通過上面指令進入控制臺,可以運行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 //用來部署Migrations.sol的
-- 1544177454_simple.js //合約遷移文件
|-- test //測試文件目錄
-- 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的配置文件