剝開比原看代碼17:比原是如何顯示交易的詳細資料的?

來源:互聯網
上載者:User

作者:freewind

比原項目倉庫:

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

Gitee地址:https://gitee.com/BytomBlockc...

在上上篇文章裡,我們還剩下一個小問題沒有解決,即前端是如何顯示一個交易的詳細資料的。

先看對應的圖片:


這個圖片由於太長,分成了兩個,實際上可以看作一個。

那麼這個頁面是怎麼來的呢?這是在前面以列表的方式顯示交易摘要資訊後,可以點擊摘要資訊右上方的“查看詳情”連結開啟。

那我們在本文看一下,比原是如何顯示這個交易的詳細資料的。

由於它分成了前後兩端,那麼我們跟以前一樣,把它再分成兩個小問題:

  1. 前端是怎麼向後台發送請求,並顯示資料的
  2. 後端是如何拿到相應的資料發送給前台的

需要說明的是,這個表格中包含了很多資訊,但是我們在本文並不打算去解釋。因為能看懂的一看就能明白,看不懂的就需要準確的瞭解了比原的核心之後才能解釋清楚,而這一塊等到我們晚點再專門研究。

前端是怎麼向後台發送請求,並顯示資料的

首先我們看一下顯示交易詳細資料頁面的路由path是多少。當我們把滑鼠放在交易摘要頁面右上方的“查看詳情”時,會發現url類似於:

http://localhost:9888/dashboard/transactions/2d94709749dc59f69cad4d6aea666586d9f7e86b96c9ee81d06f66d4afb5d6dd

其中http://localhost:9888/dashboard/可以看作是這個應用的根路徑,那麼路由path應該就是/transactions/2d94709749dc59f69cad4d6aea666586d9f7e86b96c9ee81d06f66d4afb5d6dd,後面那麼長的顯然是一個id,所以我們應該到代碼中尋找類似於/transactions/:id這樣的字串,哦,遺憾的是沒有找到。。。

那隻能從頭開始了,先找到前端路由的定義:

src/routes.js#L15-L35

// ...import { routes as transactions } from 'features/transactions'// ...const makeRoutes = (store) => ({  path: '/',  component: Container,  childRoutes: [    // ...    transactions(store),    // ...  ]})

其中的transactions就是我們需要的,而它對應了features/transactions/routes.js

src/features/transactions/routes.js#L1-L21

