前言

最近DApp的开发貌似很火,学习了区块链的一些知识之后,相信有很多人和我一样,也想了解开发一个DApp是一个怎样的流程。

下面将通过一个简单的栗子来初识一下DApp的开发流程,届时,我们也将开发出第一个DApp应用–《永存的留言》。

项目介绍

《永存的留言》是一个基于以太坊的在线留言平台。它的功能十分简单–用户可以在平台上进行留言,平台每10s随机的展示留言内容。
但是它的特点在于,利用区块链的特性,保证了数据的真实性、完整性和安全性。

  • 使用Solidity开发后端方法
  • 使用Truffle框架
  • 基于unbox react脚手架项目
  • 部署在以太坊测试网络上Ropoetn Test Network
  • 使用MetaMask钱包插件发布交易

开发步骤

下载react项目模板

确保本地已经准备好Truffle所需的环境,准备以下命令,下载react项目模板。

1
2
$ mkdir a && cd a
truffle unbox react

当看到 Unbox successful. Sweet!提示时,表明下载成功。

编写智能合约

这是我们唯一的实现合约,包含的发送留言和读取留言的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
pragma solidity ^0.4.19;

contract SimpleStorage {
// 留言结构体
struct Message {
string word; // 留言
address from; // 留言者地址
string timestamp ; // 留言unix时间戳
}

Message[] private wordArr;

/**
* 写入留言的方法
*/
function setWord(string s, string t) public {
wordArr.push(Message({
word: s,
from: msg.sender,
timestamp: t
}));
}

/**
* 获取随机留言的方法
*/
function getRandomWord(uint random) public view returns (uint, string, address, string) {
if(wordArr.length==0){
return (0, "", msg.sender, "");
}else{
Message storage result = wordArr[random];
return (wordArr.length, result.word, result.from, result.timestamp);
}
}
}

编译、部署合约

修改发布的脚本

1
2
3
4
5
6
7
var SimpleStorage = artifacts.require("./SimpleStorage.sol");
//var Words = artifacts.require("Words");

module.exports = function(deployer) {
deployer.deploy(SimpleStorage);
//deployer.deploy(Words);
};

执行truffle compile进行合约的编译

获取合约地址。

  • 这里我们打开MetaMask钱包插件,左上角选择Ropoetn Test Network网络.

  • 利用Remix编译,发布合约到以太坊测试环境。

  • 通过MetaMask的交易hash查询,获取已经部署到以太坊测试网络中的合约地址。

编写前端页面

这个部分主要是编写前端的展示效果和与合约交互的逻辑,这一部分最难编写,也最耗时间。

  • 主要逻辑代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
const contractAddress = "0x39e5196750dcddb1aaf6dda7d6e8dbb633482905" // 合约地址(以太坊测试网络)
var simpleStorageInstance // 合约实例

class App extends Component {
// 初始化构造
constructor(props) {
super(props)
this.state = {
word: null,
from: null,
timestamp: null,
random: 0,
count: 0,
input: '',
web3: null,
emptyTip: "还没有留言,快来创建全世界第一条留言吧~",
firstTimeLoad: true,
loading: false,
loadingTip: "留言正在写入,请耐心等待~",
waitingTip: "留言正在写入,请耐心等待~",
successTip: "留言成功",
animate: "",
in: css(styles.in),
out: css(styles.out)
}
}

// 获取Web3实例
componentWillMount() {
// Get network provider and web3 instance.
getWeb3
.then(results => {
this.setState({
web3: results.web3
})
// Instantiate contract once web3 provided.
this.instantiateContract()
})
.catch(() => {
console.log('Error finding web3.')
})
}

// 获取合约实例
instantiateContract() {
/*
* SMART CONTRACT EXAMPLE
*
* Normally these functions would be called in the context of a
* state management library, but for convenience I've placed them here.
*/
const contract = require('truffle-contract')
const simpleStorage = contract(SimpleStorageContract)
simpleStorage.setProvider(this.state.web3.currentProvider)

// Get accounts.
this.state.web3.eth.getAccounts((error, accounts) => {
simpleStorage.at(contractAddress).then(instance => {
simpleStorageInstance = instance

/*simpleStorage.deployed().then((instance) => {
simpleStorageInstance = instance // 部署本地Ganache*/
console.log("合约实例获取成功")
})
.then(result => {
return simpleStorageInstance.getRandomWord(this.state.random)
})
.then(result => {
console.log("读取成功", result)
if(result[1]!=this.setState.word){
this.setState({
pub-f7b48bfa48024ba389d1065d05b367db.r2.dev animate: this.state.out
})
setTimeout(() => {
this.setState({
count: result[0].c[0],
word: result[1],
from: result[2],
timestamp: result[3],
animate: this.state.in,
firstTimeLoad: false
})
}, 2000)
}else{
this.setState({
firstTimeLoad: false
})
}
this.randerWord()
})

})
}

// 循环从区块上随机读取留言
randerWord() {
setInterval(() => {
let random_num = Math.random() * (this.state.count? this.state.count: 0)
this.setState({
random: parseInt(random_num)
})
console.log("setInterval读取", this.state.random)
simpleStorageInstance.getRandomWord(this.state.random)
.then(result => {
console.log("setInterval读取成功", result)
if(result[1]!=this.setState.word){
this.setState({
animate: this.state.out
})
setTimeout(() => {
this.setState({
count: result[0].c[0],
word: result[1],
from: result[2],
timestamp: result[3],
animate: this.state.in
})
}, 2000)
}
})
}, 10000)
}

// 写入区块链
setWord(){
if(!this.state.input) return
this.setState({
loading: true
})
let timestamp = new Date().getTime()
simpleStorageInstance.setWord(this.state.input, String(timestamp), {from: this.state.web3.eth.accounts[0]})
.then(result => {
this.setState({
loadingTip: this.state.successTip
})
setTimeout(() => {
this.setState({
loading: false,
input: '',
loadingTip: this.state.waitingTip
})
}, 1500)

})
.catch(e => {
// 拒绝支付
this.setState({
loading: false
})
})
}
// 时间戳转义
formatTime(timestamp) {
let date = new Date(Number(timestamp))
let year = date.getFullYear()
let month = date.getMonth() + 1
let day = date.getDate()
let hour = date.getHours()
let minute = date.getMinutes()
let second = date.getSeconds()
let fDate = [year, month, day, ].map(this.formatNumber)
return fDate[0] + '年' + fDate[1] + '月' + fDate[2] + '日' + ' ' + [hour, minute, second].map(this.formatNumber).join(':')
}
/** 小于10的数字前面加0 */
formatNumber(n) {
n = n.toString()
return n[1] ? n : '0' + n
}

}

运行项目

使用npm start启动项目,浏览器的3000端口运行。
效果如下(一定要连接到Ropoetn Test Network网络才可以)。

总结

这样我们就开发出了我们的第一个DApp,体会了开发的基本流程。

  • 明确项目需求
  • 确定开发环境
  • 编写、测试、部署合约
  • 设计前端页面,使前后端交互
  • 发布测试