作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 在前面一篇文章,我們粗略的研究了一下比原的dashboard是如何做出來的,但是對里 ...
作者:freewind
比原項目倉庫:
Github地址:https://github.com/Bytom/bytom
Gitee地址:https://gitee.com/BytomBlockchain/bytom
在前面一篇文章,我們粗略的研究了一下比原的dashboard是如何做出來的,但是對裡面提到的各種細節功能,並沒有深入的去研究。那麼從本文開始,我們將在這一段時間,分別研究裡面提到的每一項功能。
在前一篇文章中,當我們第一次在瀏覽器中打開dashboard時,因為還沒有創建過密鑰,所以比原會提示我們輸入一些別名和密碼,為我們創建一個密鑰和相應的帳戶。就是下麵這張圖所對應的:
那麼本文就將研究一下,當我們點擊了"Register"按鈕以後,我們在前端頁面上填寫的參數,到底是如何一步步的傳到比原的後端的。
跟之前一樣,我們將對這個問題進行細分,然後各個擊破:
- 前端:當我們填完表單,點了提交以後,比原在前端是如何發送數據的?
- 後端:比原的後端是如何接收到數據的?
前端:當我們填完表單,點了提交以後,數據會發送到後端的哪個介面?
當我們點擊了"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個數字並不是從上到下標註,而是按照我們關註的順序來的:
- 表單上的各個輸入框,就是我們填寫別名和密碼的地方。這裡需要關註的是每個
TextField
的fieldProps
屬性,它對應我們提交到後臺的數據的name
- 就是那個“Register”按鈕了。需要註意的是,它的
type
是submit
,也就是說,點擊它以後,將會觸發所在form
的onSubmit
方法 - 回到了
form
的開頭。註意它的onSubmit
裡面,調用的是handleSubmit(this.submitWithErrors)
。其中的handleSubmit
是從該表單所使用的第三方redux-form中傳入的,用來處理表單提交,我們在這裡不關註它,只需要知道我們需要把自己的處理函數this.submitWithErrors
傳給它。而在後者中,我們將會調用比原節點提供的web api - 第3步中的
this.submitWithErrors
最終將走到這裡定義的submitWithErrors
函數 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
是為了讓後臺創建密鑰而需要準備的參數,一個是alias
,一個是password
,它們都是用戶填寫的1.2
是調用後臺用於創建密鑰的介面,把keyData
傳過去,並且拿到返回的resp
後,進行後續的處理2.1
是為了讓後臺創建帳戶而需要準備的參數,分別是root_xpubs
,quorum
和alias
,其中root_xpubs
是創建密鑰後返回的公鑰,quorum
目前不知道(TODO),alias
是用戶填寫的帳戶別名2.2
這一句沒有作用(經過官方確認了),因為我在代碼中沒有找到處理CREATE_REGISTER_KEY
的代碼。可以看這個issue#282.3
調用後臺創建帳戶,把accountData
傳過去,可以拿到返回的resp
2.4
調用成功後,再使用redux的dispatch
函數分發一個CREATE_REGISTER_ACCOUNT
信息。不過這個信息好像也沒有太大用處。
關於CREATE_REGISTER_ACCOUNT
,我在代碼中找到了兩處相關:
const accountInit = (state = false, action) => { if (action.type == 'CREATE_REGISTER_ACCOUNT') { return true } return state }
export const flashMessages = (state = {}, action) => { switch (action.type) { // ... case 'CREATE_REGISTER_ACCOUNT': { return newSuccess(state, 'CREATE_REGISTER_ACCOUNT') } // ... } }
第一個看起來沒什麼用,第二個應該是用來在操作完成後,顯示相關的錯誤信息。
那就讓我們把關註點放在1.2
和2.3
這兩個後臺調用的地方吧。
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
介面。經過一長串的跟蹤,我們終於找到了一個。
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
方法:
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)) // ...
很快,我們就發現了:
/create-account
: 對應a.createAccount
/create-key
: 對應a.pseudohsmCreateKey
讓我們先看一下a.pseudohsmCreateKey
:
func (a *API) pseudohsmCreateKey(ctx context.Context, in struct { Alias string `json:"alias"` Password string `json:"password"` }) Response { // ... }
可以看到,pseudohsmCreateKey
的第二個參數,是一個struct
,它有兩個欄位,分別是Alias
和Password
,這正好和前面從前端傳過來的參數keyData
對應。那麼這個參數的值是怎麼由提交的JSON數據轉換過來的呢?上次我們說到,主要是由a.pseudohsmCreateKey
外面套著的那個jsonHandler
進行的,它會處理與http協議相關的操作,以及把JSON數據轉換成這裡需要的Go類型的參數,pseudohsmCreateKey
就可以直接用了。
由於在這個小問題中,我們問題的邊界是比原後臺是如何拿到數據的,所以我們到這裡就可以停止對這個方法的分析了。它具體是怎麼創建密鑰的,這在以後的文章中將詳細討論。
再看a.createAccount
:
// 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 { // ... }
與前面一樣,這個方法的參數RootXPubs
、Quorum
和Alias
也是由前端提交,並且由jsonHandler
自動轉換好的。
當我們清楚了在本文中,前後端數據是如何交互的,就很容易推廣到更多的情景。在前端還在很多的頁面和表單,在很多地方都需要調用後端的介面,我相信按照本文的思路,應該都可以快速的找到。如果有比較特殊的情況,我們以後會再專門寫文章講解。