吧友們, 昨天的「百度貼吧」還差一個使用者介面, 程式碼都在這兒了...

買賣虛擬貨幣

來源 | Embark

編譯 | 王國璽

責編 | 喬治

出品 | 區塊鏈大本營(blockchain_camp)

本文目的很明確:手把手教你使用 DApp 開發框架 Embark 構建一個去中心化百度貼吧(文末附 GitHub 地址),主要包括以下 3 部分:

  1. 明確 DApp 需求,部署智慧合約;

  2. 使用 EmbarkJS 測試智慧合約;

  3. 使用 React 構建 DApp 的前端。

上一篇文章中,營長手把手帶你們使用 Solidity 語言部署合約,並使用 EmbarkJS 完成智慧合約測試,本文基於此繼續深入,使用 JavaScript 使用者介面框架 React 構建去中心化百度貼吧的前端。

渲染第一個元件

在構建與智慧合約例項互動的元件之前,我們需要先在螢幕上實際渲染一個簡單的文字,以確保 React 框架已經得到了正確的配置。

為此,我們需要將 React 框架新增為專案的依賴項。事實上,我們的程式碼依賴兩個程式包:react 和 react-dom。之所以需要 react-dom 是因為它可以在 DOM (Document Object Model,文件物件模型)環境中渲染使用 React 框架定義的元件,聽起來令人摸不著頭腦,簡單來說這就是瀏覽器所做的工作。

接下來我們需要將這兩個依賴項新增到專案的 package.json 中:

"dependencies": {"react": "^16.4.2","react-dom": "^16.4.2"}

完成後,我們需要實際安裝這些依賴項,我們只需要在終端中執行以下命令:

npm install

一切正常的話,現在我們就可以使用 React 框架了。由於 Embark 框架並不需要指定任何前端框架,因此我們不會過多關注 React 框架特有的屬性,僅僅完成構建應用程式的工作就已足矣。

在 React 框架中建立元件非常簡單。我們需要做的就是建立一個繼承了 React 的 Component (元件)型別的類,然後新增一個渲染函式 render() 來展示元件的檢視。

我們需要為專案中的所有元件建立一個資料夾:

mkdir app/js/components

接下來,我們需要為根元件建立一個檔案,我們簡單地把根元件命名為 App 並使用相同的檔名:

touch app/js/components/App.js

如前所述,我們需要在螢幕上渲染一些文字來確保 React 框架沒有出錯,也就是說,我們需要編寫以下程式碼:

import React, { Component } from'react';
exportclassAppextendsComponent{
render() {return<h2>DReddit</h2> }}

這些程式碼的可讀性還是很強的,幾乎可以做到自解釋(self explanatory)。在程式碼中我們匯入了 React 及其 Component(元件)型別,並建立了一個繼承 Component 元件的 App 類。React 框架將使用渲染函式 render() 來展示出元件的檢視,並且會返回用 JavaScript 語法拓展 JSX 編寫的模板。JSX 在語法上看起來很像 HTML,只是它帶有一些用來嵌入像控制結構這樣功能的額外語法,稍後我們會再使用它!

現在我們已經定義好了這個元件,接下來就需要告訴 React 框架來實際渲染這個元件。為此,我們需要轉到 app / js / index.js 檔案,並在其中新增以下程式碼:

import React from'react';import { render } from'react-dom';import { App } from'./components/App';
render(<App />, document.getElementById('root'));

由於 React 在當前這個指令碼範圍中還不可用,所以我們首先需要再次匯入 React,同時我們還需要從 react-dom 中匯入渲染函式 render(),渲染函式會幫助我們將根元件渲染到 HTML 文件的某個元素中。在這種情況下,我們想要渲染的根元件元素是那些顯示為根元件 root 的元素。

接下來我們來快速設定它,我們需要在 app / index.html 檔案中新增一個顯示為根元件 root 的新元素:

<body><divid="root"></div><scriptsrc="js/app.js"></script></body>

請注意,程式碼中在選擇了根元件 root 後,我們還更新了 script 標籤。這樣做是為了保證,我們在渲染函式 render()中指定的元素在指令碼執行時是實際可用的。

