便签DApp实战

EOS DApp实战:构建多用户便签应用
本文档详细介绍了如何基于EOS和React开发一个多用户便签DApp。从启动节点服务到初始化钱包与账号,再到编译、部署合约以及运行便签应用,每个步骤都有清晰的操作指南。此外,还阐述了项目文件的组织结构,包括前端代码、合约代码和构建输出目录。

基于EOS和React实现一个多用户便签应用。

运行示例代码

请按照以下顺序运行示例代码:

1、启动节点服务程序

首先在1#终端启动nodeos,建议先清理历史数据:

~$ rm ~/.local/share/eosio/nodeos/data
~$ nodeos

或者使用方便脚本:

~$ clean-nodeos.sh

在2#终端启动keosd:

~$ keosd

2、初始化钱包与账号

在3#终端执行以下脚本重新初始化默认钱包:

~$ init-wallet.sh

使用以下脚本创建两个账号:todo.user和sodfans:

~$ new-account.sh todo.user
~$ new-account.sh sodfans

3、编译、部署便签合约

在3#终端进入~/repo/chapter7目录,运行以下脚本编译、部署合约:

~$ cd ~/repo/chapter7
~/repo/chapter7$ build-contract.sh contract/todo.cpp
~/repo/chapter7$ deploy-contract.sh todo.user build/todo

使用cleos测试合约:

~/reo/chapter7$ cleos push action todo.user create '["sodfans",7878,"buy some milk at 711"]' -p sodfans

查看数据表记录:

~/repo/chapter7$ cleos get table todo.user sodfans todos

4、运行便签DApp

在3#终端执行如下命令启动测试web服务器:

~/repo/chapter7$ npm run dev

然后刷新练习环境中的嵌入浏览器,打开便签页面,进行添加、删除等操作。

使用如下命令在dist目录下生成可离线运行的文件:

~/repo/chapter7$ num run build

项目文件组织

1、前端代码目录:app

  • index.html:单页宿主HTML
  • <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8" />
      <title>TODO DAPP - Hubwiz.com</title>
    </head>
    <body>
      <div id="app"></div>
      <script src="/bundle.js"></script>
    </body>
    </html>

  • app.jsx: js打包入口文件
  • import React, { Component } from 'react';
    import ReactDOM from 'react-dom';
    import TodoBox from './components/TodoBox';
    import {TodoProvider} from './components/TodoContext'
    
    ReactDOM.render(
      <TodoProvider><TodoBox/></TodoProvider>,
      document.getElementById('app'));

  • components:组件目录
  • TodoBox.jsx

import "bootstrap/dist/css/bootstrap.css"

import React, { Component } from 'react';
import TodoEditor from './TodoEditor';
import TodoList from './TodoList';
import TodoItem from './TodoItem';
import {TodoContext} from './TodoContext'

class TodoBox extends React.Component{
   
  render(){
    const state = this.props.store
    let stateText = state.loading ? '正在保存...' : ''
    let finished = state.tasks.filter(t => t.done).length
    let statText =  '完成情况:' + finished + ' / ' + state.tasks.length

    return (
      <div className="card" style={{ width: 500 }}>
        <div className="card-body">
          <div className="card-title">任务便签</div>
          <TodoList/>
    	  <TodoEditor/>
          <div style={{padding:'10px 0 0 0'}}>
            <small>{stateText}</small> 
            <small className="float-right">{statText} </small>
          </div>
        </div>
      </div>
    )
  }
  
}

