以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

在本系列關於使用以太坊構建DApps教程的第6部分中,我們通過添加投票,黑名單,股息分配和撤銷來完成DAO合約,同時投入一些額外的輔助函數以實現良好的標準。在本教程中,我們將構建一個用於與我們的故事Story交互的Web界面,否則我們無法統計用戶如何參與。所以這是我們故事Story發佈之前的最後一部分。

由於這不是一個Web應用程序教程,我們將保持非常簡單。下面的代碼不是生產就緒的,只是作爲如何將JavaScript連接到區塊鏈的概念證明。但首先,讓我們添加一個新的遷移。

自動轉移

現在,當我們部署代幣和DAO時,它們位於區塊鏈上但不進行交互。爲了測試我們構建的內容,我們需要手動將代幣所有權和餘額轉移到DAO,這在測試期間可能很乏味。

讓我們寫一個新的遷移,爲我們做這件事。創建文件4_configure_relationship.js並將以下內容放在其中:

var Migrations = artifacts.require("./Migrations.sol");
var StoryDao = artifacts.require("./StoryDao.sol");
var TNSToken = artifacts.require("./TNSToken.sol");

var storyInstance, tokenInstance;

module.exports = function (deployer, network, accounts) {

    deployer.then(function () {
            return TNSToken.deployed();
        }).then(function (tIns) {
            tokenInstance = tIns;
            return StoryDao.deployed();
        }).then(function (sIns) {
            storyInstance = sIns;
            return balance = tokenInstance.totalSupply();
        }).then(function (bal) {
            return tokenInstance.transfer(storyInstance.address, bal);
        })
        .then(function (something) {
            return tokenInstance.transferOwnership(storyInstance.address);
        });
}

這是這段代碼的作用。首先,你會注意到它是基於promise的。它充滿了各種調用。這是因爲我們在調用下一個數據之前依賴於返回一些數據的函數。所有合約調用都是基於promise的,這意味着它們不會立即返回數據,因爲Truffle需要向節點請求信息,因此promise在將來返回數據。我們強制代碼等待這些數據,使用then關鍵詞並提供所有then調用函數,這些函數在最終給出時將使用此結果調用。

所以,按順序:

  • 首先,向節點詢問已部署代幣的地址並將其返回。
  • 然後,接受此數據,將其保存到全局變量中,並詢問已部署的DAO的地址並將其返回。
  • 然後,接受這些數據,將其保存到全局變量中,並詢問代幣合約的所有者將在其帳戶中具有的餘額,這在技術上是總供應量,並返回此數據。
  • 然後,一旦你得到這個餘額,用它來調用這個代幣的transfer函數,並將令牌發送到DAO的地址並返回結果。
  • 然後,忽略返回的結果——我們只想知道它何時完成——最後將代幣的所有權轉移到DAO的地址,返回數據但不丟棄它。

運行truffle migrate --reset現在應該產生這樣的輸出:

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

前端

前端是一個常規的靜態HTML頁面,其中包含一些JavaScript用於與區塊鏈和一些CSS進行通信以使頁面變得不那麼難看。

讓我們在子文件夾public創建一個文件index.html,併爲其提供以下內容:

<!DOCTYPE HTML>

<html lang="en">
<head>
    <title>The Neverending Story</title>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <meta name="description" content="The Neverending Story is an community curated and moderated Ethereum dapp-story">
    <link rel="stylesheet" href="assets/css/main.css"/>
</head>
<body>

    <div class="grid-container">
        <div class="header container">
            <h1>The Neverending Story</h1>
            <p>A story on the Ethereum blockchain, community curated and moderated through a Decentralized Autonomous Organization (DAO)</p>
        </div>
        <div class="content container">
            <div class="intro">
                <h3>Chapter 0</h3>
                <p class="intro">It's a rainy night in central London.</p>
            </div>
            <hr>
            <div class="content-submissions">
                <div class="submission">
                    <div class="submission-body">This is an example submission. A proposal for its deletion has been submitted.</div>
                    <div class="submission-submitter">0xbE2B28F870336B4eAA0aCc73cE02757fcC428dC9</div>
                    <div class="submission-actions">
                        <div class="deletionproposed" data-votes="3024" data-deadline="1531607200"></div>
                    </div>
                </div>
                <div class="submission">
                        <div class="submission-body">This is a long submission. It has over 244 characters, just we can see what it looks like when rendered in the UI. We need to make sure it doesn't break anything and the layout also needs to be maintained, not clashing with actions/buttons etc.</div>
                        <div class="submission-submitter">0xbE2B28F870336B4eAA0aCc73cE02757fcC428dC9</div>
                        <div class="submission-actions">
                            <div class="delete"></div>
                        </div>
                </div>
                <div class="submission">
                        <div class="submission-body">This is an example submission. A proposal for its deletion has been submitted but is looking like it'll be rejected.</div>
                        <div class="submission-submitter">0xbE2B28F870336B4eAA0aCc73cE02757fcC428dC9</div>
                        <div class="submission-actions">
                            <div class="deletionproposed" data-votes="-790024" data-deadline="1531607200"></div>
                        </div>
                </div>
            </div>    
        </div>
        <div class="events container">
            <h3>Latest Events</h3>
            <ul class="eventlist">

            </ul>
        </div>
        <div class="information container">
            <p>Logged in / out</p>
            <div class="avatar">
                <img src="http://placeholder.pics/svg/200/DEDEDE/555555/avatar" alt="avatar">
            </div>
            <dl>
                <dt>Contributions</dt>
                <dd>0</dd>
                <dt>Deletions</dt>
                <dd>0</dd>
                <dt>Tokens</dt>
                <dd>0</dd>
                <dt>Proposals submitted</dt>
                <dd>0</dd>
                <dt>Proposals voted on</dt>
                <dd>0</dd>
            </dl>
        </div>
    </div>