大功告成!接下來我們啟動 Embark 框架,此時螢幕上應該會出現剛剛定義的元件:

embark run

構建建立帖子元件 CreatePost 

上面的例子可能讓你對如何構建元件有了基本的瞭解,現在是時候構建真正有用的元件了。首先我們會構建一個使用者建立帖子時使用的元件。與上面定義的 App 元件類似,我們需要構建一個新的建立帖子元件 createPost,它帶有一個渲染函式 render()來展示輸入資料的簡單表單(form)。我們還需要向表單中新增事件處理程式,以便使用者在提交表單時,我們可以訪問到使用者提交的資料並將其傳送到智慧合約中。

建立一個表單非常簡單:

import React, { Component } from'react';
exportclassCreatePostextendsComponent{
render() {return (<form><div><label>Topic</label><inputtype="text"name="topic" /></div><div><textareaname="content"></textarea></div><button>Post</button></form> ) }}

為在螢幕上展示這個元件,我們需要將其定義為 App 元件的一部分。具體而言,就是讓 App 元件渲染建立帖子元件 CreatePost ,我們可以簡單地將它新增到 App 元件的渲染函式中:

import { CreatePost } from'./CreatePost';
exportclassAppextendsComponent{
render() {return (<React.Fragment><h2>DReddit</h2><CreatePost /></React.Fragment&> ) }}

React 框架不允許在單個元件檢視中使用多個根元件,因此我們需要用到 React.Fragment。顯然,除了我們剛才定義的靜態表單之外,根元件中沒有其他的渲染任務。

接下來我們繼續完善表單的功能。首先,我們需要確保輸入到表單中的資料在元件中可用。React 元件中的狀態物件 state 可以幫助解決這個問題。我們所要做的就是給它一些初始值來初始化它,並在需要時使用設定狀態函式 setState()來更新它。

我們可以在建立帖子元件 CreatePost 中透過使用建構函式來引入狀態物件 state ,相應的我們還可以直接初始化它:

exportclassCreatePostextendsComponent{
constructor(props) {super(props);this.state = {topic: '',content: '',loading: false }; } ...}

接下來,我們將該狀態繫結到表單欄位:

<form><div><label>Topic</label><inputtype="text"name="topic"value={this.state.topic} /></div><div><textareaname="content"value={this.state.content}></textarea></div><button>Post</button></form>

你可能會問程式碼中的 loading 是幹嘛的,彆著急,我們馬上會說到它。最後但同樣重要的是,我們需要新增一些事件處理程式,以便在使用者輸入資料時檢視中的更改能傳遞迴元件並更新元件的狀態。為了確保一切正常,我們還需要為表單提交新增一個事件處理程式,讓它輸出狀態物件 state 中的資料,換句話說,我們需要更新處理程式 handleChange()和建立帖子處理程式 createPost(),程式碼如下:

export classCreatePostextendsComponent {  ...  handleChange(field, event) {this.setState({      [field]: event.target.value    });  }
createPost(event) {event.preventDefault(); console.log(this.state); } ...}

請注意程式碼中更新處理程式 handleChange()的實現方式,我們在其中使用了設定狀態函式 setState()來更新傳遞給該函式的值。現在我們需要做的就是將這些處理程式附加到表單中:

<formonSubmit={e => createPost(e)}><div><label>Topic</label><inputtype="text"name="topic"value={this.state.topic}onChange={e => handleChange('topic', e)} /></div><div><textareaname="content"value={this.state.content}onChange={e => handleChange('content', e})></textarea></div><buttontype="submit">Post</button></form>

由於我們正在使用表單的 onSubmit()處理程式,因此很重要的一點就是將 type =“submit” 新增到按鈕物件 button 中,或將按鈕物件更改為 <input type =“submit”>,否則,表單將不會發出提交事件。

做完了這些,在提交表單時我們就能在控制檯中看到元件的狀態了!接下來最大的挑戰就是使用 EmbarkJS 和它的 API 實現元件與智慧合約例項的互動。

1、將資料上傳到 IPFS

