做者:freewindhtml
比原項目倉庫:前端
Github地址:https://github.com/Bytom/bytomreact
Gitee地址:https://gitee.com/BytomBlockchain/bytomgit
在上上篇文章裏,咱們還剩下一個小問題沒有解決,即前端是如何顯示一個交易的詳細信息的。github
先看對應的圖片:redux
這個圖片因爲太長,分紅了兩個,實際上能夠看做一個。後端
那麼這個頁面是怎麼來的呢?這是在前面以列表的方式顯示交易摘要信息後,能夠點擊摘要信息右上角的「查看詳情」連接打開。數組
那咱們在本文看一下,比原是如何顯示這個交易的詳細信息的。編輯器
因爲它分紅了先後兩端,那麼咱們跟之前同樣,把它再分紅兩個小問題:函數
須要說明的是,這個表格中包含了不少信息,可是咱們在本文並不打算去解釋。由於能看懂的一看就能明白,看不懂的就須要準確的瞭解了比原的核心以後才能解釋清楚,而這一塊等到咱們晚點再專門研究。
首先咱們看一下顯示交易詳細信息頁面的路由path是多少。當咱們把鼠標放在交易摘要頁面右上角的「查看詳情」時,會發現url相似於:
http://localhost:9888/dashboard/transactions/2d94709749dc59f69cad4d6aea666586d9f7e86b96c9ee81d06f66d4afb5d6dd
其中http://localhost:9888/dashboard/
能夠看做是這個應用的根路徑,那麼路由path應該就是/transactions/2d94709749dc59f69cad4d6aea666586d9f7e86b96c9ee81d06f66d4afb5d6dd
,後面那麼長的顯然是一個id,因此咱們應該到代碼中尋找相似於/transactions/:id
這樣的字符串,哦,遺憾的是沒有找到。。。
那隻能從頭開始了,先找到前端路由的定義:
// ... 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部分:
const item = this.props.item
這樣的代碼,這裏的item
就是咱們要展現的數據,對應本文就是一個transaction
對象,它是從this.props
中拿到的,因此咱們能夠推斷在這個文件(或者引用的某個文件)中,會有一個connect
方法,把store裏的數據塞過來。一下子咱們去看看。後面兩行相似就不說了。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
:
// ... 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
,以及咱們不關心的queries
和generated
。
花了一下午,終於把這塊弄清楚了。看來對於分析動態語言,必定要腦洞大開,不能預設緣由,另外要利用各類調試工具,從不一樣的角度去查看數據。要不是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 }
依次講解這個函數中的三處代碼:
newObjects
,它將在最後替代state.transaction.items
,後面會向它裏面賦值transaction
的話,會把數組中每一個元素中的某些屬性提高到根下,方便使用newObjects
中,id
爲key,對象自己爲value通過這些處理之後,咱們才能使用state.transaction.items[ownProps.params.id]
拿到合適的transaction對象,而且由Show.jsx
顯示。
前端這塊基本上弄清楚了。咱們繼續看後端
前面咱們說過,根據以往的經驗,咱們能夠推導出前端會訪問後端的/list-transactions
這個接口。咱們欣喜的發現,這個接口咱們正好在前一篇文章中研究過,這裏就能夠徹底跳過了。
到今天爲止,咱們終於把「比原是如何建立一個交易的」這件事的基本流程弄清楚了。雖然還有不少細節,以及觸及到核心的知道都被忽略了,可是感受本身對於比原內部的運做彷佛又多了一些。
也許如今積累的知識差很少了,該向比原的核心進發了。在下一篇,我將會嘗試理解和分析比原的核心,在學習的過程當中,可能會採用跟目前探索流程分解問題不一樣的方式。另外,可能前期會花很多時間,因此下一篇出來得會晚一些。固然,若是失敗了,說明我目前積累的知識仍是不夠,我還須要再回到當前的作法,想辦法再從不一樣的地方多剝一些比原的外殼,而後再嘗試。