<script src="assets/js/web3.min.js"></script>
<script src="assets/js/app.js"></script>
<script src="assets/js/main.js"></script>

</body>
</html>

注意:這是一個非常基本的框架,僅用於演示集成。請不要把這當成最終產品!

你可能缺少web3文件夾中的dist文件夾。該軟件仍處於測試階段,因此仍有可能出現輕微漏洞。要解決此問題並使用dist文件夾,請運行npm install ethereum/web3.js --save

對於CSS,讓我們在public/assets/css/main.css一些基本內容:

@supports (grid-area: auto) {
    .grid-container{
      display: grid;
      grid-template-columns: 6fr 5fr 4fr;
      grid-template-rows: 10rem ;
      grid-column-gap: 0.5rem;
      grid-row-gap: 0.5rem;
      justify-items: stretch;
      align-items: stretch;
      grid-template-areas:
      "header header information"
      "content events information";
      height: 100vh;
    }
    .events {
      grid-area: events;
    }
    .content {
      grid-area: content;
    }
    .information {
      grid-area: information;
    }
    .header {
      grid-area: header;
      text-align: center;
    }

    .container {
        border: 1px solid black;
        padding: 15px;
        overflow-y: scroll;
    }

    p {
        margin: 0;
    }
  }

body {
    padding: 0;
    margin: 0;
    font-family: sans-serif;
}

然後作爲JS,我們將在public/assets/js/app.js

var Web3 = require('web3');

var web3 = new Web3(web3.currentProvider);
console.log(web3);

這裏發生了什麼?

既然我們假設所有用戶都安裝了MetaMask,並且MetaMask將自己的Web3實例注入到任何訪問過的網頁的DOM中,我們基本上可以訪問我們網站上MetaMask的wallet provider。實際上,如果我們在頁面打開時登錄MetaMask,我們將在控制檯中看到:

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

注意MetamaskInpageProvider是如何激活的。實際上,如果我們在控制檯中鍵入web3.eth.accounts,我們將通過MetaMask訪問的所有帳戶都將打印出來:

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

但是,這個特殊帳戶默認添加到我自己的個人Metamask中,因此餘額爲0eth。它不是我們運行的Ganache或PoA鏈的一部分:

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

請注意,如果要求我們的MetaMask活動帳戶的餘額產生0,同時要求我們的一個私有區塊鏈帳戶的餘額產生100以太(在我的情況下它是Ganache,所以所有帳戶都用100以太初始化)。

關於語法

你會注意到這些調用的語法看起來有點奇怪:

web3.eth.getBalance("0x35d4dCDdB728CeBF80F748be65bf84C776B0Fbaf", function(err, res){console.log(JSON.stringify(res));});

爲了讀取區塊鏈數據,大多數MetaMask用戶不會在本地運行節點,而是從Infura或其他遠程節點請求它。因此,我們實際上可以依靠回調。因此,通常不支持同步方法。相反,一切都是通過promise或回調來完成的——就像本文開頭的部署步驟一樣。這是否意味着你需要非常熟悉爲以太坊開發JS的promise?不,這意味着以下內容。在DOM中進行JS調用時......

  • 總是提供一個回調函數作爲你正在調用的函數的最後一個參數。
  • 假設它的返回值是雙重的:第一個error,然後是result

所以,基本上,只需考慮延遲響應就可以了。當節點響應數據時,你定義爲回調函數的函數將由JavaScript調用。是的,這意味着你不能指望你的代碼在編寫時逐行執行!