回想一下我們剛才的定義, DReddit 中建立帖子函式 createPost()接收一些位元組作為帖子的描述,我們也討論了,這些位元組實際上並不是帖子自身的資料,而是能夠指向帖子資料的 IPFS 雜湊值。換句話說,我們必須以某種方式將資料上傳到 IPFS 中,並獲得這樣的雜湊值。

幸運的是,強大的 EmbarkJS 為我們提供了大量的 API 來實現這個功能!就比如說, EmbarkJS 的儲存文件函式 EmbarkJS.Storage.saveText()會把一段字串上傳到 IPFS 中並返回其雜湊值,然後我們可以透過智慧合約中的建立帖子函式 createPost()來用這個雜湊值建立一個帖子。需要注意的是,這些 API 是非同步的,與在測試中使用到的非同步操作相同,這裡我們將使用 async / await 方法以同步方式編寫非同步程式碼。

asynccreatePost(event) {event.preventDefault();this.setState({    loading: true  });
const ipfsHash = await EmbarkJS.Storage.saveText(JSON.stringify({ topic: this.state.topic, content: this.state.content }));
this.setState({ topic: '', content: '', loading: false });}

程式碼中我們使用了將 JavaScript 物件轉換為字串的函式 JSON.stringify(),我們使用它來得到所建立帖子的主題和內容。這也是我們第一次使用 loading。我們首先將 loading 設定為true,接著我們執行操作為等待更新的使用者渲染出有用的資訊,最後再將 loading 改回 false。

<formonSubmit={e => createPost(e)}>  ...  {this.state.loading && <p>Posting...</p>  }</form>

很顯然,到這裡我們還沒有完成這個功能。上面所做的只是將帖子的資料上傳到 IPFS 中並接收它的雜湊值,接下來我們需要實現透過智慧合約中的建立帖子函式 createPost()來用這個雜湊值建立一個帖子。

2、傳送交易以建立帖子

要將交易傳送到智慧合約中,我們可以再次使用 EmbarkJS 的 API。同時我們還需要一個以太坊賬戶來傳送交易。這並不難,我們可以使用 Embark 提供的以太坊節點來生成帳戶。

完成了這些後我們就可以估算交易的 gas 需求並透過交易傳送資料。獲得以太坊賬戶的方法如下所示,請注意此處我們也可以使用 async / await 非同步處理方法:

asynccreatePost(event) {  ...const accounts = await web3.eth.getAccounts();  ...}

接下來,我們將從 EmbarkJS 中匯入 DReddit 智慧合約例項,並估算交易的gas 需求。最後我們將使用獲得的賬戶和估算的交易燃料來實際發起交易:

import DReddit from'./artifacts/contracts/DReddit';...
asynccreatePost(event) { ...const accounts = await web3.eth.getAccounts();const createPost = DReddit.methods.createPost(web3.utils.toHex(ipfsHash));const estimate = await createPost.estimateGas();await createPost.send({from: accounts[0], gas: estimate}); ...}

到這裡我們的建立帖子函式 createPost()就全部完成了!雖然我們還沒有建立所有已建立帖子的列表,但我們已經能夠透過應用程式來建立帖子了,我們可以使用 Embark 框架檢查交易是否成功。在輸入命令 embark run 啟動執行後,終端中應該會顯示這樣的輸出:

Blockchain> DReddit.createPost("0x516d5452427a47415153504552614645534173335133765a6b59436633634143776368626263387575623434374e") | 0xbbeb9fa1eb4e3434c08b31409c137c2129de65eb335855620574c537b3004f29 |gas:136089| blk:18455 |status:0x1

構建帖子元件 Post 

DReddit 應用程式的下一個挑戰在於從智慧合約例項和 IPFS 中獲取所有建立的帖子,以便我們在螢幕上展示。我們先從最簡單的開始,首先建立一個只能展示一個帖子的新元件,之後,我們將根據所獲取的資料動態地展示帖子列表。

同樣的,我們只關注正確地實現核心功能,因此我們的應用程式看起來不會特別好看。從需求上來講,帖子元件 Post 需要分別展示帖子的主題、內容、所有者、建立日期,以及好評差評的投票按鈕。

這是一個元件的基本模板:

