Skip to main content

使用 React 開發 DApp

就前面的經驗,我們理解一個DApp專案通常包含了智能合約網頁前端兩個部份。兩者之間僅透過ABI來互動。因此,在之後的章節裡,我們的專案都會包含兩個獨立的資料夾,分別放置智能合約(contract)與網頁前端(web)的部份。

graph LR subgraph 使用者 瀏覽器[DApp相容瀏覽器] 加密代幣錢包 subgraph 前端 網頁[網頁應用] end end subgraph ethereum Contract[智能合約] end 網頁 --> 加密代幣錢包 加密代幣錢包 -- ABI --> Contract Contract -- ABI --> 網頁 網頁 --- 瀏覽器

建立智能合約資料夾

這邊一樣使用 truffle 來建立智能合約資料夾。首先建立hello_react_dapp專案資料夾。

$ mkdir hello_react_dapp
$ cd hello_react_dapp

接著在專案資料夾中建立contracts資料夾。

$ mkdir contracts
$ cd contracts
$ truffle init

從其他範例中把contracts/HelloToken.solmigrations/4_deploy_hellotoken.jspackage.jsontruffle.js這幾個檔案複製到hello_react_dapp/contracts資料夾下,再執行npm install命令安裝相關函式庫。安裝好後可以使用truffle compiletruffle migrate命令編譯或部署HelloToken合約。

$ npm install
$ truffle migrate

在編譯或部署合約後,需記下HelloToken合約部署的地址。此外,在build/contracts資料夾下可以看到產生的HelloToken.json檔。裡面包含HelloToken.json智能合約的ABI。這兩個資料在DApp中將會用到。

{% mermaid %} graph LR subgraph 本地開發機器 cotracts/cotracts/**.sol -- compile --> bytecode[Contract Bytecode] end

subgraph ethereum Contract end

bytecode -- migrate --> Contract {% endmermaid %}

在 MetaMask 中導入代幣

照前面章節說明在MetaMask中加入ganache的預設帳戶,並輸入合約地址以加入自訂的HelloToekn代幣。

Imgur

建立 DApp資料夾

建立hello_react_dapp資料夾

$ mkdir hello_react_dapp
$ cd hello_react_dapp

使用npx create-react-app命令來建立web資料夾。Ethjs(支援promise且相對穩定)。

$ npx create-react-app web
$ cd web
$ npm install --save ethjs
$ npm install

這時已可使用npm start命令開啟預設網頁(但還沒連接到智能合約)。

npm start

執行npm start命令後會在本機開啟一個網頁伺服器,並會在修改web資料夾中的內容時自動再次編譯。完成時可使用npm build命令輸出網頁成品。

npxnpm提供的新功能。過去要執行create-react-appgulp等指令時,需要先使用npm install -g xxx 將這些指令安裝到 global namespace。對於create-react-app這種產生器(generator)性質的命令,由於只有在建立新專案時會用到。因為create-react-app更新頻繁,下次不知什麼時候要用到時,又要重新升級一次才能使用到最新的功能。現在用npx可以即時下載並執行 node 指令,而不需要預先安裝。

{% mermaid %} graph LR

subgraph 使用者 瀏覽器[DApp相容瀏覽器] 加密代幣錢包 subgraph 前端 網頁[網頁應用] end end

subgraph ethereum Contract[智能合約] end

subgraph 本地開發機器 web/src/** -- build --> 網頁1[網頁] end

網頁 --> 加密代幣錢包 加密代幣錢包 -- ABI --> Contract Contract -- ABI --> 網頁 網頁 --- 瀏覽器 網頁1 --> 網頁 {% endmermaid %}

透過網頁連接智能合約

透過網頁連接智能合約需要提供三個參數:合約所在的網路,合約部署的地址,合約的ABI。

將編譯好的 contracts/build/contracts/HelloToken.json複製到web/src/lib/contracts/HelloToken.json中。並在src目錄下建立constants.js檔案,將部署好的HelloToken地址複製過來。

export const CONTRACT_ADDRESS = '0x345cA3e014Aaf5dcA488057592ee47305D9B3e10';

連上乙太坊網路

npm install --save ethjs

建立 src/web3utils.js 檔案,內容如下

// ethjs wrap
import Eth from 'ethjs'

let web3 = null;
let accounts = [];

if (typeof window.web3 !== 'undefined') {
web3 = new Eth(window.web3.currentProvider);

// get accounts
web3.accounts().then(accs => {
accounts = accs;
});
} else {
console.error('No web3? You should consider trying MetaMask!');
}

export {
accounts,
web3
}

講解

if (typeof window.web3 !== 'undefined') {}

當網頁中存在window.web3物件,可假設此瀏覽器支援DApp。

web3 = new Eth(window.web3.currentProvider);

這時我們可透過web3.jsEthjs協助我們連到瀏覽器/MetaMask擴充功能套件當前所連接的網路(即window.web3.currentProvider)。

web3.accounts().then(accs => {
accounts = accs;
});

因為所有會改變區塊鏈上狀態的交易都需要附上來源帳戶地址,因此在這邊順便取得本機的所有帳戶。

export {
accounts,
web3
}

最後匯出accountsweb3,在component中可透過import { accounts, web3 } from '../web3utils'取得web3和本機的所有帳戶。

App.js

App.js內容如下:

import React, { Component } from 'react';
import './App.css';

import { accounts, web3 } from './web3utils';
import {CONTRACT_ADDRESS} from './constants';
import CONTRACT_JSON from './lib/contracts/HelloToken.json';

class App extends Component {
constructor(props) {
super(props);
this.state = {
account: '',
balance: 0,
status: ''
};
}

setStateAsync(state) {
return new Promise((resolve) => {
this.setState(state, resolve)
});
}

async componentWillMount() {
try {
if (accounts.length === 0) {
this.setStateAsync({status: 'There was an error fetching your accounts.'});
return;
}

let account = accounts[0];
let token = eth.contract(CONTRACT_JSON.abi).at(CONTRACT_ADDRESS);
let balance = await token.balanceOf(account, {from: account});
this.setStateAsync({account, balance: balance.balance.toNumber() / 100});
} catch(err) {
this.setStateAsync({status: err});
}
}

render() {
return (
<div className="App">
<header className="App-header">
<h1 className="App-title">
{this.state.status ? this.state.status : `Balance: ${this.state.balance} H@`}
</h1>
</header>
<p className="App-intro">
Account: {this.state.account}<br/>
</p>
</div>
);
}
}

export default App;

講解

import { accounts, web3 } from './web3utils';

取得web3和本機的所有帳戶。

import {CONTRACT_ADDRESS} from './constants';
import CONTRACT_JSON from './lib/contracts/HelloToken.json';

取得合約地址和從truffle編譯得到的JSON資料。

class App extends Component {
constructor(props) {}
componentWillMount()
render()
}

透過 class 語句宣告 React 元件(component)。在constructor建構函式中加入預設值。在render函式中定義網頁介面來展示帳戶與餘額。componentWillMount是React提供的預設lifecycle函式之一4,可在元件實際顯示前預先執行一些動作。

let token = eth.contract(CONTRACT_JSON.abi).at(CONTRACT_ADDRESS);

JSON資料中存有此合約的ABI。可透過eth.contract函式將ABI轉換成易於使用的API。透過at可設定呼叫此合約的地址。

let balance = await token.balanceOf(account, {from: account});
this.setStateAsync({account, balance: balance.balance.toNumber() / 100});

取得帳戶與帳戶餘額。

完成畫面

Imgur

參考資料