React造轮系列:对话框组件 - Dialog 思绪

本文是React造轮系列第二篇。

本轮子是经由过程 React + TypeScript + Webpack 搭建的,至于环境的搭建这边就不在细说了,本身着手谷歌吧。固然能够参考我的源码

想浏览更多优良文章请猛戳GitHub博客,一年百来篇优良文章等着你!

UI

《React造轮系列:对话框组件 - Dialog 思绪》

对话框平常是我们点击按钮弹出的这么一个东西,重要范例有 Alter, ConfirmModal, Modal 平常带有半透明的黑色背景。固然表面可参考 AntD 或许 Framework 等。

肯定 API

API 方面重要照样要参考偕行,因为假如有一天,他人想你用的UI框架时,你的 API 跟他之前经常运用的又不必,如许就加大了入门门坎,所以API 只管坚持跟现有的差不多。

对话框除了供应显现属性外,还要有点击确认后的回放函数,如:

alert('你好').then(fn)
confirm('肯定?').then(fn)
modal(组件名)

完成

Dialog 源码已上传到这里

dialog/dialog.example.tsx, 这里 state ,生命周期运用 React 16.8 新出的 Hook,假如对 Hook 不熟悉能够先看官网文档

dialog/dialog.example.tsx

import React, {useState} from 'react'
import Dialog from './dialog'
export default function () {
  const [x, setX] = useState(false)
  return (
    <div>
      <button onClick={() => {setX(!x)}}>点击</button>
      <Dialog visible={x}></Dialog>
    </div>
  )
}

dialog/dialog.tsx

import React from 'react'

interface Props {
  visible: boolean
}

const Dialog: React.FunctionComponent<Props> = (props) => {
  return (
    props.visible ? 
      <div>dialog</div> : 
      null
  )
}

export default Dialog

运转结果

《React造轮系列:对话框组件 - Dialog 思绪》

显现内容

上述另有题目,我们 dialog 在组件内是写死的,我们想的是直接经由过程组件内包裹的内容,如:

// dialog/dialog.example.tsx
...
<Dialog visible={x}>
  <strong>hi</strong>
</Dialog>
...

如许写,页面上是不会显现 hi 的,这里 children 属性就派上用场了,我们须要在 dialog 组件中进一步骤修正以下内容:

// dialog/dialog.tsx
...
return (
    props.visible ? 
      <div>
        {props.children}
      </div>
      : 
      null
)
...

显现遮罩

一般对话框会有一层遮罩,一般我们大都会如许写:

// dialog/dialog.tsx
...
props.visible ? 
  <div className="fui-dialog-mask">
    <div className="fui-dialog">
    {props.children}
    </div>
  </div>
  : 
  null
...

这类构造有个不好的处所就是点击遮罩层的时刻要封闭对话框,假如是用这类构造,用户点击任何 div,都相当于点击遮罩层,所以最好要离开:

// dialog/dialog.tsx
...
<div>
    <div className="fui-dialog-mask">
    </div>
    <div className="fui-dialog">
      {props.children}
    </div>
 </div>
...

因为 React 请求最外层只能有一个元素, 所以我们多用了一个 div 包裹起来,然则这类要领无形之中多了个 div,所以能够运用 React 16 以后新出的 Fragment, Fragment 跟 vue 中的 template 一样,它是不会衬着到页面的。

import React, {Fragment} from 'react'
import './dialog.scss';
interface Props {
  visible: boolean
}

const Dialog: React.FunctionComponent<Props> = (props) => {
  return (
    props.visible ? 
     <Fragment>
        <div className="fui-dialog-mask">
        </div>
        <div className="fui-dialog">
          {props.children}
        </div>
     </Fragment>
      : 
      null
  )
}

export default Dialog

完美头部,内容及底部

这里不多说,直接上代码

import React, {Fragment} from 'react'
import './dialog.scss';
import {Icon} from '../index'
interface Props {
  visible: boolean
}

const Dialog: React.FunctionComponent<Props> = (props) => {
  return (
    props.visible ? 
      <Fragment>
          <div className="fui-dialog-mask">
          </div>
          <div className="fui-dialog">
            <div className='fui-dialog-close'>
              <Icon name='close'/>
            </div>
            <header className='fui-dialog-header'>提醒</header>
            <main className='fui-dialog-main'>
              {props.children}
            </main>
            <footer className='fui-dialog-footer'>
              <button>ok</button>
              <button>cancel</button>
            </footer>
          </div>
      </Fragment>
      : 
      null
  )
}

export default Dialog

从上述代码我们能够发明我们写款式的名字时刻,为了不被第三运用掩盖,我们自定义了一个 fui-dialog前缀,在写每一个款式称号时,都要写一遍,如许明显不太合理,万一哪天我不必这个前缀时刻,每一个都要改一遍,所以我们须要一个要领来封装。

我们能够会写如许要领:

function scopedClass(name) {
  return `fui-dialog-${name}`
}

如许写不可,因为我们 name 能够不传,如许就会多出一个 -,所以须要进一步的推断:

function scopedClass(name) {

  return `fui-dialog-${name ? '-' + name : ''}`
}

那另有无更简约的要领,运用 filter 要领:

function scopedClass(name ?: string) {
  return ['fui-dialog', name].filter(Boolean).join('-')
}

挪用体式格局以下:

  ....
  <Fragment>
      <div className={scopedClass('mask')}>
      </div>
      <div className={scopedClass()}>
        <div className={scopedClass('close')}>
          <Icon name='close'/>
        </div>
        <header className={scopedClass('header')}>提醒</header>
        <main className={scopedClass('main')}>
          {props.children}
        </main>
        <footer className={scopedClass('footer')}>
          <button>ok</button>
          <button>cancel</button>
        </footer>
      </div>
  </Fragment>
 ...

人人在主意,如许写是有题目,每一个组件都写一个函数吗,假如 Icon 组件,我还须要写一个 fui-icon, 解决要领是把 前缀当一个参数,如:

function scopedClass(name ?: string) {
  return ['fui-dialog', name].filter(Boolean).join('-')
}

挪用体式格局以下:

className={scopedClass('fui-dialog', 'mask')}

如许写,还不如直接写款式,这类体式格局是即是白写了一个要领,那怎样办?这就须要高阶函数进场了。完成以下:

function scopeClassMaker(prefix: string) {
  return function (name ?: string) {
    return [prefix, name].filter(Boolean).join('-')
  }
}

const scopedClass = scopeClassMaker('fui-dialog')

scopeClassMaker 函数是高等函数,返回一个带了 prefix 参数的函数。

事宜处置惩罚

在写事宜处置惩罚之前,我们 Dialog 须要吸收一个 buttons 属性,就是显现的操纵按钮并增加事宜:

// dialog/dialog.example.tsx
...
<Dialog visible={x} buttons = {
  [
    <button onClick={()=> {setX(false)}}>1</button>,
    <button onClick={()=> {setX(false)}}>2</button>,
  ]
}>
  <div>hi</div>
</Dialog>
...

我们看到这个,第一回响反映应当是以为如许写很贫苦,我写个 dialog, visible要本身,按钮要本身,连事宜也要本身写。请接收这类设定。虽然贫苦,但异常的好明白。这跟 Vue 的理念是不太一样的。固然后面会进一步骤优化。

组件内衬着以下:

<footer className={sc('footer')}>
  {
    props.buttons
  }
</footer>

运转起来你会发明有个正告:

《React造轮系列:对话框组件 - Dialog 思绪》

重如果说我们衬着数组时,须要加个 key,解决要领有两种,就是不要运用数组体式格局,固然这不治本,所以这里 React.cloneElemen 进场了,它能够克隆元素并增加对应的属性值,以下:

{
  props.buttons.map((button, index) => {
    React.cloneElement(button, {key: index})
  })
}

对应的点击封闭事宜相对轻易这边就不讲了,能够自行检察源码

接下来来看一个款式的题目,起首先给出我们遮罩的款式:

.fui-dialog {
  position: fixed; background: white; min-width: 20em;
  z-index: 2;
  border-radius: 4px; top: 50%; left: 50%; transform: translate(-50%, -50%);
  &-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%;
    background: fade_out(black, 0.5);
    z-index: 1;
  }
  .... 以下省略别的款式
}

我们遮罩 .fui-dialog-mask 运用 fixed 定位觉得是没题目的,那假如在挪用 dialog 同级在加以下这么元素:

<div style={{position:'relative', zIndex: 10, background:'#fff'}}>666</div>
   
<button onClick={() => {setX(!x)}}>点击</button>
<Dialog visible={x}>
...
</Dialog>

运转结果:

《React造轮系列:对话框组件 - Dialog 思绪》

发明遮罩并没有遮住 666 的内容。这是为何?

《React造轮系列:对话框组件 - Dialog 思绪》

看构造也很好明白,遮罩元素与 666 是同级构造,且层级比 666 低,固然是掩盖不了的。那我们能够就会如许做,给.fui-dialog-mask设置一个 zIndex 比它大的呗,如 9999

结果:

《React造轮系列:对话框组件 - Dialog 思绪》