import React, { Component } from'react';
exportclassPostextendsComponent{
render() {return (<React.Fragment><hr /><h3>Some Topic</h3><p>This is the content of a post</p><p><small><i>created at 2019-02-18 by 0x00000000000000</i></small></p><button>Upvote</button><button>Downvote</button></React.Fragment> ) }}

有很多種方法都可以用來實現資料的動態展示。通常,我們可以將一個或多個屬性傳遞給帖子元件 Post,這個元件表示整個帖子物件,它的渲染函式 render()可以實現資料的動態展示。但是在這裡,我們將選擇一個稍微不同的實現方法。我們將透過帖子元件 Post 接收儲存在智慧合約中的 IPFS 雜湊值並讓它自己解析資料。

為了保證智慧合約和元件中的各功能命名一致,我們將元件中想要儲存的資料也叫做描述。然後我們可以使用資料獲取函式 EmbarkJS.Storage.get()來獲取 IPFS 雜湊值對應的資料,也就是實際的帖子資料。為了在帖子元件 Post 的檢視中展示資料,我們將對剛才獲取的資料進行解析並相應地使用設定狀態函式 setState()。

為了確保在元件準備就緒之後這些操作都能正常執行,我們把這些操作都放在 componentDidMount()生命週期鉤子函式(life cycle hook)中執行:

import React, { Component } from'react';import EmbarkJS from'.artifacts/embarkjs';
exportclassPostextendsComponent{
constructor(props) {super(props);this.state = {topic: '',content: '' }; }
async componentDidMount() {const ipfsHash = web3.utils.toAscii(this.props.description);const data = await EmbarkJS.Storage.get(ipfsHash);const { topic, content } = JSON.parse(data);
this.setState({ topic, content }); } ...}

這裡需要強調的一點是:在頁面載入時呼叫資料獲取函式 EmbarkJS.Storage.get()或其他任何 EmbarkJS 函式可能會失敗,因為此時儲存系統可能還尚未完全初始化。不過這對於資料上傳函式 EmbarkJS.Storage.uploadText()來說並不是問題,因為我們只能在 Embark 框架初始化完成後呼叫了它。

不過,從理論上來講,建立一個帖子時可能會存在競爭條件(race condition,是指裝置或系統出現不恰當的執行時序,因而得到不正確的結果)。為了確保 EmbarkJS 在任何時間點都能準備就緒,我們將使用到判斷是否準備就緒的鉤子函式 onReady()。一旦 EmbarkJS 準備就緒,EmbarkJS.onReady()就會執行一次呼叫,在這裡被調函式的最佳選擇就是應用程式的渲染函式,所以我們在 Embark 框架的 onReady() 函式中呼叫渲染函式 render() 來渲染 App 元件。

EmbarkJS.onReady(() => {  render(<App />, document.getElementById('root'));});

這也意味著我們的應用程式只會在 EmbarkJS 準備就緒時執行渲染,展示資料。從理論上來說,這樣做等待的時間可能會變長,但就我們這個 DReddit 應用程式而言,造成影響的可能性不大。

我們還需要新增帖子所有者和帖子建立日期。按照預期,所有者和建立日期都將作為帖子的屬性被記錄下來。我們只需要以使用者可以理解的方式對資料進行格式化,展示所有者並不會有什麼問題,但要以人類可讀的形式展示日期就需要安裝並匯入日期格式庫 dateformat,安裝的操作如下所示:

npm install--save dateformat

安裝完成後,我們需要更新帖子元件 Post 的渲染函式 render(),將得到的帖子建立日期 creationDate 轉換成人類可讀的形式。

...import dateformat from'dateformat';
exportclassPostextendsComponent{ ... render() {const formattedDate = dateformat(newDate(this.props.creationDate * 1000),'yyyy-mm-dd HH:MM:ss' );return (<React.Fragment><hr /><h3>{this.state.topic}</h3><p>{this.state.content}</p><p><small><i>created at {formattedDate} by {this.props.owner}</i></small></p><button>Upvote</button><button>Downvote</button></React.Fragment> ) }}

請注意,在渲染函式 render() 中建立的變數可以任意地新增資料,所以我們不需要讓它們在 props (React 用來在元件之間傳遞值的一種物件)或狀態物件 state 上可用。事實上, React 框架預設 props 物件都是隻讀的(read only,即不可修改)。

我們可以試著將一些資料新增到 App 元件檢視中來測試一下新的帖子元件 Post。接下來,我們將透過從智慧合約中提取帖子來實現這個功能。

需要注意的是,這個程式碼片段中的雜湊值是我所儲存資料的雜湊值,因而它在你的本地 IPFS 節點中是不可用的,你需要將它替換成你資料的雜湊值。具體而言,你只需要記錄資料上傳至 IPFS 時返回的雜湊值並將其轉換為十六進位制。

export class App extends Component {
render() { return (<React.Fragment><h2>DReddit</h2><CreatePost /><Postdescription="0x516d655338444b53464546725369656a747751426d683377626b56707566335770636e4c715978726b516e4b5250"creationDate="1550073772"owner="0x00000000000" /></React.Fragment> ) }}

構建帖子列表元件 List

在構建展示帖子列表的元件之前,我們必須想辦法來最佳化智慧合約。目前我們還沒有一個很好的方法從智慧合約中獲取陣列資料,也就是說要實現帖子的列表展示功能我們需要逐個獲取帖子的資料。為此,我們需要獲取帖子的總個數並透過迭代來索引所有的帖子,從而實現對每個帖子的獲取。

我們需要在 DReddit 智慧合約中引入一個判斷帖子個數的函式 numPosts():

functionnumPosts() publicviewreturns (uint) {return posts.length;}

當我們新增帖子時,帖子個數 posts.length 會相應的增加,因此我們可以把它用做讀取帖子時的索引。當然了,如果願意的話你也可以寫一個測試驗證一下它的正確性!

有了這個,我們就可以開始構建帖子列表元件 List 了。List 元件維護著一個要在螢幕上展示的帖子列表,我們可以從最簡單的功能開始再一步步深入,具體程式碼如下:

import React, { Component } from 'react';
export classListextendsComponent{
constructor(props) {super(props);this.state = { posts: [] }; }
render() {return (<React.Fragment> {this.state.posts.map(post => {return ( <Post key={post.id} description={post.description} creationDate={post.creationDate} owner={post.owner} />) })} </React.Fragment> ) }}

這裡最有趣的部分就是渲染函式 render(),程式碼中我們遍歷了所有的 state.posts (目前為空),然後在每次迭代中渲染一個帖子元件 Post。另一個需要注意的點是,每個帖子元件 Post 都會收到一個鍵值 key, React 框架在迴圈建立檢視時需要用到這個鍵值。你可能會發現到目前為止我們還沒用過帖子的序號 post.id,不要擔心,我們馬上就會用到它。

現在我們已經可以將帖子列表元件 List 放在 App 元件中了。但現在它還不會展示任何內容,因為我們還沒有釋出任何帖子,我們接下來就要做這個工作。

import { List } from'./List';
exportclassAppextendsComponent{
render() {return (<React.Fragment><h2>DReddit</h2><CreatePost /><List /></React.Fragment> ) }}

a)獲取帖子資料

如前所述,我們將使用智慧合約的判斷帖子個數函式 numPosts()來獲取帖子的總數。然後我們將帖子總數作為索引來迭代單獨訪問每個帖子。這個邏輯應該在帖子列表元件 List 準備就緒後執行,因此我們需要在 List 元件的定義之後加入 componentDidMount()函式:

exportclassListextendsComponent{  ...  async componentDidMount() {const totalPosts = await DReddit.methods.numPosts().call();
let list = [];
for (let i = 0; i < totalPosts; i++) {const post = DReddit.methods.posts(i).call(); list.push(post); }
list = awaitPromise.all(list); } ...}

請注意,在上面的程式碼中,我們並沒有用 await 語句來等待每次對帖子的呼叫。這是故意為之,因為我們不可能等待每一個承諾的完成,所以我們會收集所有需要的承諾,然後使用 Promise.all()函式一次性解決所有這些承諾。

最後但同樣重要的是,前面也提到了我們需要為每個帖子新增一個 id 屬性。我們可以簡單地遍歷所有帖子並將帖子的索引賦值給 id。這些操作完成後,我們可以使用設定狀態函式 setState()來更新元件的狀態並展示列表:

