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

栏目: 服务器 · 发布时间: 5年前

内容简介:本文是React造轮系列第二篇。对话框一般是我们点击按钮弹出的这么一个东西,主要类型有

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

想阅读更多优质文章请 猛戳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 中各自的方法:

alert confirm modal
onClose onClose * 2 onClose
component component component
render render render
return api

从表格可以看出,modal 与其它两个只多了一个 retrun api,其实其它两个也可以返回对应的 Api,只是我们没去调用而已,所以补上:

alert confirm modal
onClose onClose * 2 onClose
component component component
render render render
return api return api return 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造轮系列:对话框组件 - Dialog 思路


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

An Introduction to Probability Theory and Its Applications

An Introduction to Probability Theory and Its Applications

William Feller / Wiley / 1991-1-1 / USD 120.00

Major changes in this edition include the substitution of probabilistic arguments for combinatorial artifices, and the addition of new sections on branching processes, Markov chains, and the De Moivre......一起来看看 《An Introduction to Probability Theory and Its Applications》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

SHA 加密
SHA 加密

SHA 加密工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具