恩,觉得没题目,这时候我们在 Dialog 组件在嵌套一层 zIndex 为 9 的呢,如:

<div style={{position:'relative', zIndex: 9, background:'#fff'}}>
  <Dialog visible={x}>
    ...
  </Dialog>
</div>

运转结果以下:

《React造轮系列:对话框组件 - Dialog 思绪》

发明,父元素被压住了,内里元素 zIndex 值怎样的高,都没有结果。

那这要怎样破?答案是不要让它出现在任何元素的内里,这怎样能够呢。这里就须要引出一个奇异的 API了。这个 API 叫做 传送门(portal)

用法以下:

return ReactDOM.createPortal(
  this.props.children,
  domNode
);

第一个参数就是你的 div,第二个参数就是你要去的处所。

import React, {Fragment, ReactElement} from 'react'
import ReactDOM from 'react-dom'
import './dialog.scss';
import {Icon} from '../index'
import {scopedClassMaker} from '../classes'

interface Props {
  visible: boolean,
  buttons: Array<ReactElement>,
  onClose: React.MouseEventHandler,
  closeOnClickMask?: boolean
}

const scopedClass = scopedClassMaker('fui-dialog')
const sc = scopedClass

const Dialog: React.FunctionComponent<Props> = (props) => {

  const onClickClose: React.MouseEventHandler = (e) => {
    props.onClose(e)
  }
  const onClickMask: React.MouseEventHandler = (e) => {
    if (props.closeOnClickMask) {
      props.onClose(e)
    }
  }
  const x = props.visible ? 
  <Fragment>
      <div className={sc('mask')} onClick={onClickMask}>
      </div>
      <div className={sc()}>
        <div className={sc('close')} onClick={onClickClose}>
          <Icon name='close'/>
        </div>
        <header className={sc('header')}>提醒</header>
        <main className={sc('main')}>
          {props.children}
        </main>
        <footer className={sc('footer')}>
          {
            props.buttons.map((button, index) => {
              React.cloneElement(button, {key: index})
            })
          }
        </footer>
      </div>
  </Fragment>
  : 
  null
  return (
    ReactDOM.createPortal(x, document.body)
  )
}

Dialog.defaultProps = {
  closeOnClickMask: false
}


export default Dialog

运转结果:

《React造轮系列:对话框组件 - Dialog 思绪》

固然如许,假如 Dialog 层级比同级的 zIndex 小的话,照样掩盖不了。 那 zIndex 平常设置成若干比较合理。平常 Dialog 这层设置成 1, mask 这层设置成2。定的越小越好,因为用户能够去改。

zIndex 的治理

《React造轮系列:对话框组件 - Dialog 思绪》

zIndex 治理平常就是前端架构师要做的了,依据营业产景来分别,如广告肯定是要在页面最上面,所以 zIndex 平常是属于第一流的。

轻易的 API 之 Alert

上述我们运用 Dialog 组件挪用体式格局比较贫苦,写了一堆,有时刻我们想到运用 alert 直接弹出一个对话框如许简朴轻易。如

  <h1>example 3</h1>
  <button onClick={() => alert('1')}>alert</button>

我们想直接点击 button ,然后弹出我们自定义的对话框内容为1 ,须要在 Dialog 组件内我们须要导出一个 alert 要领,以下:

// dialog/dialog.tsx
...
const alert = (content: string) => {
  const component = <Dialog visible={true} onClose={() => {}}>
    {content}
  </Dialog>
  const div = document.createElement('div')
  document.body.append(div)
  ReactDOM.render(component, div)
}

export {alert}
...

运转结果:

《React造轮系列:对话框组件 - Dialog 思绪》

但有个题目,因为对话框的 visible 是由外部传入的,且 React 是单向数据流的,在组件内并不能直接修正 visible,所以在 onClose 要领我们须要再次衬着一个新的组件,并设置新组件 visibleture,掩盖本来的组件:

...
const alert = (content: string) => {
  const component = <Dialog visible={true} onClose={() => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
  }}>
    {content}
  </Dialog>
  const div = document.createElement('div')
  document.body.append(div)
  ReactDOM.render(component, div)
}
..

轻易的 API 之 confirm

confirm 挪用体式格局:

<button onClick={() => confirm('1', ()=>{}, ()=> {})}>confirm</button>

第一个参数是显现的内容,每二个参数是确认的回调,第三个参数是作废的回调函数。

完成体式格局:

