Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

基于Decorator的React高阶组件的思路分析与实现 #6

Open
fi3ework opened this issue Feb 4, 2018 · 0 comments
Open

基于Decorator的React高阶组件的思路分析与实现 #6

fi3ework opened this issue Feb 4, 2018 · 0 comments
Labels

Comments

@fi3ework
Copy link
Owner

fi3ework commented Feb 4, 2018

深入react 技术栈 一书中,提到了基于 Decorator 的 HOC。而不是直接通过父组件来逐层传递 props,因为当业务逻辑越来越复杂的时候,props 的传递和维护也将变得困难且冗余。

书里对基于 Decorator 的 HOC 没有给出完整的实现,在这里实现并记录一下实现的思路。

整个实现的代码放到了 我的Github 上,是用来获取豆瓣的电影列表的,npm start 即可。

整体思路

image

书里描述的整体思路,先将整个组件,按照 view 抽象为互不重叠的最小的原子组件,使组件间组合更自由。在这里最小的组件就是 SearchInputSelectInputList。原子组件一定是纯粹的、木偶式的组件,如果他们自身带有复杂的交互/业务逻辑,那么在组合起来以后可想需要修改多少个原子组件,也就失去了相对配置式的优势。

组件实现

原子组件

这是对原书代码稍加修改的 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 的中有钩子函数,有要传递给原子组件的参数。

这样,视图逻辑就由原子组件组成,交互/业务逻辑由 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 不会和外部有关联,均可

参考资料

@fi3ework fi3ework added the React label Feb 4, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant