likes
comments
collection
share

React的组件封装新规范,应该以HTML的结构树进行填写!!

作者站长头像
站长
· 阅读数 43

公众号 傻梦兽IT有话说 欢迎大家进行一个关注,给我一点一点小努力!

近一年的code review的工作中,需要制作一些高可用的业务场景组件。那我的首要工作当然也是要写高可用的组件库啦。

在这一段的工作里面,我发现一个问题,像我们这些中小企业来讲一般都是基于antd的组件进行开发。当然react也给出了一套自己的组件封装规范。

可重用的组件是什么样的?

我们以antdmodal组件为例子。按照antd的案例代码来演示。我们平时写代码的时候是怎么阅读的呢?大概是如下情况。

<Modal 
    visible={visible} 
    title="Title" 
    onOk={this.handleOk} 
    onCancel={this.handleCancel} 
    footer={[ 
    <Button key="back" onClick={this.handleCancel}> Return </Button>,
    <Button key="submit" type="primary" loading={loading} onClick={this.handleOk}> Submit </Button>, 
    <Button key="link" href="https://google.com" type="primary" loading={loading} onClick={this.handleOk} > Search on Google </Button>, ]} > 
    <p>Some contents...</p>
    <p>Some contents...</p> 
    <p>Some contents...</p> 
    <p>Some contents...</p>
    <p>Some contents...</p> 
</Modal>

我们在进行抽象组件的时候,把很多的行为,使用参数的形式进行了传递,从而进行功能上的多态行为实现。

这么做的问题是什么呢?

有没有发现我们在阅读代码的时候,这种写法有点变扭?我吗需要去看一个title的结构的时候是不知道这个组件是长什么样子的。或者footer的这些样子我们也需要进行文档的api查阅后才知道它是用来做什么!我们只有一个children阅读起来比较可观。

在这里我比较期待看到的结果是类似html的结构数据。就类似下面的代码,可读性的同时,部分的ui我们也可以进行控制render。我们追随到bootstrap的modal。

<div class="modal fade" tabindex="-1" role="dialog">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
        <h4 class="modal-title">Modal title</h4>
      </div>
      <div class="modal-body">
        <p>One fine body&hellip;</p>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Save changes</button>
      </div>
    </div><!-- /.modal-content -->
  </div><!-- /.modal-dialog -->
</div><!-- /.modal -->

这个时候我就在想,我们有没有方式把Modal变成以上的方式呢?

<Modal> 
    <ModalOpenButton> <button>Open Modal</button> </ModalOpenButton>
    
    <ModalContents aria-label="Modal label (for screen readers)">
    
    <ModalDismissButton> 
        <button>Close Modal</button> 
    </ModalDismissButton> 
    
    <h3>Modal title</h3> 
        <div>Some great contents of the modal</div> 
    </ModalContents> 
</Modal>

但是,这并不是从代码量上看起来更复杂了。我们已将控制组件行为的能力赋予给了组件的使用者而不是创建者,这称为控制反转。它肯定比我们现有的 antd Modal 组件有更多的代码,但它更简单,更灵活,适合我们未来的用例,而且不会变得更加复杂。

例如,考虑这样一种情况:我们不想只渲染表单,而是想要渲染我们喜欢的任何内容。我们的 Modal 支持这一点,但 Modal 需要接受一个新的参数。或者,如果我们希望关闭按钮显示在内容的下方,该怎么办?我们需要一个名为 renderCloseBelow 的特殊参数。但是对于我们的 Modal,这显而易见可以轻松做到。你只需将 ModalCloseButton 组件移动到所需的位置即可。

更加灵活,更少的接口暴露。

创建我们的第一个复合组件

我们只需要使用到Context的一点点小修改即可完成我们的操作。

import * as React from 'react'
import VisuallyHidden from '@reach/visually-hidden'

/* Here the Dialog and CircleButton is a custom component 
Dialog is nothing button some styles applied on reach-dialog 
component provided by @reach-ui */
import {Dialog, CircleButton} from './lib'

const ModalContext = React.createContext()
//this helps in identifying the context while visualizing the component tree
ModalContext.displayName = 'ModalContext'

function Modal(props) {
  const [isOpen, setIsOpen] = React.useState(false)

  return <ModalContext.Provider value={[isOpen, setIsOpen]} {...props} />
}

function ModalDismissButton({children: child}) {
  const [, setIsOpen] = React.useContext(ModalContext)
  return React.cloneElement(child, {
    onClick: () => setIsOpen(false),
  })
}

function ModalOpenButton({children: child}) {
  const [, setIsOpen] = React.useContext(ModalContext)
  return React.cloneElement(child, {
    onClick: () => setIsOpen(true),
})
}

function ModalContentsBase(props) {
  const [isOpen, setIsOpen] = React.useContext(ModalContext)
  return (
    <Dialog isOpen={isOpen} onDismiss={() => setIsOpen(false)} {...props} />
  )
}

function ModalContents({title, children, ...props}) {
  return (
    //we are making generic reusable component thus we allowed user custom styles
   //or any prop they want to override
    <ModalContentsBase {...props}>
      <div>
        <ModalDismissButton>
          <CircleButton>
            <VisuallyHidden>Close</VisuallyHidden>
            <span aria-hidden>×</span>
          </CircleButton>
        </ModalDismissButton>
      </div>
      <h3>{title}</h3>
      {children}
    </ModalContentsBase>
  )
}

export {Modal, ModalDismissButton, ModalOpenButton, ModalContents}

在使用组件的过程中,我们就可以获取到一个完整的html结构树。

<Modal>
     <ModalOpenButton>
         <Button>Login</Button>
     </ModalOpenButton>
     <ModalContents aria-label="Login form" title="Login">
         <LoginForm
            onSubmit={register}
            submitButton={<Button>Login</Button>}
          />
      </ModalContents>
 </Modal>

ModalOpenButton的位置和布局你可以随意的放置,也不用去实现onClick: () => setIsOpen(true),;

当然如果你想让Button Loing 拥有onclick的事件的时候,可以参考这种思路

const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn(...args))

只需要把以上的代码修改成

function ModalOpenButton({children: child}) {
  const [, setIsOpen] = React.useContext(ModalContext)
  return React.cloneElement(child, {
    onClick: callAll(() => setIsOpen(true), child.props.onClick),
  })
}
<ModalOpenButton>
  <button onClick={() => console.log('sending data to facebook ;)')}>Open Modal</button>
</ModalOpenButton>

总结

不要匆忙地就进行组件的抽象,也不要把一切都留给参数。也许它现在是一个简单的组件,但你不知道将来需要实现哪些用例,不要认为这是时间和可维护性之间的权衡,复杂性可能会呈指数级增长。

在 React 中发挥复合组件的优势,让你的生活更轻松。

转载自:https://juejin.cn/post/7093729295364947981
评论
请登录