昨天的区块链「百度贴吧」还差一个用户界面 代码都在这儿
本文目的很明确:手把手教你使用 DApp 开发框架 Embark 构建一个去中心化百度贴吧(文末附 GitHub 地址),主要包括以下 3 部分:
明确 DApp 需求,部署智能合约;
使用 EmbarkJS 测试智能合约;
使用 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';export class App extends Component { render() { returnDReddit
}}
这些代码的可读性还是很强的,几乎可以做到自解释(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(, document.getElementById('root'));
由于 React 在当前这个脚本范围中还不可用,所以我们首先需要再次导入 React,同时我们还需要从 react-dom 中导入渲染函数 render(),渲染函数会帮助我们将根组件渲染到 HTML 文档的某个元素中。在这种情况下,我们想要渲染的根组件元素是那些显示为根组件 root 的元素。
接下来我们来快速设置它,我们需要在 app / index.html 文件中添加一个显示为根组件 root 的新元素:
请注意,代码中在选择了根组件 root 后,我们还更新了 script 标签。这样做是为了保证,我们在渲染函数 render()中指定的元素在脚本执行时是实际可用的。
大功告成!接下来我们启动 Embark 框架,此时屏幕上应该会出现刚刚定义的组件:
embark run
构建创建帖子组件 CreatePost
上面的例子可能让你对如何构建组件有了基本的了解,现在是时候构建真正有用的组件了。首先我们会构建一个用户创建帖子时使用的组件。与上面定义的 App 组件类似,我们需要构建一个新的创建帖子组件 createPost,它带有一个渲染函数 render()来展示输入数据的简单表单(form)。我们还需要向表单中添加事件处理程序,以便用户在提交表单时,我们可以访问到用户提交的数据并将其发送到智能合约中。
创建一个表单非常简单:
import React, { Component } from 'react';export class CreatePost extends Component { render() { return () }}
为在屏幕上展示这个组件,我们需要将其定义为 App 组件的一部分。具体而言,就是让 App 组件渲染创建帖子组件 CreatePost ,我们可以简单地将它添加到 App 组件的渲染函数中:
import { CreatePost } from './CreatePost';export class App extends Component { render() { return () }} DReddit
React 框架不允许在单个组件视图中使用多个根组件,因此我们需要用到 React.Fragment。显然,除了我们刚才定义的静态表单之外,根组件中没有其他的渲染任务。
接下来我们继续完善表单的功能。首先,我们需要确保输入到表单中的数据在组件中可用。React 组件中的状态对象 state 可以帮助解决这个问题。我们所要做的就是给它一些初始值来初始化它,并在需要时使用设置状态函数 setState()来更新它。
我们可以在创建帖子组件 CreatePost 中通过使用构造函数来引入状态对象 state ,相应的我们还可以直接初始化它:
export class CreatePost extends Component { constructor(props) { super(props); this.state = { topic: '', content: '', loading: false }; } ...}
接下来,我们将该状态绑定到表单字段:
你可能会问代码中的 loading 是干嘛的,别着急,我们马上会说到它。最后但同样重要的是,我们需要添加一些事件处理程序,以便在用户输入数据时视图中的更改能传递回组件并更新组件的状态。为了确保一切正常,我们还需要为表单提交添加一个事件处理程序,让它输出状态对象 state 中的数据,换句话说,我们需要更新处理程序 handleChange()和创建帖子处理程序 createPost(),代码如下:
export class CreatePost extends Component { ... handleChange(field, event) { this.setState({ [field]: event.target.value }); } createPost(event) { event.preventDefault(); console.log(this.state); } ...}
请注意代码中更新处理程序 handleChange()的实现方式,我们在其中使用了设置状态函数 setState()来更新传递给该函数的值。现在我们需要做的就是将这些处理程序附加到表单中:
由于我们正在使用表单的 onSubmit()处理程序,因此很重要的一点就是将 type =“submit” 添加到按钮对象 button 中,或将按钮对象更改为 ,否则,表单将不会发出提交事件。
做完了这些,在提交表单时我们就能在控制台中看到组件的状态了!接下来最大的挑战就是使用 EmbarkJS 和它的 API 实现组件与智能合约实例的交互。
1、将数据上传到 IPFS
回想一下我们刚才的定义, DReddit 中创建帖子函数 createPost()接收一些字节作为帖子的描述,我们也讨论了,这些字节实际上并不是帖子自身的数据,而是能够指向帖子数据的 IPFS 哈希值。换句话说,我们必须以某种方式将数据上传到 IPFS 中,并获得这样的哈希值。
幸运的是,强大的 EmbarkJS 为我们提供了大量的 API 来实现这个功能!就比如说, EmbarkJS 的存储文档函数 EmbarkJS.Storage.saveText()会把一段字符串上传到 IPFS 中并返回其哈希值,然后我们可以通过智能合约中的创建帖子函数 createPost()来用这个哈希值创建一个帖子。需要注意的是,这些 API 是异步的,与在测试中使用到的异步操作相同,这里我们将使用 async / await 方法以同步方式编写异步代码。
async createPost(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。
很显然,到这里我们还没有完成这个功能。上面所做的只是将帖子的数据上传到 IPFS 中并接收它的哈希值,接下来我们需要实现通过智能合约中的创建帖子函数 createPost()来用这个哈希值创建一个帖子。
2、发送交易以创建帖子
要将交易发送到智能合约中,我们可以再次使用 EmbarkJS 的 API。同时我们还需要一个以太坊账户来发送交易。这并不难,我们可以使用 Embark 提供的以太坊节点来生成帐户。
完成了这些后我们就可以估算交易的 gas 需求并通过交易发送数据。获得以太坊账户的方法如下所示,请注意此处我们也可以使用 async / await 异步处理方法:
async createPost(event) { ... const accounts = await web3.eth.getAccounts(); ...}
接下来,我们将从 EmbarkJS 中导入 DReddit 智能合约实例,并估算交易的gas 需求。最后我们将使用获得的账户和估算的交易燃料来实际发起交易:
import DReddit from './artifacts/contracts/DReddit';...async createPost(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';export class Post extends Component { render() { return () }}
Some Topic
This is the content of a post
created at 2019-02-18 by 0x00000000000000
有很多种方法都可以用来实现数据的动态展示。通常,我们可以将一个或多个属性传递给帖子组件 Post,这个组件表示整个帖子对象,它的渲染函数 render()可以实现数据的动态展示。但是在这里,我们将选择一个稍微不同的实现方法。我们将通过帖子组件 Post 接收存储在智能合约中的 IPFS 哈希值并让它自己解析数据。
为了保证智能合约和组件中的各功能命名一致,我们将组件中想要存储的数据也叫做描述。然后我们可以使用数据获取函数 EmbarkJS.Storage.get()来获取 IPFS 哈希值对应的数据,也就是实际的帖子数据。为了在帖子组件 Post 的视图中展示数据,我们将对刚才获取的数据进行解析并相应地使用设置状态函数 setState()。
为了确保在组件准备就绪之后这些操作都能正常运行,我们把这些操作都放在 componentDidMount()生命周期钩子函数(life cycle hook)中执行:
import React, { Component } from 'react';import EmbarkJS from '.artifacts/embarkjs';export class Post extends Component { 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(, document.getElementById('root'));});
这也意味着我们的应用程序只会在 EmbarkJS 准备就绪时执行渲染,展示数据。从理论上来说,这样做等待的时间可能会变长,但就我们这个 DReddit 应用程序而言,造成影响的可能性不大。
我们还需要添加帖子所有者和帖子创建日期。按照预期,所有者和创建日期都将作为帖子的属性被记录下来。我们只需要以用户可以理解的方式对数据进行格式化,展示所有者并不会有什么问题,但要以人类可读的形式展示日期就需要安装并导入日期格式库 dateformat,安装的操作如下所示:
npm install --save dateformat
安装完成后,我们需要更新帖子组件 Post 的渲染函数 render(),将得到的帖子创建日期 creationDate 转换成人类可读的形式。
...import dateformat from 'dateformat';export class Post extends Component { ... render() { const formattedDate = dateformat( new Date(this.props.creationDate * 1000), 'yyyy-mm-dd HH:MM:ss' ); return () }}
{this.state.topic}
{this.state.content}
created at {formattedDate} by {this.props.owner}
请注意,在渲染函数 render() 中创建的变量可以任意地添加数据,所以我们不需要让它们在 props (React 用来在组件之间传递值的一种对象)或状态对象 state 上可用。事实上, React 框架默认 props 对象都是只读的(read only,即不可修改)。
我们可以试着将一些数据添加到 App 组件视图中来测试一下新的帖子组件 Post。接下来,我们将通过从智能合约中提取帖子来实现这个功能。
需要注意的是,这个代码片段中的哈希值是我所存储数据的哈希值,因而它在你的本地 IPFS 节点中是不可用的,你需要将它替换成你数据的哈希值。具体而言,你只需要记录数据上传至 IPFS 时返回的哈希值并将其转换为十六进制。
export class App extends Component { render() { return () }} DReddit
构建帖子列表组件 List
在构建展示帖子列表的组件之前,我们必须想办法来优化智能合约。目前我们还没有一个很好的方法从智能合约中获取数组数据,也就是说要实现帖子的列表展示功能我们需要逐个获取帖子的数据。为此,我们需要获取帖子的总个数并通过迭代来索引所有的帖子,从而实现对每个帖子的获取。
我们需要在 DReddit 智能合约中引入一个判断帖子个数的函数 numPosts():
function numPosts() public view returns (uint) { return posts.length;}
当我们添加帖子时,帖子个数 posts.length 会相应的增加,因此我们可以把它用做读取帖子时的索引。当然了,如果愿意的话你也可以写一个测试验证一下它的正确性!
有了这个,我们就可以开始构建帖子列表组件 List 了。List 组件维护着一个要在屏幕上展示的帖子列表,我们可以从最简单的功能开始再一步步深入,具体代码如下:
import React, { Component } from 'react';export class List extends Component { constructor(props) { super(props); this.state = { posts: [] }; } render() { return ({this.state.posts.map(post => { return ( ) }}) })}
这里最有趣的部分就是渲染函数 render(),代码中我们遍历了所有的 state.posts (目前为空),然后在每次迭代中渲染一个帖子组件 Post。另一个需要注意的点是,每个帖子组件 Post 都会收到一个键值 key, React 框架在循环创建视图时需要用到这个键值。你可能会发现到目前为止我们还没用过帖子的序号 post.id,不要担心,我们马上就会用到它。
现在我们已经可以将帖子列表组件 List 放在 App 组件中了。但现在它还不会展示任何内容,因为我们还没有发布任何帖子,我们接下来就要做这个工作。
import { List } from './List';export class App extends Component { render() { return () }} DReddit
a)获取帖子数据
如前所述,我们将使用智能合约的判断帖子个数函数 numPosts()来获取帖子的总数。然后我们将帖子总数作为索引来迭代单独访问每个帖子。这个逻辑应该在帖子列表组件 List 准备就绪后执行,因此我们需要在 List 组件的定义之后加入 componentDidMount()函数:
export class List extends Component { ... 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 = await Promise.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 组件中:
export class App extends Component { ... 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():
export class App extends Component { constructor(props) { super(props); this.state = { posts: [] }; } async componentDidMount() { await this.loadPosts(); } ...}
最后但同样重要的是,我们需要将帖子传递给帖子列表组件 List 并将加载帖子函数 loadPosts()传递给创建帖子组件 CreatePost 作为回调处理程序:
render() { return ()} DReddit
完成后,我们可以分别从 this.props 中获取帖子和帖子创建后处理函数 afterPostHandler()。这个功能将在帖子列表组件 List 的渲染函数 render()中实现(注意我们不再需要状态对象 this.state 了):
render() { return ({this.props.posts.map(post => { ... })} )}
然后在创建帖子组件 CreatePost 中,我们需要在创建帖子后调用帖子创建后处理函数afterPostHandler():
async createPost(event) { ... await createPost.send({from: accounts[0], gas: estimate}); await this.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 ({this.props.posts.map(post => { return ( ) }}) })}
展示好评数和差评数的功能实际上只需要在帖子组件 Post 的渲染函数 render()中插入数据。代码中我们将数据添加到按钮旁边,你可以随意将它们放在其他位置:
export class Post extends Component { ... render() { ... return (... {this.props.upvotes} {this.props.downvotes} ) }}
b)实现好评差评投票
与创建新帖子类似,对帖子进行好评差评投票也需要发送交易到 DReddit 智能合约。因此,我们将执行与创建帖子组件 CreatePost 中几乎相同的操作,唯一的区别就是在这里我们调用的是智能合约的投票函数 vote()。你应该还记得,投票函数 vote()接收两个参数,帖子序号 post id 和投票类型 Ballot,具体而言就是没有投票 NONE,好评 UPVOTE 或差评 DOWNVOTE,它的存储格式为 8 位无符号整型 uint8。
上文提到过,在这个应用中不同部分(智能合约、前端组件)的变量都有着相同的表示,这样会大大减小出错的可能,对于前端组件中的投票组件,我们仍使用 0、1、2 这三个数字来表示没有投票 NONE,好评 UPVOTE 或差评 DOWNVOTE,但问题在于 JavaScript 中不支持枚举数据结构,因此在前端组件中我们需要使用哈希对象作为替代:
const BALLOT = { NONE: 0, UPVOTE: 1, DOWNVOTE: 2}
实际上,我们的帖子组件 Post 中并没有加入帖子序号 post id,不过将帖子序号 post id 添加到帖子列表组件 List 中并不是什么难事,现在你应该知道该怎么做了!
我们需要分别在好评投票按钮和差评投票按钮上添加点击处理程序,然后再将我们在投票类型 BALLOT 中定义的好评投票和差评投票传递给它们(请注意,投票类型中的没有投票 None 只是为了保证程序逻辑的完整性,但实际上在代码中我们并没有使用它):
接下来,我们需要将该投票类型以及所投的帖子序号 post id 发送到智能合约之中。
async vote(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 对象的好评差评投票并将它们存储在组件的状态中。
export class Post extends Component { constructor(props) { super(props); this.state = { topic: '', content: '', upvotes: this.props.upvotes, downvotes: this.props.downvotes }; } ...}
同时我们还需要更改组件的渲染函数 render(),让它从组件状态中读取数据而不是从 props 对象中:
render() { ... return (... {this.state.upvotes} {this.state.downvotes} )}
这样,我们就可以在投票发起后立即使用设置状态函数 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)的状态。一般来说,如果一个用户之前没有对某个帖子投票,并且他此时没有在提交对该帖子的投票,那么他就可以对该帖子投票:
export class Post extends Component { 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 (... {this.state.upvotes} {this.state.downvotes} )}
最后但同样重要的是,我们必须确保组件的状态也做出相应的更新。当一个帖子初始化时,我们将调用智能合约中函数 canVote():
export class Post extends Component { ... async componentDidMount() { ... const canVote = await DReddit.methods.canVote(this.props.id).call(); this.setState({ topic, content, canVote }); } ...}
在进行投票时,我们在发送投票所在的交易之前要先将正在提交状态 submitting 设置为是(true),并在交易完成后再将其改为否(false),由于此时已经完成了对帖子的投票,因此能否投票状态 canVote 也应该被设置为否(false):
async vote(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/