export default props => (
  <TodoContext.Consumer>
	{ store => <TodoBox {...props} store={store}/>}
  </TodoContext.Consumer>
)

  • TodoContext.jsx

  • import React from 'react'
    import TodoStore from '../services/TodoEosStore'
    //import TodoStore from '../services/TodoMemStore'
    
    export const TodoContext = React.createContext({})
    
    export class TodoProvider extends React.Component{
      constructor(props){
        super(props)
        this.state = new TodoStore(this)
      }
      componentDidMount(){
        this.state.initState()
      }
      render(){
        return (
        	<TodoContext.Provider value={this.state}>
              {this.props.children}
            </TodoContext.Provider>
        )
      }
    }

  • TodoEditor.jsx

  • import React, { Component } from 'react';
    import ReactDOM from 'react-dom';
    import {TodoContext} from './TodoContext'
    
    class TodoEditor extends React.Component{
      constructor(props) {
        super(props)
        
        this.createTask = e => {
          e.preventDefault()
          let el = ReactDOM.findDOMNode(this.refs.newItem)
          if (!el.value) {
            //flash to notify
          } else {
            this.props.store.createTask(el.value)
            el.value = ''
          }
        }
      }
      
      render(){
        return (
          <form className="row" style={{padding:'10px'}}>
            <input className="form-control col-sm-9" ref="newItem" type="text" placeholder="请输入新任务~"/>
            <input type="submit" className="btn btn-primary col-sm-3" onClick={this.createTask} value="添加"/>
    	  </form>
        )
      }
    }
    
    export default props => (
      <TodoContext.Consumer>
        {store => <TodoEditor {...props} store={store}/>}
      </TodoContext.Consumer>
    )

  • TodoItem.jsx

  • import React, { Component } from 'react';
    import {TodoContext} from './TodoContext'
    
    class TodoItem extends React.Component{
      constructor(props) {
        super(props)
        
        this.toggleTask = e => {
          e.preventDefault()
          this.props.store.toggleTask(this.props.id)
        }    
        this.removeTask = e => {
          e.preventDefault()
          e.stopPropagation()
          this.props.store.removeTask(this.props.id)
        }
      }
      
      render(){
        return (
          <a href="#" className="list-group-item"  onClick={this.toggleTask}>
            <div className="form-check">
              <input checked={this.props.done} 
                     className="form-check-input" type="checkbox"  id="check"/>
        	  <label className="form-check-label" htmlFor="check" >{this.props.desc}</label>
              <button onClick={this.removeTask} 
                      className="btn btn-sm btn-danger float-right" size="small">删除</button>
            </div>    
          </a>
        )
      }
    }
    
    export default props => (
      <TodoContext.Consumer>
    	{ store => <TodoItem {...props} store={store}/>}
      </TodoContext.Consumer>
    )

  • TodoList.jsx

  • import React, { Component } from 'react';
    import TodoItem from './TodoItem'
    import {TodoContext} from './TodoContext'
    
    class TodoList extends React.Component{
      render(){
        return (
    	  <div className="list-group">
            {this.props.store.tasks.map(task => <TodoItem {...task} key={task.id}/>)}
          </div>
        )
      }
    }
    
    export default props => (
      <TodoContext.Consumer>
    	{ store => <TodoList {...props} store={store}/>}
      </TodoContext.Consumer>
    )

  • services:组件服务目录

TodoEosStore.js

import Eos from 'eosjs'

const wallet = {
  account: 'sodfans',
  privateKey: SODFANS_KEY ? SODFANS_KEY : '5JHZdDQ7cfHQ8PnoNqvW9kWbdsCZDTUiBBs3YLxowJEbjCKfvET'
}
class TodoEosStore{
  constructor(host,options){
    const defaults= {
      code: 'todo.user',
      table: 'todos',
      wallet: wallet,
      nodeosUrl: NODEOS_URL ? NODEOS_URL : 'http://127.0.0.1:8888',
      keosdUrl:  KEOSD_URL ? KEOSD_URL : 'http://127.0.0.1:8900',
    }
    this._options = Object.assign({},defaults,options)
    this._host = host
    this._nodeos = Eos({
      httpEndpoint: this._options.nodeosUrl,
      keyProvider: [this._options.wallet.privateKey],
    })
    
    //state
    this.loading = false,
    this.tasks = []
    
    this.initState = () => this.getTasks()
    
    //action    
    this.toggleTask = id => {      
      const opts=  {authorization:this._options.wallet.account}
      this.setLoading(true)
	  return this._nodeos.contract(this._options.code)
		.then( contract => contract.toggle(this._options.wallet.account,id,opts) )
        .then( () => this.getTasks())
        .then( () => this.setLoading(false))
    }
    this.removeTask = id => {
      const opts=  {authorization:this._options.wallet.account}
      this.setLoading(true)
      return this._nodeos.contract(this._options.code)
        .then( contract => contract.remove(this._options.wallet.account,id,opts) )
        .then( () => this.getTasks())
        .then( () => this.setLoading(false))
    }
    this.createTask = desc => {
      const opts=  {authorization:this._options.wallet.account}
      const id = Date.now()
      this.setLoading(true)
      return this._nodeos.contract(this._options.code)
        .then( contract => contract.create(this._options.wallet.account,id,desc,opts) )
        .then( () => this.getTasks())
        .then( () => this.setLoading(false))
    }
    
    //utils
    this.getTasks = () => {
	  return this._nodeos.getTableRows(true, this._options.code, this._options.wallet.account,this._options.table)      
      	.then( ret => this._host.setState({tasks:ret.rows}))
    }
    this.setLoading = flag => {
      this._host.setState({loading:flag})
    }
    
  }
}

export default TodoEosStore

TodoMemstore.js