async componentDidMount(){  ...list = list.map((post, index) => {    post.id = index;return post;  });
this.setState({ posts: list });}

到現在為止,我們的 DReddit 應用程式已經可以展示所有已建立帖子的列表。但遺憾的是,在新增新帖子時,它並不會自動重新載入帖子。因此,我們必須在每次新增帖子後重新整理瀏覽器,這樣做十分影響使用者體驗,我們現在需要解決這個問題。

b)重新載入帖子

我們有多種不同的方法來實現帖子列表的重新載入,最簡單的一種就是讓建立帖子元件 createPost 告訴帖子列表元件 List 重新載入帖子。但是,我們構建的這個 React 應用程式並沒有設定通訊層,所以最直接的方法就是更改建立帖子元件 CreatePost 和帖子列表元件 List 的父元件(在這裡就是 App 元件)中載入帖子的邏輯,讓這個父元件把邏輯傳遞到需要它的地方。這也意味著我們將把獲取帖子列表的功能放在 App 元件中,帖子列表元件 List 僅僅接收傳遞過來的純資料。

這個實現方法聽起來很繞,但不用擔心,在程式碼中實現它並不難!我們首先需要在 App 元件中定義一個讀取帖子函式 loadPosts(),然後基本上我們需要把帖子列表元件 List 中 componentDidMount()函式的所有功能都移動到 App 元件中:

exportclassAppextendsComponent {  ...async loadPosts(){const totalPosts = await DReddit.methods.numPosts().call();
let list = [];
if (totalPosts > 0) {for (let i = 0; i < totalPosts; i++) {const post = DReddit.methods.posts(i).call();list.push(post); } }
list = await Promise.all(list);list = list.map((post, index) => { post.id = index;return post; });
list;
this.setState({ posts: list }); }}

為了完成這項工作,我們還需要引入一個帖子的狀態,這樣就可以確保 App 元件在掛載時會呼叫讀取帖子函式 loadPosts():

exportclassAppextendsComponent{
constructor(props) {super(props);this.state = {posts: [] }; }
async componentDidMount() {awaitthis.loadPosts(); } ...}

最後但同樣重要的是,我們需要將帖子傳遞給帖子列表元件 List 並將載入帖子函式 loadPosts()傳遞給建立帖子元件 CreatePost 作為回撥處理程式:

render() {  return (<React.Fragment><h2>DReddit</h2><CreatePostafterPostHandler={this.loadPosts.bind(this)}/><Listposts={this.state.posts}/></React.Fragment>  )}

完成後,我們可以分別從 this.props 中獲取帖子和帖子建立後處理函式 afterPostHandler()。這個功能將在帖子列表元件 List 的渲染函式 render()中實現(注意我們不再需要狀態物件 this.state 了):

render() {return (<React.Fragment>    {this.props.posts.map(post => {      ...    })}    </React.Fragment>  )}

然後在建立帖子元件 CreatePost 中,我們需要在建立帖子後呼叫帖子建立後處理函式afterPostHandler():

asynccreatePost(event) {  ...await createPost.send({from: accounts[0], gas: estimate});awaitthis.props.afterPostHandler();
this.setState({ topic: '', content: '', loading: false });}

有了這些功能。在新建立帖子時,帖子列表會自動重新載入,你大可去試一試。

新增投票功能

我們將要實現的最後一個功能就是對帖子進行好評還是差評的投票。這需要我們回到剛剛建立的帖子元件 Post 中進行更改,首先我們必須明確此處更改要實現的功能:

  • 展示每個帖子的好評數和差評數;

  • 為使用者分別新增處理好評投票和差評投票的處理程式;

  • 確定使用者是否可以對帖子進行投票。

a)渲染帖子的票數

第一個功能是其中最瑣碎的一個,所以我們先來進行它的攻關。雖然 DReddit 智慧合約返回的資料中已經附加了好評數和差評數,但它的格式並不正確,因為智慧合約返回的資料是字串形式。接下來我們需要擴充套件 App 元件的載入帖子函式 loadPosts()來實現對帖子好評數和差評數的解析,程式碼如下:

