本文是React造轮系列第二篇。css
本轮子是经过 React + TypeScript + Webpack 搭建的,至于环境的搭建这边就不在细说了,本身动手谷歌吧。固然能够参考个人源码。html
想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!前端
对话框通常是咱们点击按钮弹出的这么一个东西,主要类型有 Alter
, Confirm
及 Modal
, Modal 通常带有半透明的黑色背景。固然外观可参考 AntD 或者 Framework 等。vue
API 方面主要仍是要参考同行,由于若是有一天,别人想你用的UI框架时,你的 API 跟他以前经常使用的又不用,这样就加大了入门门槛,因此API 尽可能保持跟现有的差很少。react
对话框除了提供显示属性外,还要有点击确认后的回放函数,如:git
alert('你好').then(fn)
confirm('肯定?').then(fn)
modal(组件名)
复制代码
Dialog 源码已经上传到这里。github
dialog/dialog.example.tsx, 这里 state ,生命周期使用 React 16.8 新出的 Hook,若是对 Hook 不熟悉能够先看官网文档。api
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('-')
}
复制代码
调用方式以下: .... <div className={scopedClass('mask')}>
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 通常是属于最高级的。
上述咱们使用 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)
}
..
复制代码
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
的时候,若是外部有回调就须要调用对应的回调函数。
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>
复制代码
在重构以前,咱们先要抽象 alert, confirm, modal 中各自的方法:
从表格能够看出,modal 与其它两个只多了一个 retrun api,其实其它两个也能够返回对应的 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
定义。
本组件为使用优化样式,若是有兴趣能够自行优化,本节源码已经上传至这里中的lib/dialog
。
方应杭老师的React造轮子课程
干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。
我是小智,公众号「大迁世界」做者,对前端技术保持学习爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复福利,便可看到福利,你懂的。