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

做者:freewindhtml

比原項目倉庫:前端

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

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

在上上篇文章裏,咱們還剩下一個小問題沒有解決,即前端是如何顯示一個交易的詳細信息的。github

先看對應的圖片:redux

這個圖片因爲太長,分紅了兩個,實際上能夠看做一個。後端

那麼這個頁面是怎麼來的呢?這是在前面以列表的方式顯示交易摘要信息後,能夠點擊摘要信息右上角的「查看詳情」連接打開。數組

那咱們在本文看一下,比原是如何顯示這個交易的詳細信息的。編輯器

因爲它分紅了先後兩端,那麼咱們跟之前同樣,把它再分紅兩個小問題:函數

  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處是建立了一個新的空對象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這個接口。咱們欣喜的發現,這個接口咱們正好在前一篇文章中研究過,這裏就能夠徹底跳過了。

到今天爲止,咱們終於把「比原是如何建立一個交易的」這件事的基本流程弄清楚了。雖然還有不少細節,以及觸及到核心的知道都被忽略了,可是感受本身對於比原內部的運做彷佛又多了一些。

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

相關文章
相關標籤/搜索