内容简介:本文是React造轮系列第二篇。对话框一般是我们点击按钮弹出的这么一个东西,主要类型有
本文是React造轮系列第二篇。
想阅读更多优质文章请 猛戳GitHub博客 ,一年百来篇优质文章等着你!
UI
对话框一般是我们点击按钮弹出的这么一个东西,主要类型有 Alter
, Confirm
及 Modal
, 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
运行效果
显示内容
上述还有问题,我们 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>
运行起来你会发现有个警告:
主要是说我们渲染数组时,需要加个 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>
运行效果:
发现遮罩并没有遮住 666 的内容。这是为什么?
看结构也很好理解,遮罩元素与 666 是同级结构,且层级比 666 低,当然是覆盖不了的。那咱们可能就会这样做,给 .fui-dialog-mask
设置一个 zIndex
比它大的呗,如 9999
。
效果:
恩,感觉没问题,这时我们在 Dialog 组件在嵌套一层 zIndex 为 9
的呢,如:
<div style={{position:'relative', zIndex: 9, background:'#fff'}}> <Dialog visible={x}> ... </Dialog> </div>
运行效果如下:
发现,父元素被压住了,里面元素 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
运行效果:
当然这样,如果 Dialog 层级比同级的 zIndex 小的话,还是覆盖不了。 那 zIndex
一般设置成多少比较合理。一般 Dialog 这层设置成 1
, mask 这层设置成 2
。定的越小越好,因为用户可以去改。
zIndex 的管理
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} ...
运行效果:
但有个问题,因为对话框的 visible 是由外部传入的,且 React 是单向数据流的,在组件内并不能直接修改 visible,所以在 onClose 方法我们需要再次渲染一个新的组件,并设置新组件 visible
为 ture
,覆盖原来的组件:
... 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 类型。
运行效果:
这还有个问题,如果需要加按钮呢,可能会这样写:
<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
定义。
总结
- scopedClass 高阶函数的使用
- <Fragment>
- 传送门 portal
- 动态生成组件
- 闭包传 API
本组件为使用优化样式,如果有兴趣可以自行优化,本节源码已经上传至 这里 中的 lib/dialog
。
你的点赞是我持续分享好东西的动力,欢迎点赞!
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 使用导航组件: 对话框目的地
- 使用HTML5原生对话框元素,轻松创建模态框组件
- Android进度对话框
- VBScript - 弹出“文件选择对话框”方法大全!
- jQuery实现定时隐藏对话框的方法分析
- 使用React手写一个对话框或模态框
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
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》 这本书的介绍吧!