剥开比原看代码09:通过dashboard创建密钥时,前端的数据是如何传到后端的?

简介: 作者:freewind比原项目仓库:Github地址:https://github.com/Bytom/bytomGitee地址:https://gitee.com/BytomBlockchain/bytom在前面一篇文章,我们粗略的研究了一下比原的dashboard是如何做出来的,但是对里面提到的各种细节功能,并没有深入的去研究。

作者:freewind

比原项目仓库:

Github地址:https://github.com/Bytom/bytom

Gitee地址:https://gitee.com/BytomBlockchain/bytom

在前面一篇文章,我们粗略的研究了一下比原的dashboard是如何做出来的,但是对里面提到的各种细节功能,并没有深入的去研究。那么从本文开始,我们将在这一段时间,分别研究里面提到的每一项功能。

在前一篇文章中,当我们第一次在浏览器中打开dashboard时,因为还没有创建过密钥,所以比原会提示我们输入一些别名和密码,为我们创建一个密钥和相应的帐户。就是下面这张图所对应的:  

那么本文就将研究一下,当我们点击了"Register"按钮以后,我们在前端页面上填写的参数,到底是如何一步步的传到比原的后端的。

跟之前一样,我们将对这个问题进行细分,然后各个击破:

  1. 前端:当我们填完表单,点了提交以后,比原在前端是如何发送数据的?
  2. 后端:比原的后端是如何接收到数据的?

前端:当我们填完表单,点了提交以后,数据会发送到后端的哪个接口?

当我们点击了"Register"按钮,在前端页面中,一定会在某个地方触发一个向比原节点webapi接口发出请求的操作。究竟是访问的哪个web api?提交的数据又是什么样的呢?让我们先从前端代码中寻找一下。

注意,比原的前端代码位于另一个项目仓库bytom/dashboard中。为了能与我们在本系列文章中使用的比原v1.0.1的代码相匹配,我找到了dashboard中的v1.0.0的代码,并且提交到了一个单独的项目中:freewind/bytom-dashboard-v1.0.0。注意该项目代码未做任何修改,其master分支对应于官方代码仓库的v1.0.0分支。之所以要弄一个单独的出来,这是因为我们在文章中,每次引用一段代码的时候,都会给出相应的github上的链接,方便读者跳过去查看全貌,使用一个独立项目,会让这个过程更简便一些。

由于比原的前端页面是使用React为主的,所以我猜想在代码中,也该会有一个名为Register的组件,或者某个表单中有一个名为Register的按钮。经过搜索,我们幸运的发现了Register.jsx 这个组件文件,它正好是我们需要的。

经过高度简化后的代码如下:

src/features/app/components/Register/Register.jsx#L9-L148

class Register extends React.Component {
  // ...
  // 4. 
  submitWithErrors(data) {
    return new Promise((resolve, reject) => {
      // 5. 
      this.props.registerKey(data)
        .catch((err) => reject({_error: err.message}))
    })
  }
  // ...

  render() {
    // ...
    return (
      // ...
      // 3.
      <form className={styles.form} onSubmit={handleSubmit(this.submitWithErrors)}>
        // 1.
        <TextField
          title={lang === 'zh' ? '账户别名' : 'Account Alias'}
          placeholder={lang === 'zh' ? '请输入账户别名...' : 'Please enter the account alias...'}
          fieldProps={accountAlias} />
        <TextField
          title={lang === 'zh' ? '密钥别名' : 'Key Alias'}
          placeholder={lang === 'zh' ? '请输入密钥别名...' : 'Please enter the key alias...'}
          fieldProps={keyAlias}/>
        <TextField
          title={lang === 'zh' ? '密钥密码' : 'Key Password'}
          placeholder={lang === 'zh' ? '请输入密钥密码...' : 'Please enter the key password...'}
          fieldProps={password}
          type='password'/>
        <TextField
          title={lang === 'zh' ? '重复输入密钥密码' : 'Repeat your key password'}
          placeholder={lang === 'zh' ? '请重复输入密钥密码...' : 'Please repeat the key password...'}
          fieldProps={repeatPassword}
          type='password'/>

        // 2. 
        <button type='submit' className='btn btn-primary' disabled={submitting}>
          {lang === 'zh' ? '注册' : 'Register'}
        </button>
        // ....
        </form>
      // ...
    )
  }
}

上面的代码,共有5个地方需要注意,被我用数字标示出来了。注意这5个数字并不是从上到下标注,而是按照我们关注的顺序来的:

  1. 表单上的各个输入框,就是我们填写别名和密码的地方。这里需要关注的是每个TextFieldfieldProps属性,它对应我们提交到后台的数据的name
  2. 就是那个“Register”按钮了。需要注意的是,它的typesubmit,也就是说,点击它以后,将会触发所在formonSubmit方法
  3. 回到了form的开头。注意它的onSubmit里面,调用的是handleSubmit(this.submitWithErrors)。其中的handleSubmit是从该表单所使用的第三方redux-form中传入的,用来处理表单提交,我们在这里不关注它,只需要知道我们需要把自己的处理函数this.submitWithErrors传给它。而在后者中,我们将会调用比原节点提供的web api
  4. 第3步中的this.submitWithErrors最终将走到这里定义的submitWithErrors函数
  5. submitWithErrors将会发起一个异步请求,最终调用由外部传进来的registerKey函数

