基于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' } };