async loadPosts() {  ...  list = list.map((post, index) => {    post.id = index;    post.upvotes = parseInt(post.upvotes, 10);    post.downvotes = parseInt(post.downvotes, 10);return post;  });  ...}

完成後,我們可以透過帖子列表元件 List 中的 props 物件將每個帖子的好評數和差評數傳遞給每個帖子元件 Post :

export class List extends Component {  ...  render() {    return (<React.Fragment>      {this.props.posts.map(post => {        return (<Postkey={post.id}description={post.description}creationDate={post.creationDate}upvotes={post.upvotes}downvotes={post.downvotes}owner={post.owner}          />)      })}</React.Fragment>    )  }}

展示好評數和差評數的功能實際上只需要在帖子元件 Post 的渲染函式 render()中插入資料。程式碼中我們將資料新增到按鈕旁邊,你可以隨意將它們放在其他位置:

exportclassPostextendsComponent{  ...  render() {    ...    return (<React.Fragment>        ...        {this.props.upvotes} <button>Upvote</button>        {this.props.downvotes} <button>Downvote</button></React.Fragment>    )  }}

b)實現好評差評投票

與建立新帖子類似,對帖子進行好評差評投票也需要傳送交易到 DReddit 智慧合約。因此,我們將執行與建立帖子元件 CreatePost 中幾乎相同的操作,唯一的區別就是在這裡我們呼叫的是智慧合約的投票函式 vote()。你應該還記得,投票函式 vote()接收兩個引數,帖子序號 post id 和投票型別 Ballot,具體而言就是沒有投票 NONE,好評 UPVOTE 或差評 DOWNVOTE,它的儲存格式為 8 位無符號整型 uint8。

上文提到過,在這個應用中不同部分(智慧合約、前端元件)的變數都有著相同的表示,這樣會大大減小出錯的可能,對於前端元件中的投票元件,我們仍使用 0、1、2 這三個數字來表示沒有投票 NONE,好評 UPVOTE 或差評 DOWNVOTE,但問題在於 JavaScript 中不支援列舉資料結構,因此在前端元件中我們需要使用雜湊物件作為替代:

constBALLOT = {NONE: 0,UPVOTE: 1,DOWNVOTE: 2}

實際上,我們的帖子元件 Post 中並沒有加入帖子序號 post id,不過將帖子序號 post id 新增到帖子列表元件 List 中並不是什麼難事,現在你應該知道該怎麼做了!

我們需要分別在好評投票按鈕和差評投票按鈕上新增點選處理程式,然後再將我們在投票型別 BALLOT 中定義的好評投票和差評投票傳遞給它們(請注意,投票型別中的沒有投票 None 只是為了保證程式邏輯的完整性,但實際上在程式碼中我們並沒有使用它):

<buttononClick={e => this.vote(BALLOT.UPVOTE)}>Upvote</button><buttononClick={e => this.vote(BALLOT.DOWNVOTE)}>Downvote</button>

接下來,我們需要將該投票型別以及所投的帖子序號 post id 傳送到智慧合約之中。

asyncvote(ballot) {const accounts = await web3.eth.getAccounts();const vote = DReddit.methods.vote(this.props.id, ballot);const estimate = await vote.estimateGas();
await vote.send({from: accounts[0], gas: estimate});}

我們還希望在成功傳送投票後更新檢視。我們需要透過帖子的 props 物件獲取帖子的好評差評投票並相應地渲染它們。但是,如果在接收到投票後立刻更新這些值就好了。為此,我們需要更改程式碼,讓它只讀取一次來自 props 物件的好評差評投票並將它們儲存在元件的狀態中。

exportclassPostextendsComponent{
constructor(props) {super(props);this.state = {topic: '',content: '',upvotes: this.props.upvotes,downvotes: this.props.downvotes }; } ...}

同時我們還需要更改元件的渲染函式 render(),讓它從元件狀態中讀取資料而不是從 props 物件中:

render() {  ...  return (<React.Fragment>      ...      {this.state.upvotes} <button...>Upvote</button>      {this.state.downvotes} <button...>Downvote</button></React.Fragment>  )}

