前言

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

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

项目介绍

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

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

开发步骤

下载react项目模板

确保本地已经准备好Truffle所需的环境,准备以下命令,下载react项目模板。
$ mkdir a && cd a
truffle unbox react
当看到 Unbox successful. Sweet!提示时,表明下载成功。

编写智能合约

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

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);
        }
    }
}

编译、部署合约

修改发布的脚本。

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网络.
    dapp_2.png
  • 利用Remix编译,发布合约到以太坊测试环境。
    dapp_3.png
  • 通过MetaMask的交易hash查询,获取已经部署到以太坊测试网络中的合约地址。
    dapp_4.png

编写前端页面

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

  • 主要逻辑代码
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_5.png

总结

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

  • 明确项目需求
  • 确定开发环境
  • 编写、测试、部署合约
  • 设计前端页面,使前后端交互
  • 发布测试
Last modification:June 4, 2018
如果觉得我的文章对你有用,请随意赞赏