从这里我们还看不到调用的是哪个api,所以我们必须继续去寻找registerKey。很快就在同文件中找到了registerKey

src/features/app/components/Register/Register.jsx#L176-L180

(dispatch) => ({
    registerKey: (token) => dispatch(actions.core.registerKey(token)),
    // ...
  })

它又将会调用actions.core.registerKey这个函数:

src/features/core/actions.js#L44-L87

const registerKey = (data) => {
  return (dispatch) => {
    // ...
    // 1.1
    const keyData = {
      'alias': data.keyAlias,
      'password': data.password
    }
    // 1.2
    return chainClient().mockHsm.keys.create(keyData)
      .then((resp) => {
        // ...
        // 2.1
        const accountData = {
          'root_xpubs':[resp.data.xpub],
          'quorum':1,
          'alias': data.accountAlias}
        // 2.2
        dispatch({type: 'CREATE_REGISTER_KEY', data})

        // 2.3
        chainClient().accounts.create(accountData)
          .then((resp) => {
            // ...
            // 2.4
            if(resp.status === 'success') {
              dispatch({type: 'CREATE_REGISTER_ACCOUNT', resp})
            }
          })
    // ...
      })
    // ...
  }
}

可以看到,在这个函数中,做的事情还是很多的。而且并不是我一开始预料的调用一次后台接口就行了,而是调用了两次(分别是创建密钥和创建帐户)。下面进行分析:

  1. 1.1是为了让后台创建密钥而需要准备的参数,一个是alias,一个是password,它们都是用户填写的
  2. 1.2是调用后台用于创建密钥的接口,把keyData传过去,并且拿到返回的resp后,进行后续的处理
  3. 2.1是为了让后台创建帐户而需要准备的参数,分别是root_xpubsquorumalias,其中root_xpubs是创建密钥后返回的公钥,quorum目前不知道(TODO),alias是用户填写的帐户别名
  4. 2.2这一句没有作用(经过官方确认了),因为我在代码中没有找到处理CREATE_REGISTER_KEY的代码。可以看这个issue#28
  5. 2.3调用后台创建帐户,把accountData传过去,可以拿到返回的resp
  6. 2.4调用成功后,再使用redux的dispatch函数分发一个CREATE_REGISTER_ACCOUNT信息。不过这个信息好像也没有太大用处。

关于CREATE_REGISTER_ACCOUNT,我在代码中找到了两处相关:

  1. src/features/core/reducers.js#L229-L234
const accountInit = (state = false, action) => {
  if (action.type == 'CREATE_REGISTER_ACCOUNT') {
    return true
  }
  return state
}
  1. src/features/app/reducers.js#L10-L115
export const flashMessages = (state = {}, action) => {
  switch (action.type) {
    // ...
    case 'CREATE_REGISTER_ACCOUNT': {
      return newSuccess(state, 'CREATE_REGISTER_ACCOUNT')
    }
    // ...
  }
}

第一个看起来没什么用,第二个应该是用来在操作完成后,显示相关的错误信息。

那就让我们把关注点放在1.22.3这两个后台调用的地方吧。

  1. chainClient().mockHsm.keys.create(keyData)对应的是:

src/sdk/api/mockHsmKeys.js#L3-L31

const mockHsmKeysAPI = (client) => {
  return {
    create: (params, cb) => {
      let body = Object.assign({}, params)
      const uri = body.xprv ? '/import-private-key' : '/create-key'

      return shared.tryCallback(
        client.request(uri, body).then(data => data),
        cb
      )
    },
    // ...
  }
}

可以看到在create方法中,如果找不到body.xprv(就是本文对应的情况),则会调用后台的/create-key接口。经过一长串的跟踪,我们终于找到了一个。

  1. chainClient().accounts.create(accountData)对应的是:

src/sdk/api/accounts.js#L3-L30

const accountsAPI = (client) => {
  return {
    create: (params, cb) => shared.create(client, '/create-account', params, {cb, skipArray: true}),
    // ...
  }
}

很快我们在这边,也找到了创建帐户时调用的接口为/create-account

前端这边,我们终于分析完了。下一步,将进入比原的节点(也就是后端)。

后端:比原的后端是如何接收到数据的?

如果我们对前一篇文章还有印象的话,会记得比原在启动之后,会在Node.initAndstartApiServer方法中启动web api对应的http服务,并且在API.buildHandler()方法中会配置很多的功能点,其中一定会有我们这里调用的接口。

让我们看看API.buildHandler方法:

api/api.go#L164-L244