class TodoMemStore{
  constructor(host){
    this._host = host

    this.loading = false,
    this.tasks = []
        
    this.initState = () => {}
    
    this.toggleTask = id => {
      let idx = this._find(id)
      if(idx <0) return
      this.tasks[idx].done = ! this.tasks[idx].done
      this._host.setState({tasks:this.tasks})      
    }
    this.removeTask = id => {
      let idx = this._find(id)
      if(idx < 0) return 
      this.tasks.splice(idx,1)
      this._host.setState({tasks:this.tasks})
    }
    this.createTask = desc => {
      let id = Date.now()
      this.tasks.push({id,desc,done:false})
      this._host.setState({tasks:this.tasks})
    }
    
    this._find = id => {
      for(let i=0;i<this.tasks.length;i++){         
        if(this.tasks[i].id === id) return i
      }
      return -1
    }    
  }
}

export default TodoMemStore

2、合约代码目录:contract

  • todo.cpp:便签合约
  • #include <eosiolib/eosio.hpp>
    
    class todo_contract : public eosio::contract {
      public:
        todo_contract(account_name self):eosio::contract(self){}
    
        // @abi action
        void create(account_name author, const uint64_t id, const std::string& desc) {
          require_auth(author);
          todo_table todos(_self,author);
          todos.emplace(author, [&](auto& record) {
            record.id  = id;
            record.desc = desc;
            record.done = false;
          });
    
          eosio::print("todo#", id, " created");
        }
    
        // @abi action
        void remove(account_name author, const uint64_t id) {
          require_auth(author);
          todo_table todos(_self,author);
          auto iter = todos.find(id);
          todos.erase(iter);
    
          eosio::print("todo#", id, " deleted");
        }
    
        // @abi action
        void toggle(account_name author, const uint64_t id) {
          require_auth(author);
          todo_table todos(_self,author);
          auto iter = todos.find(id);
          eosio_assert(iter != todos.end(), "Todo does not exist");
    
          todos.modify(iter, author, [&](auto& record) {
            record.done = ! record.done;
          });
    
          eosio::print("todo#", id, " toggle todo state");
        }
    
      private:
        // @abi table todos i64
        struct todo {
          uint64_t id;
          std::string desc;
          bool done;
    
          uint64_t primary_key() const { return id; }
          EOSLIB_SERIALIZE(todo, (id)(desc)(done))
        };
    
        typedef eosio::multi_index<N(todos), todo> todo_table;
    };
    
    EOSIO_ABI(todo_contract, (create)(remove)(toggle))

3、构建输出目录

  • 前端:dist
  • 合约:build

package.json

  • {
      "name": "chapter8",
      "version": "1.0.0",
      "description": "",
      "main": "webpack.config.js",
      "dependencies": {
        "eosjs": "^15.0.3"
      },
      "devDependencies": {},
      "scripts": {
        "dev": "webpack-dev-server",
        "build": "webpack"
      },
      "author": "",
      "license": "ISC"
    }
    

    webpack.config.js

  • const webpack = require('webpack');
    const CopyWebpackPlugin = require('copy-webpack-plugin');
    const path = require('path')
    const fs = require('fs')
    
    let sodfansPrivateKey 
    try{
      sodfansPrivateKey = fs.readFileSync('/home/user/repo/tools/artifacts/sodfans/activePrivateKey','utf-8').trim()
    }catch(e){
      console.error('ERROR: you need sodfans account to run this app.')
      process.exit(0)
    }
    
    module.exports = {
      mode: 'production',
      performance: {hints:false},
      entry: './app/app.jsx',
      output: {
        path: path.join(__dirname,'dist'),
        filename: 'bundle.js'
      },
      plugins: [
        //new webpack.HotModuleReplacementPlugin(),
        new CopyWebpackPlugin([
          { from: './app/index.html', to: "index.html" }
        ]),
        new webpack.DefinePlugin({
          NODEOS_URL: JSON.stringify(process.env.NODEOS_URL),
          KEOSD_URL: JSON.stringify(process.env.KEOSD_URL),
          SODFANS_KEY: JSON.stringify(sodfansPrivateKey)
        })
        
      ],
      module: {
        rules: [
          {
            test: /\.(js|jsx)$/,
            exclude: /node_modules/,
            loader: 'babel-loader'
          },
          { test: /\.css$/,
            include: /node_modules/,
            loader: 'style-loader!css-loader' 
          },
          { test: /\.less$/, 
            exclude: /node_modules/, 
            loader: 'style-loader!css-loader!less-loader' 
          },
          { test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/, 
            loader: 'url-loader'
          }
        ]
      },
      resolve: {
        extensions: ['*', '.js', '.jsx']
      },
      devServer: {
       contentBase: './dist',
        host: '0.0.0.0',
        disableHostCheck:true,
        public:'0.0.0.0'  
      }
    };

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-每天进步一点点-

你的鼓励是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值