import { List, New, AssetShow, AssetUpdate } from './components'import { makeRoutes } from 'features/shared'export default (store) => {  return makeRoutes(    store,    'transaction',    List,    New,    Show,    // ...  )}

這個函數將會為transactions產生很多相關的路由路徑。當我們把一些組件,比如列表顯示List,建立New,顯示詳情Show等等傳進去之後,makeRoutes就會按照預先定義好的路徑規則去添加相關的path。我們看一下makeRoutes

src/features/shared/routes.js#L1-L44

import { RoutingContainer } from 'features/shared/components'import { humanize } from 'utility/string'import actions from 'actions'const makeRoutes = (store, type, List, New, Show, options = {}) => {  const loadPage = () => {    store.dispatch(actions[type].fetchAll())  }  const childRoutes = []  if (New) {    childRoutes.push({      path: 'create',      component: New    })  }  if (options.childRoutes) {    childRoutes.push(...options.childRoutes)  }  // 1.   if (Show) {    childRoutes.push({      path: ':id',      component: Show    })  }  return {    // 2.        path: options.path || type + 's',    component: RoutingContainer,    name: options.name || humanize(type + 's'),    name_zh: options.name_zh,    indexRoute: {      component: List,      onEnter: (nextState, replace) => {        loadPage(nextState, replace)      },      onChange: (_, nextState, replace) => { loadPage(nextState, replace) }    },    childRoutes: childRoutes  }}

這段代碼看起來眼熟,因為我們在之前研究餘額和交易的列表顯示的時候,都見過它。而我們今天關注的是Show,即標記為第1處的代碼。

可以看到,當傳進來了Show組件時,就需要為其產生相關的路由path。具體是在childRouters中添加一個path:id,而它本身的路由path是在第2處定義的,預設為type + 's',而對於本例來說,type的值就是transaction,所以Show所對應的完整path就是/transactions/:id,正是我們所需要的。

再回到第1處代碼,可以看到Show組件是從外部傳進來的,從前面的函數可以看到它對應的是src/features/transactions/components/Show.jsx

我們進去看一下這個Show.jsx,首先是定義html組件的函數render

src/features/transactions/components/Show.jsx#L16-L96

class Show extends BaseShow {  render() {    // 1.    const item = this.props.item    const lang = this.props.lang    const btmAmountUnit = this.props.btmAmountUnit    let view    if (item) {      // ..      view = <div>        <PageTitle title={title} />        <PageContent>          // ...          <KeyValueTable            title={lang === 'zh' ? '詳情' : 'Details'}            items={[              // ...            ]}          />          {item.inputs.map((input, index) =>            <KeyValueTable              // ...            />          )}          {item.outputs.map((output, index) =>            <KeyValueTable              // ...            />          )}        </PageContent>      </div>    }    return this.renderIfFound(view)  }}

代碼被我進行了大量的簡化,主要是省略了很多資料的計算和一些顯示組件的參數。我把代碼分成了2部分:

  1. 第1處需要注意的是類似於const item = this.props.item這樣的代碼,這裡的item就是我們要展示的資料,對應本文就是一個transaction對象,它是從this.props中拿到的,所以我們可以推斷在這個檔案(或者引用的某個檔案)中,會有一個connect方法,把store裡的資料塞過來。一會兒我們去看看。後面兩行類似就不說了。
  2. 第2處代碼主要就是頁面view的定義了,可以看到裡面主要是用到了另一個自訂群組件KeyValueTable。代碼我們就不跟過去了,參照前面的頁面效果我們可以想像出來它就是以表格的形式把一些key-value資料顯示出來。

那我們繼續去尋找connect,很快就在同一個頁面的後面,找到了如下的定義:

src/features/transactions/components/Show.jsx#L100-L117

import { actions } from 'features/transactions'import { connect } from 'react-redux'const mapStateToProps = (state, ownProps) => ({  item: state.transaction.items[ownProps.params.id],  lang: state.core.lang,  btmAmountUnit: state.core.btmAmountUnit,  highestBlock: state.core.coreData && state.core.coreData.highestBlock})// ...export default connect(  mapStateToProps,  // ...)(Show)

我只留下了需要關注的mapStateToProps。可以看到,我們在前面第1處中看到的幾個變數的賦值,在這裡都有定義,其中最重要的item,是從store的目前狀態state中的transaction中的items中取出來的。

那麼state.transaction是什麼呢?我開始以為它是我們從後台取回來的一些資料,使用transaction這個名字放到了store裡,結果怎麼都搜不到,最後終於發現原來不是的。

實際情況是,在我們定義reducer的地方,有一個makeRootReducer

src/reducers.js#L1-L62

// ...import { reducers as transaction } from 'features/transactions'// ...const makeRootReducer = () => (state, action) => {  // ...  return combineReducers({    // ...    transaction,    // ...  })(state, action)}

原來它是在這裡構建出來的。首先{ transaction }這種ES6的文法,換成平常的寫法,就是:

{  transaction: transaction}

另外,combineReducers這個方法,是用來把多個reducer合并起來(可能是因為store太大,所以把它拆分成多個reducer管理,每個reducer只需要處理自己感興趣的部分),並且合并以後,這個store就會變成大概這樣:

{    "transaction": { ... },    // ...}

所以前面的state.transaction就是指的這裡的{ ... }

那麼繼續,在前面的代碼中,可以從state.transaction.items[ownProps.params.id]看到,state.transaction還有一個items的屬性,它持有的是向後台/list-transactions取回的一個transaction數組,它又是什麼時候加上去的呢?

這個問題難倒了我,我花了幾個小時搜遍了比原的前後端倉庫,都沒找到,最後只好使出了Chrome的Redux DevTools大法,發現在一開始的時候,items就存在了:

在圖上有兩個紅框,左邊的表示我現在選擇的是初始狀態,右邊顯示最開始transaction就已經有了items,於是恍然大悟,這不跟前面是一樣的道理嘛!於是很快找到了定義:

src/features/transactions/reducers.js#L7-L16

export default combineReducers({  items: reducers.itemsReducer(type),  queries: reducers.queriesReducer(type),  generated: (state = [], action) => {    if (action.type == 'GENERATED_TX_HEX') {      return [action.generated, ...state].slice(0, maxGeneratedHistory)    }    return state  },})

果然,這裡也是用combineReducers把幾個reducer組合在了一起,所以store裡就會有這裡的幾個key,包括items,以及我們不關心的queriesgenerated

花了一下午,終於把這塊弄清楚了。看來對於分析動態語言,一定要腦洞大開,不能預設原因,另外要利用各種調試工具,從不同的角度去查看資料。要不是Redux的Chrome外掛程式,我不知道還要卡多久。

我個人更喜歡靜態類型的語言,對於JavaScript這種,除非萬不得以,能躲就躲,主要原因就是代碼中互相引用的線索太少了,很多時候必須看文檔、代碼甚至去猜,無法利用編輯器提供的跳轉功能。

知道了state.transaction.items的來曆以後,後面的事情就好說了。我們是從state.transaction.items[ownProps.params.id]拿到了當前需要的transaction,那麼state.transaction.items裡又是什麼時候放進去資料的呢?

讓我們再回到前面的makeRoutes

src/features/shared/routes.js#L1-L44

// ...import actions from 'actions'const makeRoutes = (store, type, List, New, Show, options = {}) => {  // 2.  const loadPage = () => {    store.dispatch(actions[type].fetchAll())  }  // ...  return {    path: options.path || type + 's',    component: RoutingContainer,    name: options.name || humanize(type + 's'),    name_zh: options.name_zh,    indexRoute: {      component: List,      onEnter: (nextState, replace) => {        loadPage(nextState, replace)      },      // 1.       onChange: (_, nextState, replace) => { loadPage(nextState, replace) }    },    childRoutes: childRoutes  }}

在上面的第1處,對於indexRoute,有一個onChange的觸發器。它的意思是,當路由的path改變了,並且新的path屬於當前的這個index路由的path(或者子path),後面的函數將會觸發。而後面函數中的loadPage的定義在第2處代碼,它又會將actions[type].fetchAll()產生的action進行dispatch。由於type在本文中是transaction,通過一步步追蹤(這裡稍有點麻煩,不過我們在之前的文章中已經走過),我們發現actions[type].fetchAll對應了src/features/shared/actions/list.js

src/features/shared/actions/list.js#L4-L147

export default function(type, options = {}) {  const listPath  = options.listPath || `/${type}s`  const clientApi = () => options.clientApi ? options.clientApi() : chainClient()[`${type}s`]  // ...  const fetchAll = () => {    // ...  }  // ...  return {    // ...    fetchAll,    // ...  }}

如果我們還對這一段代碼有印象的話,就會知道它最後將會去訪問背景/list-transactions,並在拿到資料後調用dispatch("RECEIVED_TRANSACTION_ITEMS"),而它將會被下面的這個reducer處理:

src/features/shared/reducers.js#L6-L28

export const itemsReducer = (type, idFunc = defaultIdFunc) => (state = {}, action) => {  if (action.type == `RECEIVED_${type.toUpperCase()}_ITEMS`) {    // 1.    const newObjects = {}    // 2.    const data = type.toUpperCase() !== 'TRANSACTION' ? action.param.data : action.param.data.map(data => ({      ...data,      id: data.txId,      timestamp: data.blockTime,      blockId: data.blockHash,      position: data.blockIndex    }));    // 3.     (data || []).forEach(item => {      if (!item.id) { item.id = idFunc(item) }      newObjects[idFunc(item)] = item    })    return newObjects  }  // ...  return state}

依次講解這個函數中的三處代碼:

  1. 第1處是建立了一個新的Null 物件newObjects,它將在最後替代state.transaction.items,後面會向它裡面賦值
  2. 第2處是對傳進來的資料進行一些處理,如果type是transaction的話,會把數組中每個元素中的某些屬性提升到根下,方便使用
  3. 第3處就是把各個元素放到newObjects中,id為key,對象本身為value

經過這些處理以後,我們才能使用state.transaction.items[ownProps.params.id]拿到合適的transaction對象,並且由Show.jsx顯示。

前端這塊基本上弄清楚了。我們繼續看後端

後端是如何拿到相應的資料發送給前台的

前面我們說過,根據以往的經驗,我們可以推匯出前端會訪問後端的/list-transactions這個介面。我們欣喜的發現,這個介面我們正好在前一篇文章中研究過,這裡就可以完全跳過了。

到今天為止,我們終於把“比原是如何建立一個交易的”這件事的基本流程弄清楚了。雖然還有很多細節,以及觸及到核心的知道都被忽略了,但是感覺自己對於比原內部的運作似乎又多了一些。

也許現在積累的知識差不多了,該向比原的核心進發了。在下一篇,我將會嘗試理解和分析比原的核心,在學習的過程中,可能會採用跟目前探索流程分解問題不同的方式。另外,可能前期會花不少時間,所以下一篇出來得會晚一些。當然,如果失敗了,說明我目前積累的知識還是不夠,我還需要再回到當前的做法,想辦法再從不同的地方多剝一些比原的外殼,然後再嘗試。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.