阅读这篇文章之前,可以先拜读一下这篇文章 React 进阶之高阶组件 了解一下高阶组件的概念及几种使用方式。

高阶函数科普

引自 React 进阶之高阶组件

什么是高阶函数

我们都知道高阶函数是什么, 高阶组件其实是差不多的用法,只不过传入的参数变成了react组件,并返回一个新的组件.
A higher-order component is a function that takes a component and returns a new component.

什么是属性代理

const propsProxyHoc = WrappedComponent => class extends Component {

handleClick() {
console.log('click')
}

render() {
return (<WrappedComponent
{...this.props}
handleClick={this.handleClick}
/>)
}
}

如上代码块,我们用高阶组件返回了一个全新的组件,此时该高阶组件可以拿到传进来的 props,然后再把 props 传给 render 渲染函数返回的组件。这也就是 props proxy(属性代理,和大多数方向的代理的概念类似),通俗的理解就是很直白的不同过任何继承的方式加工处理并返回的一种方式。

背景介绍

我是做 ReactJS 的时候,偶然帮同事写一个多表单项目的部分模块,发现当前已有的项目代码上,已经堆积了无数个表单页面以及对应 mobx store,在观察了每一个表单对应的页面结构、行为、以及每一个 store 的结构、甚至 API 都极其类似,不仅让人联想到了用高阶组件来重新组织所有表单结构,需要调研的大致就是以下几个问题:

1. 各个字段默认值的问题:如果所有表单都由同一个高阶组件处理,那么每个表单对应的字段是什么?是不是需要去每个子页面中再写一次?
答: 其实就算是之前的写法,都不需要在 store 中罗列出所有的字段值,除非需要给对应字段添加默认值,而不添加的话,我们的组件在接收到 undefined 的时候,绝大多数组件都是可以正常运行的,个别我们自己写的组件只需要保证不会 crash 即可,这样我们就只需要在 store 中设置一个能够被监听变化的空对象——observable.map({})
2. 如果对应页面的部分字段需要默认值怎么办?
答: 我们只需要在对应页面的构造函数中进行默认字段的添加即可。虽然在代码层面上看起来是很多个页面通过高阶组件使用了同一个 store,但实质上在内存层面看的话,它还是 N 多个页面各自有各自的 store 实例,因此我们不需要考虑多个页面之间的相互影响,只需要做两件事,在 store 中添加一个 injectFields 方法,在对应页面的构造函数中给我们的空对象注入需要默认值的字段即可。
3. 每个页面提交、修改、获取数据的接口都不同,该怎么办呢?
答:经过观察,每一个页面的接口都几乎一致,只是对应的 id 和 type 不同,我们只需要在进入该页面的时候,将对应的参数传入,这样在高阶组件中就可以获取到,从而调用对应的方法接口即可。

开始施工

具体思路及设结构

  1. 我们可以让所有页面都使用同一个 mobx store,利用组件可以接收 undefined 的特点,在 mobx store 中使用一个 observable 空对象即可完成所有字段的初始化。
  2. 设计高阶组件,职责主要有:
    1. 实例化 store 对象,然后在 render 中使用 Provider 注入即可
    2. 统一管理页面行为,如 onSubmit、onSave 等
    3. 部分相同的页面结构(如页头、页脚)可以接在高阶组件中
    4. 在 props 中吸取从路由中传递过来的参数,可以用来在 onSubmit 或者 onSave 中调用接口的时候,拼接 API url,或者干脆将这些参数直接放到 store 的构造函数中进行实例化。
    5. 用一些 state 状态量来控制按钮的状态
  3. 一些页面如果需要给组件设置默认字段时,我们只需要在对应页面的构造函数中添加即可。

整个结构的设计思路可能和继承父组件类似,在当初设计的时候,也曾经被困扰过,担心各个页面都去给我们的 “空对象” 注入字段时,会相互感染,后来从内存和继承的角度上分析了一下,我们的每一个页面都会被高阶组件重新组织,而高阶组件在生成页面实例的时候,还是会为每一个页面生成各自的实例,因此每一个页面还是拥有自己的 store,我们在访问新页面的时候,对应的 store 依旧是局部 store,生命周期依然和页面绑定,各自页面不存在共享一个高阶组件实例以及全局 store 的概念

store 的设计

store 中基本没什么可讲的,大致结构如下:

export default class {
@observable data = observable.map({...})
constructor () {
}
...
injectFields (fields) {
this.data.merge(fields)
}
}

  1. data 就是我们核心的表单数据,初始值里可以放表单共有的一些字段
  2. 构造函数里可以放有关表单的一些独特的信息,如表单的类型、id 等信息,这些信息可以来源于路由,也可以来源于一些全局的配置信息。
  3. injectFields 可以是我们在构建具体页面 Component 的时候,在构造函数里,通过该方法,给 data 里注入一些必要的默认字段,这里的 merge 方法是 MobX 提供的合并对象的方法
  4. 接下来就是一些表单验证处理的方法,以及,一些 API 相关的请求,需要注意的是,如果你的 data 中包含一层以上的对象结构,需要使用由 MobX 直接导出的 toJS 方法而非 data 对象提供的 toJS 方法,前者是一个深转化为 JS 对象,而后者则是浅转化,具体参考对象的 deep and shallow copy。

高阶组件的设计

export default WrapperedComponent => class extends Component {
constructor (props) {
super(props)
this.store = new Store(...)
}

render () {
return (
<Provider formStore={this.store}>
// 这里你可以加入一些页面结构,统一样式
// 甚至可以是表单的提交和取消动作
<WrapperedComponent {...this.props} />
</Provider>
)
}
}

这里是给高阶组件赋予了一些页面结构相关的职能,如果各页面提交、取消等动作结构上有很大的差异,我们则可以将对应的 UI 部分放到对应页面的实现中,然后通过属性注入的方式,将我们统一的 action 注入到 WrapperedComponent 中。

结语

直觉告诉我,可能文章并没有写完,我现在应该是处于一个消息不对等的状态,导致很多对读者来说非常有必要的细节我觉得没必要讲,所以有什么疑问请大家在评论里直接留言讨论~

donation