前言

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

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

项目介绍

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

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

开发步骤

下载react项目模板

确保本地已经准备好Truffle所需的环境,准备以下命令,下载react项目模板。
$ 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({
    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,体会了开发的基本流程。

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

项目源码

GitHub

参考文章

Ludis的博文