资讯专栏INFORMATION COLUMN

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

qianfeng / 1499人阅读

摘要:本文是造轮系列第二篇。实现方式事件处理跟差不多,唯一多了一步就是当点击或者的时候,如果外部有回调就需要调用对应的回调函数。

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

1.React 造轮子系列:Icon 组件思路

本轮子是通过 React + TypeScript + Webpack 搭建的,至于环境的搭建这边就不在细说了,自己动手谷歌吧。当然可以参考我的源码。

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

UI

对话框一般是我们点击按钮弹出的这么一个东西,主要类型有 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 (
    
) }

dialog/dialog.tsx

import React from "react"

interface Props {
  visible: boolean
}

const Dialog: React.FunctionComponent = (props) => {
  return (
    props.visible ? 
      
dialog
: null ) } export default Dialog

运行效果

显示内容

上述还有问题,我们 dialog 在组件内是写死的,我们想的是直接通过组件内包裹的内容,如:

// dialog/dialog.example.tsx
...

  hi

...

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

// dialog/dialog.tsx
...
return (
    props.visible ? 
      
{props.children}
: null ) ...
显示遮罩

通常对话框会有一层遮罩,通常我们大都会这样写:

// dialog/dialog.tsx
...
props.visible ? 
  
{props.children}
: null ...

这种结构有个不好的地方就是点击遮罩层的时候要关闭对话框,如果是用这种结构,用户点击任何 div,都相当于点击遮罩层,所以最好要分开:

// dialog/dialog.tsx
...
{props.children}
...

由于 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) => {
  return (
    props.visible ? 
     
        
{props.children}
: 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) => {
  return (
    props.visible ? 
      
          
提示
{props.children}
: 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("-")
}

调用方式如下:

  ....
  
      
提示
{props.children}
...

大家在想法,这样写是有问题,每个组件都写一个函数吗,如果 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
...
 {setX(false)}}>1,
    ,
  ]
}>
  
hi
...

咱们看到这个,第一反应应该是觉得这样写很麻烦,我写个 dialog, visible要自己,按钮要自己,连事件也要自己写。请接受这种设定。虽然麻烦,但非常的好理解。这跟 Vue 的理念是不太一样的。当然后面会进一步骤优化。

组件内渲染如下:

{ props.buttons }

运行起来你会发现有个警告:

主要是说我们渲染数组时,需要加个 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 同级在加以下这么元素:

666
...

运行效果:

发现遮罩并没有遮住 666 的内容。这是为什么?

看结构也很好理解,遮罩元素与 666 是同级结构,且层级比 666 低,当然是覆盖不了的。那咱们可能就会这样做,给.fui-dialog-mask设置一个 zIndex 比它大的呗,如 9999

效果:

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

...

运行效果如下:

发现,父元素被压住了,里面元素 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,
  onClose: React.MouseEventHandler,
  closeOnClickMask?: boolean
}

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

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

  const onClickClose: React.MouseEventHandler = (e) => {
    props.onClose(e)
  }
  const onClickMask: React.MouseEventHandler = (e) => {
    if (props.closeOnClickMask) {
      props.onClose(e)
    }
  }
  const x = props.visible ? 
  
      
提示
{props.children}
{ props.buttons.map((button, index) => { React.cloneElement(button, {key: index}) }) }
: 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 直接弹出一个对话框这样简单方便。如

  

example 3

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

// dialog/dialog.tsx
...
const alert = (content: string) => {
  const component =  {}}>
    {content}
  
  const div = document.createElement("div")
  document.body.append(div)
  ReactDOM.render(component, div)
}

export {alert}
...

运行效果:

但有个问题,因为对话框的 visible 是由外部传入的,且 React 是单向数据流的,在组件内并不能直接修改 visible,所以在 onClose 方法我们需要再次渲染一个新的组件,并设置新组件 visibleture,覆盖原来的组件:

...
const alert = (content: string) => {
  const component =  {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
  }}>
    {content}
  
  const div = document.createElement("div")
  document.body.append(div)
  ReactDOM.render(component, div)
}
..

便利的 API 之 confirm

confirm 调用方式:


第一个参数是显示的内容,每二个参数是确认的回调,第三个参数是取消的回调函数。

实现方式:

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 = (
   { onNo()}}
    buttons={[, 
              
            ]}
  >
    {content}
  )
  const div = document.createElement("div")
  document.body.appendChild(div)
  ReactDOM.render(component, div)
}

事件处理跟 Alter 差不多,唯一多了一步就是 confirm 当点击 yes 或者 no 的时候,如果外部有回调就需要调用对应的回调函数。