有關promises,回調和所有async jazz的更多信息,請參閱此文章

帳戶信息

如果我們打開上面提到的網站骨架,我們得到這樣的東西:

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

讓我們用真實數據填充關於帳戶信息的最右側列。

session

當用戶未登錄其MetaMask擴展名時,帳戶列表將爲空。如果甚至沒有安裝MetaMask,則提供程序將爲空(未定義)。當他們登錄MetaMask時,接口將可用並提供帳戶信息以及與連接的以太坊節點(live或Ganache或其他)的交互。

提示:要進行測試,你可以通過單擊右上角的頭像圖標然後選擇註銷來註銷MetaMask。如果用戶界面看起來不像下面的屏幕截圖,你可能需要通過打開菜單並單擊“試用Beta”來激活Beta用戶界面。

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

首先,如果用戶已註銷,請將該狀態列的所有內容替換爲用戶的消息:

<div class="information container">
    <div class="logged out">
        <p>You seem to be logged out of MetaMask or MetaMask isn't installed. Please log into MetaMask - to learn more,
            see
            <a href="https://bitfalls.com/2018/02/16/metamask-send-receive-ether/">this tutorial</a>.</p>
    </div>
    <div class="logged in" style="display: none">
        <p>You are logged in!</p>
    </div>
</div>

處理它的JS看起來像這樣(在public/assets/js/main.js):

var loggedIn;

(function () {

    loggedIn = setLoggedIn(web3.currentProvider !== undefined && web3.eth.accounts.length > 0);

})();

function setLoggedIn(isLoggedIn) {
    let loggedInEl = document.querySelector('div.logged.in');
    let loggedOutEl = document.querySelector('div.logged.out');

    if (isLoggedIn) {
        loggedInEl.style.display = "block";
        loggedOutEl.style.display = "none";
    } else {
        loggedInEl.style.display = "none";
        loggedOutEl.style.display = "block";
    }

    return isLoggedIn;
}

