基于eos的Dapp开发--元素战争(三)

2021-11-23 10:39:56 浏览数 (1)

我们在前面的章节中先后介绍了一个基于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分为两种情况:

  1. 调用成功:SET_USER这个ation被执行且UserReducer会接收到相应的action,Redux store中将会更新用户名相应的属性值,其他信息不变。
  2. 调用失败:将会记录相应的失败信息到Login登录界面。

为了连接store和web app我们还需要使用connect函数将两者关联起来,可以参看以下代码:

代码语言:javascript复制
// 将所有的状态信息和组件的属性值放到map表里
const mapStateToProps = state => state;

// 将以下的action和组件的属性值放到map表里
const mapDispatchToProps = {
  setUser: UserAction.setUser,
};

登录界面写好了,我们可以开始写第二个界面即游戏界面了,游戏界面的相关代码不再粘贴在这里,感兴趣的可以仔细阅读Game.jsx。

本文至此,大致介绍了元素战争游戏中是使用什么来开发前端页面的,开发过程中使用到了哪些组件,如何去实现一个service服务,并通过这个服务使前端和智能合约关联起来。具体的代码可以参看源码,也可以看我稍微注释的内容。官方给出的总结如下:

笔者未有过前端开发经验,写作过程中难免有误,还请各位朋友多多指正。

0 人点赞