func (a *API) buildHandler() {
    walletEnable := false
    m := http.NewServeMux()

    if a.wallet != nil {
        walletEnable = true
        // ...
        m.Handle("/create-account", jsonHandler(a.createAccount))
        // ...
        m.Handle("/create-key", jsonHandler(a.pseudohsmCreateKey))
        // ...

很快,我们就发现了:

  1. /create-account: 对应a.createAccount
  2. /create-key: 对应a.pseudohsmCreateKey

让我们先看一下a.pseudohsmCreateKey

api/hsm.go#L23-L32

func (a *API) pseudohsmCreateKey(ctx context.Context, in struct {
    Alias    string `json:"alias"`
    Password string `json:"password"`
}) Response {
    // ...
}

可以看到,pseudohsmCreateKey的第二个参数,是一个struct,它有两个字段,分别是AliasPassword,这正好和前面从前端传过来的参数keyData对应。那么这个参数的值是怎么由提交的JSON数据转换过来的呢?上次我们说到,主要是由a.pseudohsmCreateKey外面套着的那个jsonHandler进行的,它会处理与http协议相关的操作,以及把JSON数据转换成这里需要的Go类型的参数,pseudohsmCreateKey就可以直接用了。

由于在这个小问题中,我们问题的边界是比原后台是如何拿到数据的,所以我们到这里就可以停止对这个方法的分析了。它具体是怎么创建密钥的,这在以后的文章中将详细讨论。

再看a.createAccount

api/accounts.go#L15-L30

// POST /create-account
func (a *API) createAccount(ctx context.Context, ins struct {
    RootXPubs []chainkd.XPub `json:"root_xpubs"`
    Quorum    int            `json:"quorum"`
    Alias     string         `json:"alias"`
}) Response {
    // ...
}

与前面一样,这个方法的参数RootXPubsQuorumAlias也是由前端提交,并且由jsonHandler自动转换好的。

当我们清楚了在本文中,前后端数据是如何交互的,就很容易推广到更多的情景。在前端还在很多的页面和表单,在很多地方都需要调用后端的接口,我相信按照本文的思路,应该都可以快速的找到。如果有比较特殊的情况,我们以后会再专门写文章讲解。

相关文章
|
3天前
|
前端开发 Java Go
从前端到后端:构建现代化Web应用的技术实践
本文将介绍如何通过前端和后端技术相结合,构建现代化Web应用的技术实践。我们将探讨前端开发、后端架构以及多种编程语言(如Java、Python、C、PHP、Go)在构建高效、可扩展的Web应用中的应用。
|
4天前
|
前端开发 JavaScript Java
npm与Maven:前端与后端构建工具深度对比学习
npm与Maven:前端与后端构建工具深度对比学习
|
4天前
|
前端开发 JavaScript NoSQL
从前端到后端:构建全栈开发者的必备技能
随着互联网技术的不断发展,全栈开发者的需求日益增长。本文将介绍如何从前端到后端,掌握全栈开发所需的关键技能,包括前端框架的选择、后端语言的学习以及数据库的应用,帮助读者构建成为全面的技术专家。
|
5天前
|
前端开发 关系型数据库 MySQL
SpringBoot-----从前端更新数据到MySql数据库
SpringBoot-----从前端更新数据到MySql数据库
12 1
|
5天前
|
前端开发 测试技术 持续交付
《跨界合作:前端与后端如何优化协作效率》
在当今软件开发领域,前端和后端开发团队通常是分开工作的,但他们的协作质量直接影响着项目的成功与否。本文将探讨如何通过优化前端与后端的协作方式,提高开发效率和项目质量,从而实现更好的跨界合作。
|
5天前
|
JSON JavaScript 前端开发
vue的 blob文件下载文件时,后端自定义异常,并返回json错误提示信息,前端捕获信息并展示给用户
vue的 blob文件下载文件时,后端自定义异常,并返回json错误提示信息,前端捕获信息并展示给用户
|
5天前
|
小程序
外卖小程序-购物车模块表结构设计和后端代码
外卖小程序-购物车模块表结构设计和后端代码
14 0
|
5天前
|
前端开发 小程序 测试技术
前端后端测试接口mork神器,Apifox使用一分钟入门
前端后端测试接口mork神器,Apifox使用一分钟入门
13 0
|
5天前
|
存储 安全 前端开发
PHP医院安全不良事件管理系统源码(AEMS)前端vue2+element+后端laravel8不良事件上报与闭环管理
医院不良事件上报与管理系统结合现代医院管理思路,遵照PDCA全面质量循环管理方法而设计,并在多家大型三甲医院成熟运用。系统从事件上报、基于人、机、料、法 、环的RCA分析、事件整改、效果评估实现了结构化、标准化、智能化的管理和分析,满足医院可追溯化、全流程闭环管理要求,满足等级医院评审细则要求,大力提高医院不良事件上报的效率,保障事件分析的准确性,促进医疗安全的提高,避免同类事件再次发生,改善整个医院医疗安全,从而实现医院安全医疗的目标。
26 3
|
5天前
|
JSON 前端开发 搜索推荐
BoostCompass( http_server 模块 | 项目前端代码 )
BoostCompass( http_server 模块 | 项目前端代码 )
30 4