在深入react 技术栈一书中,提到了基于Decorator的HOC。而不是直接通过父组件来逐层传递props,因为当业务逻辑越来越复杂的时候,props的传递和维护也将变得困难且冗余。
书里对基于Decorator的HOC没有给出完整的实现,在这里实现并记录一下实现的思路。
整个实现的代码放到了我的Github上,是用来获取豆瓣的电影列表的,npm start
即可开箱。
整体思路
书里描述的整体思路,先将整个组件,按照view逻辑抽象为互不重叠的最小的原子组件,使组件间组合更自由。在这里最小的组件就是SearchInput
SelectInput
List
。原子组件一定是纯粹的、木偶式的组件,如果他们自身带有复杂的交互/业务逻辑,那么在组合起来以后可想需要修改多少个原子组件,也就失去了相对配置式的优势。
组件实现
原子组件
这是对原书代码稍加修改的SearchInput原子组件,因为没加Icon,所以改了一下(逃),整体思路不变。原子组件没什么可说的,木偶组件就是接收props来实现功能,是对view层逻辑的抽象。
需要一提的是displayName
,是用来确定组件的『身份』的,会被包裹它的组合组件用到,后面会提到组合组件。
export default class SearchInput extends PureComponent {
static displayName = 'SearchInput'
render() {
const { onSearch, placeholder } = this.props
return (
<div>
<p>SearchSelect</p>
<div>
<Input
type="text"
placeholder={placeholder}
onChange={onSearch}
/>
</div>
</div>
)
}
}
Decorator组件
先放代码
const searchDecorator = WrappedComponent => {
class SearchDecorator extends Component {
constructor(props) {
super(props)
this.handleSearch = this.handleSearch.bind(this)
this.state = {
keyword: ''
}
}
handleSearch(e) {
this.setState({
keyword: e.target.value
})
this.props.onSearch(e)
}
render() {
const { keyword } = this.state
return (
<WrappedComponent
{...this.props}
data={this.props.data}
keyword={keyword}
onSearch={this.handleSearch}
/>
)
}
}
Decorator的作用就是将业务/交互逻辑抽象出来进行了处理,view的逻辑还是交由原子组件来实现,可以看到最后的render
渲染的还是wrappedComponent
,只不过是在经过Decorator之后多了几个props,这些props的中有钩子函数,有要传递给原子组件的参数。
这样,视图逻辑就由view层抽象,交互/业务逻辑由Decorator来抽象。
组合组件
先上代码。
export default class Selector extends Component {
render() {
return (
<div>
{
this.props.children.map((item) => {
// SelectInput
if (item.type.displayName === 'SelectInput') {
...
}
// SearchInput
if (item.type.displayName === 'SearchInput') {
return React.cloneElement(item,
{
key: 'searchInput',
onSearch: this.props.onSearch,
placeholder: this.props.searchPlaceholder
}
)
}
// List
if (item.type.displayName === 'List') {
...
})
}
</div>
)
}
}
组合组件的children为根据不同业务需要包裹起来的原子组件,组合组件的逻辑处理功能来自于Decorator,各种Decorator的钩子函数或者参数作为props传递给了Selector,Selector再用它们去完成原子组件之间的交互。组合组件通过之前提到的displayName
为不同的原子组件分配props并根据业务需要进行组件间逻辑交互的调整。
一个 Decorator 只做最简单的逻辑,只是给组件增加一个原子的智能特性。业务组件通过组织和拼接 Decorator 来实现功能,而不是改变 Decorator 本身的逻辑。
当我们业务逻辑变得复杂的时候,不要去增加Decorator的复杂度,而是去拼接多个Decorator再通过组合组件去处理具体的业务逻辑,这样能保证Decorator的可复用性。
业务组件
const FinalSelector = compose(asyncSelectDecorator, selectedItemDecorator, searchDecorator)(Selector)
class SearchSelect extends Component {
render() {
return (
<FinalSelector {...this.props}>
<SelectInput />
<SearchInput />
<List />
</FinalSelector>
)
}
}
class App extends Component {
render() {
return (
<SearchSelect
searchPlaceholder={'请搜索电影'}
onSearch={(e) => { console.log(`自定义onSearch: ${e.target.value}`) }}
onClick={(text) => { console.log(`自定义onClick: ${text}`) }}
url="/v2/movie/in_theaters"
/>
)
}
}
通过compose
赋予组合组件不同的逻辑处理功能,然后根据业务需要让compose
后的组合组件包含原子组件,最后给从最外层传递参数就完成了。
tips
在实际的场景中也不能滥用HOC,基于Decorator的HOC一般是用来处理偏数据逻辑的部分,而DOM相关的东西就直接简单粗暴的用父组件就好了。
对比 HOC 范式 compose(render)(state) 与父组件(Parent Component)的范式 render(render(state)),如果完全利用 HOC 来实现 React 的 implement,将操作与 view 分离,也未尝不可,但却不优雅。HOC 本质上是统一功能抽象,强调逻辑与 UI 分离。但在实际开发中,前端无法逃离 DOM ,而逻辑与 DOM 的相关性主要呈现 3 种关联形式:
- 与 DOM 相关,建议使用父组件,类似于原生 HTML 编写
- 与 DOM 不相关,如校验、权限、请求发送、数据转换这类,通过数据变化间接控制 DOM,可以使用 HOC 抽象
- 交叉的部分,DOM 相关,但可以做到完全内聚,即这些 DOM 不会和外部有关联,均可
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。