做者: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
这个接口。咱们欣喜的发现,这个接口咱们正好在前一篇文章中研究过,这里就能够彻底跳过了。
到今天为止,咱们终于把“比原是如何建立一个交易的”这件事的基本流程弄清楚了。虽然还有不少细节,以及触及到核心的知道都被忽略了,可是感受本身对于比原内部的运做彷佛又多了一些。
也许如今积累的知识差很少了,该向比原的核心进发了。在下一篇,我将会尝试理解和分析比原的核心,在学习的过程当中,可能会采用跟目前探索流程分解问题不一样的方式。另外,可能前期会花很多时间,因此下一篇出来得会晚一些。固然,若是失败了,说明我目前积累的知识仍是不够,我还须要再回到当前的作法,想办法再从不一样的地方多剥一些比原的外壳,而后再尝试。