使用Truffle開發(fā)Dapp

本文內(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)聽事件的方法。


Dapp開發(fā)流程.png

第一個去中心化應(yīng)用

本應(yīng)用使用Truffle來開發(fā)Dapp。功能與 第一個去中心化應(yīng)用 一樣。

應(yīng)用效果圖如下


第一個去中心化應(yīng)用.png

應(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.jsonbs-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的配置文件
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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