const confirm = (content: string, yes?: () => void, no?: () => void) => {
  const onYes = () => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
    yes && yes()
  }
  const onNo = () => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
    no && no()
  }
  const component = (
  <Dialog 
    visible={true} onClose={() => { onNo()}}
    buttons={[<button onClick={onYes}>yes</button>, 
              <button onClick={onNo}>no</button>
            ]}
  >
    {content}
  </Dialog>)
  const div = document.createElement('div')
  document.body.appendChild(div)
  ReactDOM.render(component, div)
}

事宜处置惩罚跟 Alter 差不多,唯一多了一步就是 confirm 当点击 yes 或许 no 的时刻,假如外部有回调就须要挪用对应的回调函数。

轻易的 API 之 modal

modal 挪用体式格局:

<button onClick={() => {modal(<h1>你好</h1>)}}>modal</button>

modal 对应通报的内容就不是单单的文本了,而是元素。

完成体式格局:

const modal = (content: ReactNode | ReactFragment) => {
  const onClose = () => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
  }
  const component = <Dialog onClose={onClose} visible={true}>
    {content}
  </Dialog>
  const div = document.createElement('div')
  document.body.appendChild(div)
  ReactDOM.render(component, div)
}

注重,这边的 content 范例。

运转结果:

《React造轮系列:对话框组件 - Dialog 思绪》

这另有个题目,假如须要加按钮呢,能够会如许写:

 <button onClick={() => {modal(<h1>
     你好 <button>close</button></h1> 
  )}}>modal</button>

如许是关不了的,因为 Dialog 是封装在 modal 内里的。假如要关,必需掌握 visible,那很明显我从表面掌握不了内里的 visible,所以这个 button 没有办法把这个 modal 关掉。

解决要领就是运用闭包,我们能够在 modal 要领内里把 close 要领返回:

const modal = (content: ReactNode | ReactFragment) => {
  const onClose = () => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
  }
  const component = <Dialog onClose={onClose} visible={true}>
    {content}
  </Dialog>
  const div = document.createElement('div')
  document.body.appendChild(div)
  ReactDOM.render(component, div)
  return onClose;
}

末了多了一个 retrun onClose,因为闭包的作用,外部挪用返回的 onClose 要领能够访问到内部变量。

挪用体式格局:

const openModal = () => {
  const close = modal(<h1>你好
    <button onClick={() => close()}>close</button>
  </h1>)
}
<button onClick={openModal}>modal</button>

重构 API

在重构之前,我们先要笼统 alert, confirm, modal 中各自的要领:

alertconfirmmodal
onCloseonClose * 2onClose
componentcomponentcomponent
renderrenderrender
return api

从表格能够看出,modal 与别的两个只多了一个 retrun api,实在别的两个也能够返回对应的 Api,只是我们没去挪用罢了,所以补上:

alertconfirmmodal
onCloseonClose * 2onClose
componentcomponentcomponent
renderrenderrender
return apireturn apireturn api

如许一来,这三个函数从笼统层面上来看是相似的,所以这三个函数应当合成一个。

起首抽取大众部份,先取名为x ,内容以下:

const x= (content: ReactNode, buttons ?:Array<ReactElement>, afterClose?: () => void) => {
  const close = () => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
    afterClose && afterClose()
  }
  const component = 
  <Dialog visible={true} 
    onClose={() => {
      close(); afterClose && afterClose()
    }}
    buttons={buttons}
  >
    {content}
  </Dialog>
  const div = document.createElement('div')
  document.body.append(div)
  ReactDOM.render(component, div)
  return close
}

alert 重构后的代码以下:

const alert = (content: string) => {
  const button = <button onClick={() => close()}>ok</button>
  const close = x(content, [button])
}

confirm 重构后的代码以下:

const confirm = (content: string, yes?: () => void, no?: () => void) => {

  const onYes = () => {
    close()
    yes && yes()
  }
  const onNo = () => {
    close()
    no && no()
  }
  const buttons = [
    <button onClick={onYes}>yes</button>, 
    <button onClick={onNo}>no</button>
  ]
  const close =  modal(content, buttons, no)
}

modal 重构后的代码以下:

const modal = (content: ReactNode | ReactFragment) => {
  return x(content)
}

末了发明实在 x 要领就是 modal 要领,所以变动 x 名为 modal,删除对应的 modal 定义。

总结

  1. scopedClass 高阶函数的运用
  2. <Fragment>
  3. 传送门 portal
  4. 动态天生组件
  5. 闭包传 API

本组件为运用优化款式,假如有兴致能够自行优化,本节源码已上传至这里中的lib/dialog

参考

方应杭先生的React造轮子课程

你的点赞是我延续分享好东西的动力,迎接点赞!

《React造轮系列:对话框组件 - Dialog 思绪》

    原文作者:前端小智
    原文地址: https://segmentfault.com/a/1190000019296556
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