便利的 API 之 modal

modal 调用方式:


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

实现方式:

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

注意,这边的 content 类型。

运行效果:

这还有个问题,如果需要加按钮呢,可能会这样写:

  
  )}}>modal

这样是关不了的,因为 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 = 
    {content}
  
  const div = document.createElement("div")
  document.body.appendChild(div)
  ReactDOM.render(component, div)
  return onClose;
}

最后多了一个 retrun onClose,由于闭包的作用,外部调用返回的 onClose 方法可以访问到内部变量。

调用方式:

const openModal = () => {
  const close = modal(

你好

) }
重构 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, afterClose?: () => void) => {
  const close = () => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
    afterClose && afterClose()
  }
  const component = 
   {
      close(); afterClose && afterClose()
    }}
    buttons={buttons}
  >
    {content}
  
  const div = document.createElement("div")
  document.body.append(div)
  ReactDOM.render(component, div)
  return close
}

alert 重构后的代码如下:

const alert = (content: string) => {
  const 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 = [
    , 
    
  ]
  const close =  modal(content, buttons, no)
}

modal 重构后的代码如下:

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

最后发现其实 x 方法就是 modal 方法,所以更改 x 名为 modal,删除对应的 modal 定义。

总结

scopedClass 高阶函数的使用

传送门 portal

动态生成组件

闭包传 API

本组件为使用优化样式,如果有兴趣可以自行优化,本节源码已经上传至这里中的lib/dialog

参考

方应杭老师的React造轮子课程

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/109833.html

相关文章

  • React造轮系列:Layout 组件思路

    摘要:本文是造轮系列第三篇。造轮子系列组件思路造轮系列对话框组件思路想阅读更多优质文章请猛戳博客一年百来篇优质文章等着你初始化参考组件分别分为五个组件。参考方应杭老师的造轮子课程交流干货系列文章汇总如下,觉得不错点个,欢迎加群互相学习。 本文是React造轮系列第三篇。 1.React 造轮子系列:Icon 组件思路 2.React造轮系列:对话框组件 - Dialog 思路 想阅读更多优质...

    neroneroffy 评论0 收藏0
  • 读zent源码库之Dialog组件实现

    摘要:但是,最后一步,事件怎么绑定呢这块没有深入研究了,不过我想,应该这样去实现也是没有问题的。的具体做法是,把方法放到了一个叫做的组件上去实现这个功能,然后再把内容放进这个组件。其他的逻辑比如显示隐藏之类,全部都放到组件自身上去实现。 1、Dialog组件提供什么功能,解决什么问题? zent的Dialog组件,使用姿势是这样的(代码摘自zent官方文档:https://www.youza...

    陈江龙 评论0 收藏0
  • 为什么使用react

    摘要:为什么会慢呢因为对的修改为影响网页的用户界面,重绘页面是一项昂贵的操作。太多的操作会导致一系列的重绘操作,为了确保执行结果的准确性,所有的修改操作是按顺序同步执行的。回流操作主要会发生在几种情况下当对节点执行新增或者删除操作时。 一. 函数式编程 React 把用户界面抽象成一个个组件,如按钮组件 Button、对话框组件 Dialog、日期组件 Calendar。 开发者通过组...

    Towers 评论0 收藏0
  • 一款简单到极致的 React 数据流框架——Refast

    摘要:经过派发器,调用回调,修改数据层。这是一个循环往复的过程,最大的特点是数据单向流动。是团队开发并开源的一款数据流框架。它是一些修改组件状态的函数大体与类似,一般也称之为集合。的第一个参数始终是。简单到极致,就是我们设计的初衷。 Flux 是 React 框架的好伴侣。它优秀的单向数据流设计,使得数据的流向更加清晰,能帮助开发者更好的管理和调试组件的内部状态。Facebook 官方出 F...

    laznrbfe 评论0 收藏0
  • 前端每周清单第 48 期:Slack Webpack 构建优化,CSS 命名规范与用户追踪,Vue.

    摘要:发布是由团队开源的,操作接口库,已成为事实上的浏览器操作标准。本周正式发布,为我们带来了,,支持自定义头部与脚部,支持增强,兼容原生协议等特性变化。新特性介绍日前发布了大版本更新,引入了一系列的新特性与提升,本文即是对这些变化进行深入解读。 showImg(https://segmentfault.com/img/remote/1460000012940044); 前端每周清单专注前端...

    sean 评论0 收藏0

发表评论

0条评论

qianfeng

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<