说到React的组件化,可能许多人第一印象就是写一个React.Component
,再简单不过。我也问过一部分同学,说:
我正在做React组件化,你知道React怎么组件化么?
他们很惊讶:React天生不就是为组件化的么?组件可以定义props
和state
,状态改变了引发组件的重绘,组件之间并不影响。
我说好,那现在如果有一个组件,我从这个工程拷出来,粘贴到另一个工程,然后代码跑不起来了。原因是这个组件需要一个list
属性,它包含一个某种数据结构的列表,那种数据结构也未知,总之这个组件迁移过后各种报错和undefined!怎么解决?
“组件只是视图层,至于数据层面,需要自顶向下下发,这个list数据,应该是要发一个ajax去获取吧?”
“嗯对,你提到一个概念,自顶向下,为什么要自顶向下呢?如果底层任意一个组件有改动,最顶层的组件也要改动罗?”
“差不多是这样的。”
“嗯,那我要做的就是解决这个问题。”
回到“我们做了什么”的问题上来,我们所有的探索,都是为了减少组件迁移带来的额外工作量,进而让页面由组件拼接这种开发模式成为可能,再进而,我们会做一个平台,拖拖拽拽出一个React工程(注意我没有说“一个页面”)成为可能。
围绕这个目标,借鉴已有的前端GUI开发经验(多数并不是来自于React方面的实践),我们做的工作包括:反向依赖、状态隔离、无actionType化、禁止依赖检查。
在一般的React实践中,视图层和数据层的依赖都是正向的。视图层的正向依赖可以举例为:组件B是组件A的子元素,那么需要再组件A中显示声明组件B的存在。
import B from './B'
//...
render(){
return <div>
<B />
</div>
}
数据层的正向依赖可以举例为:子组件B的数据需要父组件A自顶向下数据发放。
import B from './B'
//...
render(){
return <div>
<B list={this.props.list}/>
</div>
}
在我们的探索中,发现视图层的正向依赖很浅,依赖变动带来的工作量小,是可以接受的。例如,组件B被删除了,页面不需要显示组件B的内容,如果组件A不做改动,代码肯定是报错的,因为找不到B。我们直接将import B from './B'
等删除即可,并不会带来太多的工作量。
但是数据层的正向依赖往往很深。例如,组件B被拷贝到了其他工程里,迁移过后各种报错、各种undefined。我们只能去找这个组件B需要哪些props字段,然后从顶层下发给它,如果没有所需的数据,还得单独拉一个ajax请求去获取组件B需要的数据。有时候我们会用React的connect方法直接注入,但组件多了,会偶现connect注入的属性重名的情况,一片凌乱。
所以,一定得将数据层的正向依赖关系拆开。
解决办法是将正向依赖反过来。由父组件A声明某种服务接口,然后子组件B按需依赖父组件B的服务接口。这种设计模式有些地方也叫做“依赖注入(dependence inject)”。
// 伪代码高能注意!!!
B.require('A', 'data_list', (list)=>{
// .....
})
光有反向依赖还不够,对于组件B,它可能依赖于一个ajax接口,但是整个页面可能只有组件B会用到这个ajax接口。如果组件B从工程中移走,就一定势必剩下一部分和B相关的代码保留在工程中。且如果B组件仍然需要上级的某些属性传入,组件迁移后还是不能直接使用。
我们希望一个组件能够向插件一样被拔下来,同时能够在另一个工程里即插即用。要达到这一点,诸如以下的写法是不提倡的:
return <div>
<B name={this.props.name}>
</div>
因为如果组件B迁移了,开发者很可能不知道this.props.name
怎么取值。所以,我们希望将一定粒度的组件无props化,去除组件与上层,同级组件的任何依赖,在render函数中只有添加一个tag标签就可以使用:
return <div>
<B />
</div>
我们只希望用三个标签来完成,不带任何props:
return <div>
<Logo />
<Tabs />
<Content />
</div>
有些人可能会说,这还不简单么?只要用redux的connect方法封装一下,就不用给这个jsx标签添加props了。其实这样做表面上是没有props了,实际上,组件的数据仍然来自于顶层,依赖同样存在,组件迁移后仍然不可用。我们要做到的是:即没有自顶向下的数据依赖,又没有标签上的props传入。
我们的做法也很简单,既然不带任何props,上面例子中,Content中明显是一个列表,如果顶层没有将列表传入,就必须保证Content
组件自己持有这个列表。对于列表的所有操作都封装在这个Content组件中,保持数据的独立性。这就是我们达到的“无props化”。这个做法最初也叫做状态隔离,后来我觉得“没有props”这样的表述一听就懂,就换成了“无props化”
这一点是针对redux来说的。但凡使用React的工程,都会选择一个状态管理工具。Redux使用者较多,我们也是其中一员。Redux中使用action和reducer的概念进行事件分发和数据组装。对于开发同学来说,这个操作过于繁琐(不是复杂)了。例如,我们会创建若干看起来一模一样的action.js
,其中写了无数看起来一模一样的action_type
。然后创建对应的看起来差不多的reducer.js
,引用action.js中的那个常量actiontype。这波操作从我最开始接触reducer的时候就觉得过于恶心。没错,保持事件分发有助于解耦,但是action和reducer的写法过于冗余,代码可读性缺失,一眼望去全是看起来一模一样的常量名。于是在这次组件化进程中我们简化了这种写法,抽象出来一个叫做fk-action-type
的工具,最后我们的action-reducer语法变成了这样:
store.listen('fetch', {
action: name => params => cgiAction(name, {
url: params.url,
data: params.data,
}),
reducer: name => ({
[name+'_success']: (state, action) => {
return newState;
}
})
})
如果一切都是透传,那么可以省略对应的action和reducer:
store.listen('setTab', {
reducer: plainReducer((state, action)=>{
return Object.assign({}, state, {tabIndex: action.data})
})
})
以往一百行的代码,现在30行就能写完。
在我们的组件化中,依赖并不是直接引用的。会存在一个完全解耦的依赖声明。例如,组件B依赖组件A拉取ajax数据后的返回结果,会这样声明:
storeB.listenOther('A.fetch_success', (action)=>{
// do something with action dispatched by A
})
如果组件B被迁移到其他工程,以上代码可能跟着迁移过去。如果其他工程没有A,或者有A但是没有fetch_success
事件,虽然这段代码不会引起任何错误,但我们希望这些无用的依赖声明越少越好,保持开发者良好的组件迁移习惯,记得删除无用的依赖声明很重要。要做到这一点其实也很容易,我们是可以通过静态的依赖检查给与开发者提示,“无用的监听B:当前工程并没有A”,或者“无用的监听B:A并不会发出fetch_success事件”,等等。