這樣,我們就可以在投票發起後立即使用設定狀態函式 setState()來更新狀態:

async vote(ballot) {  ...this.setState({    upvotes: this.state.upvotes + (ballot == BALLOT.UPVOTE ? 1 : 0),    downvotes: this.state.downvotes + (ballot == BALLOT.DOWNVOTE ? 1 : 0)  });}

大功告成,我們現在可以對帖子進行好評差評投票,且對每個帖子只能投票一次,沒錯,當我們對一個帖子多次投票時,程式會報錯。這是因為,我們在智慧合約中加入了一項限制條件,確保使用者無法對已經投票或還未建立的帖子進行好評差評投票。

成功近在眼前,最後我們只需要將這個投票限制邏輯加入前端程式中。

c)使用函式 canVote() 禁用投票按鈕

這個投票限制邏輯實現起來非常簡單。如果使用者不能對帖子投票,我們只需要禁用投票按鈕。我們可以透過呼叫智慧合約中能否投票函式 canVote()來確定使用者能否進行投票。同時,我們還需要考慮到,如果使用者已經對一個帖子進行了投票,只是這筆包含投票的交易還未被加入到區塊鏈中,也就是說此時投票尚未完成,這時我們不應該允許使用者對該帖子再次投票。

在程式碼中,這個功能對應於投票是否正在提交(submitting)的狀態。一般來說,如果一個使用者之前沒有對某個帖子投票,並且他此時沒有在提交對該帖子的投票,那麼他就可以對該帖子投票:

exportclassPostextendsComponent{
constructor(props) {super(props);this.state = {topic: '',content: '',upvotes: this.props.upvotes,downvotes: this.props.downvotes,canVote: true,submitting: false }; } ...}

接下來,我們需要更新帖子元件 Post 的渲染函式 render(),以便在使用者不能對帖子投票時禁用投票按鈕:

render() {  ...  const disabled = this.state.submitting || !this.state.canVote;return (<React.Fragment>      ...      {this.state.upvotes} <buttondisabled={disabled}...>Upvote</button>      {this.state.downvotes} <buttondisabled={disabled}...>Downvote</button></React.Fragment>  )}

最後但同樣重要的是,我們必須確保元件的狀態也做出相應的更新。當一個帖子初始化時,我們將呼叫智慧合約中函式 canVote():

exportclassPostextendsComponent{  ...  async componentDidMount() {    ...    const canVote = await DReddit.methods.canVote(this.props.id).call();this.setState({ topic, content, canVote });  }  ...}

在進行投票時,我們在傳送投票所在的交易之前要先將正在提交狀態 submitting 設定為是(true),並在交易完成後再將其改為否(false),由於此時已經完成了對帖子的投票,因此能否投票狀態 canVote 也應該被設定為否(false):

asyncvote(ballot) {  ...this.setState({ submitting: true });await vote.send({from: accounts[0], gas: estimate + 1000});
this.setState({ ... canVote: false, submitting: false });}

Bingo!執行一下程式碼看看效果吧!

一些建議

上述所實現的功能只是百度貼吧提供功能的冰山一角,因此,我們還可以在很多地方做出改進和最佳化,以下是我的一些建議:

  • 按照反向的時間順序對帖子進行排序,以便最新提交的帖子始終位於頁面頂部;

  • 透過智慧合約事件實現帖子列表的重新載入;

  • 引入路由,以便不同使用者在建立和檢視帖子時有不同的檢視;

  • 使用 CSS(層疊樣式表)來美化應用程式的檢視;

透過使用 IPFS 和智慧合約組合開發一款去中心化應用並不是難事,更多功能等你去挖掘喲。

GitHub 地址:

https://github.com/embark-framework/dreddit-tutorial

原文地址:

1、Setting up the project and implementing a Smart Contract

https://embark.status.im/news/2019/02/04/building-a-decentralized-reddit-with-embark-part-1/

2、Testing the Smart Contract through EmbarkJS

https://embark.status.im/news/2019/02/11/building-a-decentralized-reddit-with-embark-part-2/

3、Building a simple front-end using React

https://embark.status.im/news/2019/02/18/building-a-decentralized-reddit-with-embark-part-3/

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;