我们在前面的章节中先后介绍了一个基于EOS的Dapp中主要包含有哪些内容以及智能合约的编写过程和规范,今天我们来谈谈一个Dapp开发中另一个不可或缺的内容,即前端是如何开发的。
在本次课程之前需要指出:在本课程中将涉及到private-key的操作,由于这仅仅是个教程所以在这里故意将private-key的使用简单化了,在我们自己进行DAPP的开发过程中是不可取的,一定要注意保护好用户的隐私以及自己Dapp智能合约的账户安全。
上一节中我们在智能合约中实现了一个名为login的ation,用户通过前端进行登录,然后使用一个名为eosjs的Javascript的库提交请求到智能合约,在本节中我们还将使用另外一个JavaScript库Redux来处理React app的状态信息,Redux并不仅仅是为了React而设计的,因此我们要使用一个react-redux模块来实现这些。首先通过以下命令来安装相应的库:
代码语言:javascript复制npm install --save redux
npm install --save react-redux
npm install --save eosjs
我们来看Login.jsp文件,其中包含了用户名输入框,private-key输入框,提交按钮三个部分,当然你现在点击这个按钮是不会有任何反应的,button是react的一个组件,我们可以在src/components/Button看到这些内容,button类封装了我们整个web app的按钮的绘制和样式,通过复用这个组件,我们可以避免大规模的使用CSS等来构建前端页面。
代码语言:javascript复制import React, { Component } from 'react';
import { connect } from 'react-redux';
// button组件
import { Button } from 'components';
// 服务组件
import { UserAction } from 'actions';
import { ApiService } from 'services';
class Login extends Component {
constructor(props) {
super(props);
// 构造函数来存储数据的状态和错误信息
this.state = {
form: {
username: '',
key: '',
error: '',
},
}
// 绑定消息函数
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
// 响应键盘上的动作
handleChange(event) {
const { name, value } = event.target;
const { form } = this.state;
this.setState({
form: {
...form,
[name]: value,
error: '',
},
});
}
// 用户点击提交按钮的相应
handleSubmit(event) {
event.preventDefault();
const { form } = this.state;
const { setUser } = this.props;
//通过apiService发送登录的trx到智能合约,如果登录成功了保存用户名到redux,如果失败了向用户展示错误信息
return ApiService.login(form)
.then(() => {
setUser({ name: form.username });
})
.catch(err => {
this.setState({ error: err.toString() });
});
}
我们首先要把登录框放到main app中,为此我们来编辑下这个文件src/components/App/App.jsx。接下来我们将在登录框中构建并绑定一些响应函数,需要存储登录的数据然后提交用户的登录信息到智能合约中去,然后通过一个构造器和两个函数来实现这些。
- 构造函数--用来初始化一些信息同时绑定两个响应函数,这样我们就可以方便的查询组件的状态。
- handleChange函数--当用户更新用户名或者密码的时候就会被触发,然后更新组件的状态。
- handleSubmit函数--发送用户的登录请求到智能合约。
上面说了这么多,其实前端和智能合约之间并没有产生交互,接下来我们来看如何实现交互。在frontend文件夹中我们可以看到.env文件,它用来存储一些变量的地方如,类似于环境变量:
- REACT_APP_EOS_HTTP_ENDPOINT--接口的地址
- REACT_APP_EOS_CONTRACT_NAME--合约的名字
接下来让我们尝试创建一个服务端组件,命名为ApiService,这个服务组件将会让前端和智能合约联系起来。在创建服务组件的时候我们使用了takeAction方法,该方法将发送transaction信息到智能合约。在这里我们使用了eosjs中的三个库:
- RPC
- SignatureProvider
- Api
关于这三个库的相关信息也可以参考下eosjs的文档。
在takeAction中我们将向智能合约发送两部分内容即:action和dataValue。为了trx处理的方便,我们将使用api.transact() 将发送的内容转为JSON格式。我们从代码中可以看到JSON中主要包含有三部分,账户、action的名字、权限。接下来定义login中的内容:用户名、key。这些信息已经保存在本地了,可以拿来直接使用,现在我们可以用ApiService.login()触发登录操作了。
登录功能实现之后,我们需要通知组件,以方便在登录过程中的调用。我们可以通过把登录消息存储在Redux中来实现,首先让我们来创建三个组件:
- action
- reducer
- store
Action 是把数据从应用传到 store 的有效载荷,它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。action一般都是存储在Redux中的一个普通的JavaScript对象,在本教程中我们只需定义一个action,我们称之为SET_USER,对应到我们上一节内容中的多索引表中存储的数据,在frontend/src/actions/UserAction.js可以找到:
代码语言:javascript复制import { ActionTypes } from 'const';
class UserAction {
static setUser({ name, win_count, lost_count, game }) {
return {
type: ActionTypes.SET_USER,
name, // 用户名
win_count, // 用户胜利的次数
lost_count,// 用户失败的次数
game, // 当前游戏信息
}
}
}
export default UserAction;
同样的在frontend/src/reducers/UserReducer.js我们可以找到Reducer的相关信息,Action 只是描述了有事情发生了这一事实,并没有指明应用如何更新 state。而这正是 reducer 要做的事情。在 Redux 应用中,所有的 state 都被保存在一个单一对象中。建议在写代码前先想一下这个对象的结构。如何才能以最简的形式把应用的 state 用对象描述出来。在本文中我们在reducer中定义了一个初始化状态和一个状态声明相关内容。当SET_USER action被检测到的时候,我们会用Object.assign()通过创建一个副本的方式去更新初始化的状态。这个函数将会针对store中的每一个用户生成一个新的对象,开发者尽量不要直接修改Redux的store。
代码语言:javascript复制import { ActionTypes } from 'const';
//初始化一些信息
const initialState = {
name: "",
win_count: 0,
lost_count: 0,
game: null,
};
export default function (state = initialState, action) {
switch (action.type) {
case ActionTypes.SET_USER: {
return Object.assign({}, state, {
name: typeof action.name === "undefined" ? state.name : action.name,
win_count: action.win_count || initialState.win_count,
lost_count: action.lost_count || initialState.lost_count,
game: action.game || initialState.game,
});
}
default:
return state;
}
}
本代码中将会使用Redux utility工具combinedReducers导出UserReducer,在frontend/src/reducers/index.js.可以找到,当然我们也可以在以后的开发过程中扩展添加更多的reducer
Store 就是把action和reducer联系到一起的对象。Store 有以下职责:
- 维持应用的 state;
- 提供 getState() 方法获取 state;
- 提供 dispatch(action) 方法更新 state;
- 通过 subscribe(listener) 注册监听器。
再次强调一下 Redux 应用只有一个单一的 store。当需要拆分处理数据的逻辑时,使用 reducer 组合 而不是创建多个 store。在本文中store的路径为frontend/src/store/index.js。
代码语言:javascript复制import { createStore, compose } from 'redux';
import rootReducer from 'reducers';
const initialState = {};
const enhancers = [];
// DevTools Extension for debugging in Chrome
if (process.env.NODE_ENV === 'development') {
const devToolsExtension = window.devToolsExtension;
if (typeof devToolsExtension === 'function') {
enhancers.push(devToolsExtension());
}
}
const composedEnhancers = compose(
...enhancers
)
const store = createStore(
rootReducer,
initialState,
composedEnhancers
)
export default store;
上面介绍了三个组件:action,reducer,store,但并未将三者如何融合的作出说明,当用户点击确认按钮的时候会通过handleSubmit()调用服务组件里的ApiService.login(),然后通过该方法调用智能合约里面的ation。调用智能合约里面的action分为两种情况:
- 调用成功:SET_USER这个ation被执行且UserReducer会接收到相应的action,Redux store中将会更新用户名相应的属性值,其他信息不变。
- 调用失败:将会记录相应的失败信息到Login登录界面。
为了连接store和web app我们还需要使用connect函数将两者关联起来,可以参看以下代码:
代码语言:javascript复制// 将所有的状态信息和组件的属性值放到map表里
const mapStateToProps = state => state;
// 将以下的action和组件的属性值放到map表里
const mapDispatchToProps = {
setUser: UserAction.setUser,
};
登录界面写好了,我们可以开始写第二个界面即游戏界面了,游戏界面的相关代码不再粘贴在这里,感兴趣的可以仔细阅读Game.jsx。
本文至此,大致介绍了元素战争游戏中是使用什么来开发前端页面的,开发过程中使用到了哪些组件,如何去实现一个service服务,并通过这个服务使前端和智能合约关联起来。具体的代码可以参看源码,也可以看我稍微注释的内容。官方给出的总结如下:
笔者未有过前端开发经验,写作过程中难免有误,还请各位朋友多多指正。