第一部分——(function () { -包含一旦網站加載就要執行的邏輯。因此,當頁面準備就緒時,內部的任何內容都會立即執行。調用單個函數setLoggedIn並將條件傳遞給它條件是:

  • 設置web3對象的currentProvider(即網站中存在web3客戶端)。
  • 可用的帳戶數量非零,即可通過此Web3提供商使用帳戶。 換句話說,我們已登錄至少一個帳戶。

如果這些條件一起評估爲true,則setLoggedIn函數使“Logged out”消息不可見,並且“Logged In”消息可見。

所有這些都具有能夠使用任何其他web3提供商的額外優勢。如果最終出現MetaMask替代方案,它將立即與此代碼兼容,因爲我們並未明確期望任何地方的MetaMask。

帳戶頭像

因爲以太坊錢包的每個私鑰都是唯一的,所以它可用於生成獨特的圖像。這是你在MetaMask的右上角或使用MyEtherWallet時看到的彩色化身,儘管Mist,MyEtherWallet和MetaMask都使用不同的方法。讓我們爲登錄用戶生成一個並顯示它。

Mist中的圖標是使用Blockies庫生成的——是自定義的,因爲原始文件具有損壞的隨機數生成器,並且可以爲不同的鍵生成相同的圖像。因此,要安裝此文件,請將此文件下載到assets/js文件夾中。然後,在index.html我們在main.js之前包含它:

 <script src="assets/js/app.js"></script>
    <script src="assets/js/blockies.min.js"></script>
    <script src="assets/js/main.js"></script>

</body>

我們還應該升級logged.in容器:

<div class="logged in" style="display: none">
    <p>You are logged in!</p>
    <div class="avatar">

    </div>
</div>

在main.js,我們啓動該功能。

if (isLoggedIn) {
      loggedInEl.style.display = "block";
      loggedOutEl.style.display = "none";

      var icon = blockies.create({ // All options are optional
          seed: web3.eth.accounts[0], // seed used to generate icon data, default: random
          size: 20, // width/height of the icon in blocks, default: 8
          scale: 8, // width/height of each block in pixels, default: 4
      });

      document.querySelector("div.avatar").appendChild(icon);

因此,我們升級JS代碼的登錄部分以生成圖標並將其粘貼到頭像部分。我們應該在渲染之前將它與CSS稍微對齊:

 div.avatar { width: 100%; text-align: center; margin: 10px 0; } 

現在,如果我們在登錄MetaMask時刷新頁面,我們應該會看到生成的頭像圖標。

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

帳戶餘額

現在讓我們輸出一些帳戶餘額信息。

我們擁有一系列只讀功能,我們專門爲此目的而開發。所以讓我們查詢區塊鏈並詢問一些信息。爲此,我們需要通過以下步驟調用智能合約功能

1.ABI

獲取我們正在調用的函數的合約的ABI。ABI包含函數簽名,因此我們的JS代碼知道如何調用它們。在此處瞭解有關ABI的更多信息。

你可以通過在編譯後打開項目文件夾中的build/TNSToken.jsonbuild/StoryDao.json文件並僅選擇abi部分來獲取TNS代幣和StoryDAO的ABI([]方括號之間的部分):

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

我們將這個ABI放在我們的JavaScript代碼的頂部,進入main.js如下所示:

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

請注意,上面的屏幕截圖顯示了我的代碼編輯器(Microsoft Visual Code)摺疊的縮寫插入。如果你查看行號,你會注意到令牌的ABI是400行代碼,而DAO的ABI是另外1000行,所以將它粘貼到本文中是沒有意義的。

2.實例化代幣

if (loggedIn) {

    var token = TNSToken.at('0x3134bcded93e810e1025ee814e87eff252cff422');
    var story = StoryDao.at('0x729400828808bc907f68d9ffdeb317c23d2034d5');
    token.balanceOf(web3.eth.accounts[0], function(error, result) {console.log(JSON.stringify(result))});
    story.getSubmissionCount(function(error, result) {console.log(JSON.stringify(result))});
//...

我們使用Truffle給我們的地址調用每個合約,並分別爲每個tokenstory創建一個實例。然後,我們簡單地調用函數(與以前一樣異步)。控制檯給我們兩個零,因爲MetaMask中的帳戶有0個代幣,因爲現在故事story中有0個提交。

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

3.讀取和輸出數據

最後,我們可以使用我們提供的信息填充用戶的個人資料數據。

讓我們更新我們的JavaScript:

var loggedIn;

(function () {

    loggedIn = setLoggedIn(web3.currentProvider !== undefined && web3.eth.accounts.length > 0);

    if (loggedIn) {

        var token = TNSToken.at('0x3134bcded93e810e1025ee814e87eff252cff422');
        var story = StoryDao.at('0x729400828808bc907f68d9ffdeb317c23d2034d5');

        token.balanceOf(web3.eth.accounts[0], function(error, result) {console.log(JSON.stringify(result))});
        story.getSubmissionCount(function(error, result) {console.log(JSON.stringify(result))});

        readUserStats().then(User => renderUserInfo(User));
    }

})();

async function readUserStats(address) {
    if (address === undefined) {
        address = web3.eth.accounts[0];
    }
    var User = {
        numberOfSubmissions: await getSubmissionsCountForUser(address),
        numberOfDeletions: await getDeletionsCountForUser(address),
        isWhitelisted: await isWhitelisted(address),
        isBlacklisted: await isBlacklisted(address),
        numberOfProposals: await getProposalCountForUser(address),
        numberOfVotes: await getVotesCountForUser(address)
    }
    return User;
}

function renderUserInfo(User) {
    console.log(User);

    document.querySelector('#user_submissions').innerHTML = User.numberOfSubmissions;
    document.querySelector('#user_deletions').innerHTML = User.numberOfDeletions;
    document.querySelector('#user_proposals').innerHTML = User.numberOfProposals;
    document.querySelector('#user_votes').innerHTML = User.numberOfVotes;
    document.querySelector('dd.user_blacklisted').style.display = User.isBlacklisted ? 'inline-block' : 'none';
    document.querySelector('dt.user_blacklisted').style.display = User.isBlacklisted ? 'inline-block' : 'none';
    document.querySelector('dt.user_whitelisted').style.display = User.isWhitelisted ? 'inline-block' : 'none';
    document.querySelector('dd.user_whitelisted').style.display = User.isWhitelisted ? 'inline-block' : 'none';
}

async function getSubmissionsCountForUser(address) {
    if (address === undefined) {
        address = web3.eth.accounts[0];
    }
    return new Promise(function (resolve, reject) {
        resolve(0);
    });
}
async function getDeletionsCountForUser(address) {
    if (address === undefined) {
        address = web3.eth.accounts[0];
    }
    return new Promise(function (resolve, reject) {
        resolve(0);
    });
}
async function getProposalCountForUser(address) {
    if (address === undefined) {
        address = web3.eth.accounts[0];
    }
    return new Promise(function (resolve, reject) {
        resolve(0);
    });
}
async function getVotesCountForUser(address) {
    if (address === undefined) {
        address = web3.eth.accounts[0];
    }
    return new Promise(function (resolve, reject) {
        resolve(0);
    });
}
async function isWhitelisted(address) {
    if (address === undefined) {
        address = web3.eth.accounts[0];
    }
    return new Promise(function (resolve, reject) {
        resolve(false);
    });
}
async function isBlacklisted(address) {
    if (address === undefined) {
        address = web3.eth.accounts[0];
    }
    return new Promise(function (resolve, reject) {
        resolve(false);
    });
}

讓我們更改個人資料信息部分:

<div class="logged in" style="display: none">
    <p>You are logged in!</p>
    <div class="avatar">

    </div>
    <dl>
        <dt>Submissions</dt>
        <dd id="user_submissions"></dd>
        <dt>Proposals</dt>
        <dd id="user_proposals"></dd>
        <dt>Votes</dt>
        <dd id="user_votes"></dd>
        <dt>Deletions</dt>
        <dd id="user_deletions"></dd>
        <dt class="user_whitelisted">Whitelisted</dt>
        <dd class="user_whitelisted">Yes</dd>
        <dt class="user_blacklisted">Blacklisted</dt>
        <dd class="user_blacklisted">Yes</dd>
    </dl>
</div>

你會注意到我們在獲取數據時使用了promises,即使我們的函數當前只是模擬函數:它們會立即返回平面數據。這是因爲每個函數都需要不同的時間來獲取我們要求它獲取的數據,因此我們將在填充User對象之前等待它們完成,然後將其傳遞給render函數,該函數更新了屏幕。

如果您對JS承諾不熟悉並希望瞭解更多信息,請參閱此帖子

現在,我們所有的功能都是嘲笑; 我們需要先做一些寫操作才能閱讀。 但首先我們需要準備好注意那些寫作的發生!

監聽事件

爲了能夠跟蹤合約發出的事件,我們需要監聽它們——否則我們將所有這些emit語句都放入代碼中。我們構建的模擬UI的中間部分用於保存這些事件。

以下是我們如何監聽區塊鏈發出的事件:

// Events

var WhitelistedEvent = story.Whitelisted(function(error, result) {
    if (!error) {
        console.log(result);
    }
})

這裏我們在StoryDao合約的story實例上調用Whitelisted函數,並將回調傳遞給它。每當觸發此給定事件時,將自動調用此回調。因此,當用戶被列入白名單時,代碼將自動將該事件的輸出記錄到控制檯。

但是,這隻會獲取網絡挖掘的最後一個塊的最後一個事件。因此,如果從第1塊到第10塊觸發了幾個白名單事件,它只會向我們展示第10塊中的那些事件,如果有的話。更好的方法是使用這種方法:

story.Whitelisted({}, { fromBlock: 0, toBlock: 'latest' }).get((error, eventResult) => {
  if (error) {
    console.log('Error in myEvent event handler: ' + error);
  } else {  
    // eventResult contains list of events!
    console.log('Event: ' + JSON.stringify(eventResult[0].args));
  }
});

注意:將上面的內容放在JS文件底部的一個單獨的部分,一個專門用於事件。

在這裏,我們使用get函數,它允許我們定義從中獲取事件的塊範圍。我們使用0到最新,這意味着我們可以獲取此類型的所有事件。但是這增加了與上述監聽方法發生衝突的可能。監聽方法輸出最後一個塊的事件,get方法輸出所有這些事件。我們需要一種方法來使JS忽略雙重事件。不要寫那些你已經從歷史中獲取的東西。我們會進一步做到這一點,但就目前而言,讓我們來處理白名單。

帳戶白名單

最後,讓我們進行一些寫操作。

第一個也是最簡單的一個是白名單。請記住,要獲得白名單,帳戶需要向DAO的地址發送至少0.01以太。你將在部署時獲得此地址。如果你的Ganache/PoA鏈在本課程的各個部分之間重新啓動,那沒關係,只需使用truffle migrate --reset重新運行,你就可以獲得代幣和DAO的新地址。在我的例子中,DAO的地址是0x729400828808bc907f68d9ffdeb317c23d2034d5,我的代幣是0x3134bcded93e810e1025ee814e87eff252cff422

設置完所有內容後,讓我們嘗試向DAO地址發送一定數量的以太。讓我們嘗試0.05以太只是爲了好玩,所以我們可以看看DAO是否爲我們提供額外的計算代幣,以支付超額費用。

注意:不要忘記自定義gas量——只需在21000限制之上再拍一個零——使用標記爲紅色的圖標。爲什麼這有必要?因爲由簡單的ether發送(回調函數)觸發的函數執行超過21000的額外邏輯,這對於簡單發送就足夠了。所以我們需要達到極限。不要擔心:超出此限制的任何內容都會立即退款。有關gas如何工作的入門讀物,請參見[https://www.sitepoint.com/ethereum-transaction-costs]。

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

在交易確認後(你將在MetaMask中將其視爲“已確認”),我們可以在MetaMask帳戶中檢查代幣金額。我們首先需要將自定義代幣添加到MetaMask中,以便跟蹤它們。根據下面的動畫,過程如下:選擇MetaMask菜單,向下滾動到Add Tokens,選擇Custom Token,粘貼Truffle在遷移時給你的代幣地址,點擊Next,查看餘額是否爲ok,然後選擇添加Add Tokens

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

對於0.05 eth,我們應該有400k令牌,我們這樣做。

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

但是這個事件怎麼樣?我們收到了這個白名單的通知嗎?我們來看看控制檯吧。

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

實際上,完整的數據集就在那​​裏——發出事件的地址,塊數和挖掘它的hash,等等。其中包括args對象,它告訴我們事件數據:addr是被列入白名單的地址,狀態是它是添加到白名單還是從中刪除。成功!

如果我們現在刷新頁面,則事件再次出現在控制檯中。但是怎麼樣?我們沒有將任何新人列入白名單。爲什麼事件會發生警告?EVM中的事件是它們不像JavaScript那樣是一次性的事情。當然,它們包含任意數據並僅用作輸出,但它們的輸出永遠在區塊鏈中註冊,因爲導致它們的交易也永久地在區塊鏈中註冊。因此事件將在發出之後保留,這使我們不必將它們存儲在某處並在頁面刷新時調用它們!

現在讓我們將其添加到UI中的事件屏幕!編輯JavaScript文件的Events部分,如下所示:

// Events

var highestBlock = 0;
var WhitelistedEvent = story.Whitelisted({}, { fromBlock: 0, toBlock: "latest" });

WhitelistedEvent.get((error, eventResult) => {
  if (error) {
    console.log('Error in Whitelisted event handler: ' + error);
  } else {  
    console.log(eventResult);
    let len = eventResult.length;
    for (let i = 0; i < len; i++) {
      console.log(eventResult[i]);
      highestBlock = highestBlock < eventResult[i].blockNumber ? eventResult[i].blockNumber : highestBlock;
      printEvent("Whitelisted", eventResult[i]);
    }
  }
});

WhitelistedEvent.watch(function(error, result) {
  if (!error && result.blockNumber > highestBlock) {
    printEvent("Whitelisted", result);
  }
});

function printEvent(type, object) {
  switch (type) {
    case "Whitelisted":
      let el;
      if (object.args.status === true) {
          el = "<li>Whitelisted address "+ object.args.addr +"</li>";
      } else {
          el = "<li>Removed address "+ object.args.addr +" from whitelist!</li>";
      }
      document.querySelector("ul.eventlist").innerHTML += el;
    break;
    default:
    break;
  }
}

哇,變得很快,是吧?不用擔心,我們會澄清。

highestBlock變量將記住從歷史記錄中獲取的最新塊。我們創建了一個事件的實例,併爲它附加了兩個監聽器。一個是get,它從歷史記錄中獲取所有事件並記住最新的塊。另一個是watchwatch事件“實時”並在最近一個塊中出現新事件時觸發。只有當剛剛進入的塊大於我們記憶中最高的塊時,觀察者纔會觸發,確保只有新事件被附加到事件列表中。

我們還添加了一個printEvent函數執行打印事件的操作; 我們也可以將它重複用於其他類型的事件!

如果我們現在測試它,確實,我們可以很好地打印出來。

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

現在嘗試自己做這個,我們的故事Story可以發出的所有其他事件!看看你是否可以弄清楚如何一次處理它們,而不必爲每個都寫出這個邏輯。(提示:在數組中定義它們的名稱,然後遍歷這些名稱並動態註冊事件!)

手動檢查

你還可以通過在MyEtherWallet中打開並調用其whitelist函數來手動檢查StoryBAO的白名單和所有其他公共參數。

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

你會注意到,如果我們檢查剛剛發送白名單金額的帳戶,我們將獲得true回覆,表明此帳戶確實存在於whitelist映射中。

在將其添加到Web UI之前,使用此相同的功能菜單來試驗其他功能。

提交參賽作品

最後,讓我們從UI進行正確的寫函數調用。這一次,我們將在故事Story中提交一個條目。首先,我們需要清除我們在開始時放在那裏的示例條目。編輯HTML到這個:

<div class="content container">
    <div class="intro">
        <h3>Chapter 0</h3>
        <p class="intro">It's a rainy night in central London.</p>
    </div>
    <hr>
    <div class="submission_input">
        <textarea name="submission-body" id="submission-body-input" rows="5"></textarea>
        <button id="submission-body-btn">Submit</button>
    </div>
    ...

還有一些基本的CSS:

.submission_input textarea {
  width: 100%;
}

我們添加了一個非常簡單的textarea,用戶可以通過它提交新條目。

我們現在來做JS部分吧。

首先,讓我們準備通過添加一個新事件並修改我們的printEvent函數來接受這個事件。我們還可以對整個事件部分進行一些重構,以使其更具可重用性。

// Events

var highestBlock = 0;
var WhitelistedEvent = story.Whitelisted({}, { fromBlock: 0, toBlock: "latest" });
var SubmissionCreatedEvent = story.SubmissionCreated({}, { fromBlock: 0, toBlock: "latest" });

var events = [WhitelistedEvent, SubmissionCreatedEvent];
for (let i = 0; i < events.length; i++) {
  events[i].get(historyCallback);
  events[i].watch(watchCallback);
}

function watchCallback(error, result) {
  if (!error && result.blockNumber > highestBlock) {
    printEvent(result.event, result);
  }
}

function historyCallback(error, eventResult) {
  if (error) {
    console.log('Error in event handler: ' + error);
  } else {  
    console.log(eventResult);
    let len = eventResult.length;
    for (let i = 0; i < len; i++) {
      console.log(eventResult[i]);
      highestBlock = highestBlock < eventResult[i].blockNumber ? eventResult[i].blockNumber : highestBlock;
      printEvent(eventResult[i].event, eventResult[i]);
    }
  }
}

function printEvent(type, object) {
  let el;
  switch (type) {
    case "Whitelisted":
      if (object.args.status === true) {
          el = "<li>Whitelisted address "+ object.args.addr +"</li>";
      } else {
          el = "<li>Removed address "+ object.args.addr +" from whitelist!</li>";
      }
      document.querySelector("ul.eventlist").innerHTML += el;
    break;
    case "SubmissionCreated":
      el = "<li>User " + object.args.submitter + " created a"+ ((object.args.image) ? "n image" : " text") +" entry: #" + object.args.index + " of content " + object.args.content+"</li>";
      document.querySelector("ul.eventlist").innerHTML += el;
    break;
    default:
    break;
  }
}

現在我們需要做的就是添加一個全新的事件來實例化它,然後爲它定義一個case。

接下來,讓我們提交。

document.getElementById("submission-body-btn").addEventListener("click", function(e) {
    if (!loggedIn) {
        return false;
    }

    var text = document.getElementById("submission-body-input").value;
    text = web3.toHex(text);

    story.createSubmission(text, false, {value: 0, gas: 400000}, function(error, result) {
        refreshSubmissions();
    });
});

function refreshSubmissions() {
    story.getAllSubmissionHashes(function(error, result){
        console.log(result);
    });
}

在這裏,我們向提交表單添加一個事件監聽器,一旦提交,首先拒絕所有用戶未登錄的內容,然後抓取內容並將其轉換爲十六進制格式(這是我們需要將值存儲爲bytes )。

最後,它通過調用createSubmission函數並提供兩個參數來創建交易:條目的文本和false標記(意思即不是圖像)。第三個參數是交易設置:值表示要發送多少以太,而gas表示你想要默認的gas限制量。這可以在客戶端(MetaMask)中手動更改,但這是一個很好的起點,以確保我們不會遇到限制。最後一個參數是我們現在已經習慣的回調函數,這個回調函數將調用一個刷新函數來加載故事Story的所有提交。目前,此刷新功能僅加載故事story哈希並將它們放入控制檯,以便我們檢查一切是否正常。

注意:以太量爲0,因爲第一個條目是免費的。進一步的條目將需要添加以太幣。我們將動態計算留給你當作業。提示:爲此目的,我們的DAO中有一個calculateSubmissionFee函數。

此時,我們需要在JS的頂部更改一些在頁面加載時自動執行的函數:

if (loggedIn) {

    token.balanceOf(web3.eth.accounts[0], function(error, result) {console.log(JSON.stringify(result))});
    story.getSubmissionCount(function(error, result) {console.log(JSON.stringify(result))});

    web3.eth.defaultAccount = web3.eth.accounts[0]; // CHANGE

    readUserStats().then(User => renderUserInfo(User));
    refreshSubmissions(); // CHANGE
} else {
    document.getElementById("submission-body-btn").disabled = "disabled";
}

更改標記爲//CHANGE:第一個允許我們設置執行交易的默認帳戶。這可能會在未來的Web3版本中默認使用。第二個刷新頁面加載時提交的內容,因此我們在網站打開時獲得一個完整的故事story。

如果你現在嘗試提交條目,MetaMask應在你單擊“提交”後立即打開,並要求你確認提交。

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

你還應該在事件部分中看到事件打印出來。

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

控制檯應該回顯這個新條目的哈希值。

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

注意:MetaMask目前在私有網絡和nonce方面存在問題。它在這裏描述並將很快修復,但如果nonce在提交條目時在JavaScript控制檯中收到錯誤,那麼目前的權宜之計解決方案是重新安裝MetaMask(禁用和啓用將不起作用)。請記住首先備份你的種子SEED:你需要它來重新導入你的MetaMask帳戶!

最後,讓我們獲取這些條目並顯示它們。讓我們從一些CSS開始:

.content-submissions .submission-submitter { font-size: small; } 

現在讓我們更新一下這個refreshSubmissions功能:

我們瀏覽所有提交內容,獲取它們的哈希值,獲取每個哈希值,然後在屏幕上輸出。如果提交者與登錄用戶相同,則打印“你”而不是地址。

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

讓我們添加另一個條目進行測試。

以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

結論

在這一部分中,我們爲DApp開發了基本前端的開端。

由於開發完整的前端應用程序也可以成爲它自己的一個過程,我們將作爲家庭作業留給你進一步的發展。只需調用所演示的函數,將它們綁定到常規JavaScript流程中(通過像VueJS這樣的框架或普通的舊jQuery或像我們上面所做的原生JS)並將它們綁定在一起。它實際上就像與標準服務器API交談。如果你遇到困難,請查看代碼的項目倉庫!

可以執行的其他升級:

  • 檢測web3提供程序何時更改或可用帳戶數何時更改,指示登錄或註銷事件並自動重新加載頁面。
  • 除非用戶已登錄,否則將阻止呈現提交表單。
  • 防止呈現投票和刪除按鈕,除非用戶至少有1個代幣等。
  • 讓人們提交併呈現Markdown!
  • 按時間(塊號)訂購事件,而不是按類型訂購!
  • 使事件更漂亮,更可讀:不是顯示十六進制內容,而是將其翻譯爲ASCII並截斷爲30個左右的字符。
  • 使用像VueJS這樣的適當的JS框架來從項目中獲得一些可重用性並獲得更好的結構化代碼。

在下一部分和最後一部分中,我們將專注於將我們的項目部署到實時互聯網。敬請關注!

======================================================================

分享一些以太坊、EOS、比特幣等區塊鏈相關的交互式在線編程實戰教程:

  • java以太坊開發教程,主要是針對java和android程序員進行區塊鏈以太坊開發的web3j詳解。
  • python以太坊,主要是針對python工程師使用web3.py進行區塊鏈以太坊開發的詳解。
  • php以太坊,主要是介紹使用php進行智能合約開發交互,進行賬號創建、交易、轉賬、代幣開發以及過濾器和交易等內容。
  • 以太坊入門教程,主要介紹智能合約與dapp應用開發,適合入門。
  • 以太坊開發進階教程,主要是介紹使用node.js、mongodb、區塊鏈、ipfs實現去中心化電商DApp實戰,適合進階。
  • C#以太坊,主要講解如何使用C#開發基於.Net的以太坊應用,包括賬戶管理、狀態與交易、智能合約開發與交互、過濾器和交易等。
  • EOS教程,本課程幫助你快速入門EOS區塊鏈去中心化應用的開發,內容涵蓋EOS工具鏈、賬戶與錢包、發行代幣、智能合約開發與部署、使用代碼與智能合約交互等核心知識點,最後綜合運用各知識點完成一個便籤DApp的開發。
  • java比特幣開發教程,本課程面向初學者,內容即涵蓋比特幣的核心概念,例如區塊鏈存儲、去中心化共識機制、密鑰與腳本、交易與UTXO等,同時也詳細講解如何在Java代碼中集成比特幣支持功能,例如創建地址、管理錢包、構造裸交易等,是Java工程師不可多得的比特幣開發學習課程。
  • php比特幣開發教程,本課程面向初學者,內容即涵蓋比特幣的核心概念,例如區塊鏈存儲、去中心化共識機制、密鑰與腳本、交易與UTXO等,同時也詳細講解如何在Php代碼中集成比特幣支持功能,例如創建地址、管理錢包、構造裸交易等,是Php工程師不可多得的比特幣開發學習課程。
  • tendermint區塊鏈開發詳解,本課程適合希望使用tendermint進行區塊鏈開發的工程師,課程內容即包括tendermint應用開發模型中的核心概念,例如ABCI接口、默克爾樹、多版本狀態庫等,也包括代幣發行等豐富的實操代碼,是go語言工程師快速入門區塊鏈開發的最佳選擇。

匯智網原創翻譯,轉載請標明出處。這裏是原文以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章