Практический опыт реализации легковесного и расширяемого модального компонента (Modal)

внешний интерфейс React.js
Практический опыт реализации легковесного и расширяемого модального компонента (Modal)

предисловие

Эта статья является девятой статьей автора по написанию дизайна компонентов.Сегодня я возьму вас за реализацию легкого и гибко настраиваемого модального компонента.Этот компонент появится в сторонних библиотеках компонентов, таких как Antd или elementUI.В основном он используется предоставить пользователям обратную связь о системе.

Причина, по которой я пишу статьи, связанные с компонентным дизайном, заключается в том, что, как отличный фронтенд-инженер, сталкивающийся с различными утомительными и повторяющимися задачами, мы должны «усердно работать» не шаг за шагом, а в соответствии с существующим фронтендом. , подытожил набор собственных эффективных методов развития.

[Примечания] Общая классификация интерфейсных компонентов:

  • Общие компоненты: такие как кнопка, значок и т. д.
  • Компоненты макета: такие как сетка, макет макета и т. д.
  • Навигационные компоненты: такие как Breadcrumb, Dropdown, Menu и т. д.
  • Компоненты ввода данных: такие как формы форм, переключатели Switch, загрузка загружаемых файлов и т. д.
  • Компоненты отображения данных: такие как аватар Аватора, таблица таблиц, список списков и т. д.
  • Компоненты обратной связи: такие как индикатор выполнения, ящик ящика, модальное диалоговое окно и т. д.
  • Другие виды бизнеса

Следовательно, при проектировании системы компонентов мы можем ссылаться на приведенную выше классификацию дизайна, которая также является методом классификации основных библиотек пользовательского интерфейса, таких как antd, element и zend.

текст

Прежде чем приступить к разработке компонентов, я надеюсь, что у вас есть определенные основы в CSS3 и JS и вы понимаете основной синтаксис react/vue.Давайте сначала разберем модальные компоненты.Модал делится на следующие части:

Каждый блок можно настроить индивидуально, а другие компоненты можно комбинировать. Эффект компонента после внедрения:

1. Идеи дизайна компонентов

В соответствии с принципами проектирования компонентов, изложенными автором ранее, нашим первым шагом является подтверждение требований.Модальные компоненты обычно имеют следующие требования:

  • Может управлять стилем модального тела
  • Обеспечивает обратный вызов после того, как модальное окно полностью закрыто
  • Может управлять текстом и стилем кнопки отмены
  • Может управлять текстом и стилем кнопки подтверждения
  • Контролируйте, где отображается модальное окно
  • Определяет, показывать ли кнопку закрытия в правом верхнем углу.
  • Пользовательский значок закрытия может быть настроен
  • Уничтожать ли дочерние элементы в модальном окне при закрытии конфигурации
  • Пользовательское модальное нижнее содержимое
  • Определяет, поддерживается ли закрытие клавиши esc
  • Определяет, показывать ли маску
  • Определяет, разрешено ли закрываться маске щелчка.
  • пользовательский стиль маски
  • пользовательское название
  • Управляет видимостью диалогового окна
  • пользовательская ширина диалогового окна
  • Предоставьте обратный вызов для щелчка по слою маски или правому верхнему крестику или кнопке отмены.
  • Обеспечивает обратный вызов щелчка OK

После того, как требования будут собраны, как начинающий программист, будут нарисованы следующие каркасы:

Для реагирующих игроков, если вы не используете typescript, всем рекомендуется использовать PropTypes.Это встроенный инструмент определения типа в реакции, и мы можем импортировать его непосредственно в проект.Vue имеет собственный метод определения свойств, поэтому я не буду представлять их здесь по одному. .

2. Реализуйте модальный компонент на основе реакции

2.1 Дизайн каркаса модальных компонентов

Во-первых, мы сначала пишем фреймворк компонента в соответствии с требованиями, чтобы потом бизнес-логика была более понятной:

import PropTypes from 'prop-types'
import './index.less'

/**
 * Modal Modal组件
 * @param {afterClose} func Modal完全关闭后的回调
 * @param {bodyStyle} object Modal body的样式
 * @param {cancelText} string|ReactNode 取消按钮文字
 * @param {centered} bool 居中展示Modal
 * @param {closable} bool 是否展示右上角的关闭按钮
 * @param {closeIcon} ReactNode 自定义关闭图标
 * @param {destroyOnClose} bool 关闭时销毁Modal里的子元素
 * @param {footer} null|ReactNode 底部内容,当不需要底部默认按钮时,可以设置为footer={null}
 * @param {keyboard} bool 是否支持键盘的esc键退出
 * @param {mask} bool 是否展示遮罩
 * @param {maskclosable} bool 点击蒙层是否允许关闭
 * @param {maskStyle} object 遮罩样式
 * @param {okText} string|ReactNode 确认按钮的文本
 * @param {title} string|ReactNode 标题内容
 * @param {visible} bool Modal是否可见
 * @param {width} string Modal宽度
 * @param {onCancel} func 点击遮罩或者取消按钮,或者键盘esc按键时的回调
 * @param {onOk} func 点击确定的回调
 */
function Modal(props) {
  const {
    afterClose,
    bodyStyle,
    cancelText,
    centered,
    closable,
    closeIcon,
    destroyOnClose,
    footer,
    keyboard,
    mask,
    maskclosable,
    maskStyle,
    okText,
    title,
    visible,
    width,
    onCancel,
    onOk
  } = props
  return <div className="xModalWrap">
    <div className="xModalContent">
      <div className="xModalHeader">

      </div>
      <div className="xModalBody">

      </div>
      <div className="xModalFooter">

      </div>
    </div>
    <div className="xModalMask"></div>
  </div>
}

export default Modal

С помощью этой структуры давайте реализуем контент шаг за шагом.

2.2 Реализация основных функций конфигурации

Базовая функция настройки часто не связана с бизнес-логикой, и используется только для управления отображением и сокрытием элементов.Поскольку она очень проста в реализации, сначала реализуем функции следующих атрибутов:

  • bodyStyle
  • cancelText
  • closable
  • closeIcon
  • footer
  • mask
  • maskStyle
  • okText
  • title
  • width

Эти функции были частично реализованы после сборки фреймворка, потому что они относительно просты и не требуют другой сложной логики.Необходимо только выставить свойства и использовать их.Конкретная реализация выглядит следующим образом:

// ...
function Modal(props) {
  // ...
  return <div className="xModalWrap">
    <div 
      className="xModalContent"
      style={{
        width
      }}
    >
      <div className="xModalHeader">
        <div className="xModalTitle">
          { title }
        </div>
      </div>
      {
        closable && 
        <span className="xModalCloseBtn">
          { closeIcon || <Icon type="FaTimes" /> }
        </span>
      }
      <div className="xModalBody" style={bodyStyle}>
        { children }
      </div>
      {
        footer === null ? null :
          <div className="xModalFooter">
            {
              footer ? footer :
                <div className="xFooterBtn">
                  <Button className="xFooterBtnCancel" type="pure">{ cancelText }</Button>
                  <Button className="xFooterBtnOk">{ okText }</Button>
                </div>
            }
          </div>
      }
    </div>
    {
      mask && <div className="xModalMask" style={maskStyle}></div>
    }
  </div>
}

С помощью приведенной выше реализации мы можем легко контролировать, какие элементы отображает модальный компонент, а какие элементы могут закрывать модальный.Конкретные случаи следующие:

  1. удалить нижний колонтитул (установив для нижнего колонтитула значение null)
  2. Убираем кнопку закрытия в правом верхнем углу
  3. снять маску

2.3 Реализовать видимость (анимация с всплывающими и скрытыми анимациями)

Друзья, которые знакомы с antd или element, знают, что visible используется для управления отображением и сокрытием модального окна. Мы здесь для достижения той же функции. Что касается анимации скрытия и отображения, мы используем преобразование: масштаб для ее достижения. Давайте сначала посмотрим на эффект:

Здесь автор использует useState API хуков реакции, чтобы установить состояние видимости всплывающего окна, а модальное окно по умолчанию невидимо. Конкретная логика выглядит следующим образом:

let [isHidden, setHidden] = useState(!props.visible)
const handleClose = () => {
    setHidden(false)
}

HTML-структура выглядит следующим образом:

<div className="xModalWrap" style={{display: isHidden ? 'none' : 'block'}}>

Из приведенного выше кода мы знаем, что отображение и скрытие модального окна управляется установкой display:none/block, но все мы знаем, что display:none не может выполнять эффекты анимации. вверх, мы используем @. Анимация по ключевому кадру также обратно совместима с более ранними версиями браузеров. Конкретный код css выглядит следующим образом:

@keyframes xSpread {
    0% {
        opacity: 0;
        transform: scale(0);
    }
    100% {
        opacity: 1;
        transform: scale(1);
    }
}

2.5 Реализовать по центру

Роль свойства centered состоит в том, чтобы управлять положением населения или положением всей маски или видимой области, а значение равно TRUE, а центр маски или видимой области — true. Поскольку расположение области содержимого MODAL в нашей настройке по умолчанию находится слева и справа, верхняя часть составляет 100 пикселей в верхней части видимой области, поэтому мы реализуем следующее:

<div className={`xModalContent${centered ? ' xCentered' : ''}`}>

Код css выглядит следующим образом:

&.xCentered {
    top: 50%;
    transform: translateY(-50%);
}

Эта реализация также очень проста, просто задайте имя класса динамически через атрибут centered.

2.6 Реализовать destroyOnClose

Эта функция означает, очищать ли дочерний элемент при закрытии всплывающего окна, я нахожусь в:«Знакомство с проектированием компонентов react/vue» с React Portals для создания мощного компонента Drawer (Drawer).Эта статья имеет подробное введение.Если вам интересно, вы можете изучить следующее.Здесь я имею в виду процесс внедрения. Когда destroyOnClose имеет значение true, мы можем уничтожить дочерний элемент и повторно отобразить компонент, сохранив состояние. Чтобы реализовать эту функцию, нам нужно обработать следующие события:

  • Когда нажата кнопка закрытия, уничтожьте дочерний компонент в соответствии с destroyOnClose
  • Когда нажата кнопка подтверждения, уничтожьте дочерний компонент в соответствии с destroyOnClose
  • Когда visible равно true, дочерний компонент повторно отображается в соответствии с destroyOnClose. Конкретный код реализации выглядит следующим образом:
// 关闭事件(关闭和确认事件逻辑基本一致,这里就不单独写了)
const handleClose = () => {
    setHidden(true)
    if(destroyOnClose) {
      setDestroyChild(true)
    }
    document.body.style.overflow = 'auto'
    onCancel && onCancel()
}

// visivle/destroyOnClose更新时,重新渲染子组件
useEffect(() => {
    if(visible) {
      if(destroyOnClose) {
        setDestroyChild(true)
      }
    }
  }, [visible, destroyOnClose])

Таким образом мы реализуем функцию уничтожения компонента при закрытии всплывающего окна.

2.7 Закрыть модальное окно (Modal), когда на клавиатуре нажимается ESC

Для лучшего физического исследования пользователя модальный компонент автора поддерживает события клавиатуры.Все мы знаем, что код события, соответствующий ESC клавиатуры, равен 27, поэтому мы можем использовать этот принцип, чтобы закрыть модальное окно, когда клавиатура нажимает ESC:

useEffect(() => {
    document.onkeydown = function (event) {
      let e = event || window.event || arguments.callee.caller.arguments[0]
      if (e && e.keyCode === 27) { 
        handleClose()
      }
    }
  }, [])

Поскольку прослушиватель событий нужно выполнить только один раз, зависимость useEffect может быть установлена ​​на пустой массив. Хотя это в основном реализует функцию закрытия клавиатуры, такой код явно недостаточно элегантен, поэтому давайте улучшим следующее, мы можем извлечь метод закрытия клавиатуры, а затем вернуть другую функцию в первой функции обратного вызова useEffect (эта функция хук перед выгрузкой компонента), когда компонент выгружается, мы удаляем прослушиватель событий, что может немного повысить производительность и помочь в оптимизации памяти:

const closeModal = function (event) {
    let e = event || window.event || arguments.callee.caller.arguments[0]
    if (e && e.keyCode === 27) { 
      handleClose()
    }
  }
  
 useEffect(() => {
    document.addEventListener('keydown', closeModal, false)
    return () => {
      document.removeEventListener('keydown', closeModal, false)
    }
  }, [])

Таким образом, будет ли код и реализация функций более элегантными?

2.8 Реализовать после закрытия

Роль afterClose в основном заключается в выполнении функции обратного вызова после закрытия модального окна. Мы очень хорошо используем компонент класса для достижения этой функции, потому что setState может передавать два параметра, один из которых является обратным вызовом для обновления состояния, а другой — обратным вызовом после обновления состояния, нам нужно только поместить afterClose в обновленный обратный вызов, то есть во втором параметре обратного вызова. Но наш модальный компонент в настоящее время написан с реактивными хуками и функциональными компонентами, тогдаКак реализовать обратный вызов после обновления статусаШерстяная ткань? Здесь автор предлагает идею реализации, используя замыкания для достижения, основной код выглядит следующим образом:

// 函数组件外部
let hiddenCount = 0;
//  函数组件内部
useEffect(() => {
    if(isHidden && hiddenCount) {
      hiddenCount = 0
      afterClose && afterClose()
    }
    hiddenCount = 1
  }, [isHidden])

Мы знаем, что useEffect может не только реализовывать хуки для мониторинга смонтированных компонентов, но и отслеживать обновления состояния. Мы используем это для реализации этой функции. Стоит отметить, что нам нужно сбросить hiddenCount перед выполнением afterClose, чтобы избежать другого использования модальных компонентов. функции.

2.9 Поддержка надежности, мы используем инструмент propTypes, предоставленный react:

import PropTypes from 'prop-types'
// ...
Modal.propTypes = {
  afterClose: PropTypes.func,
  bodyStyle: PropTypes.object,
  cancelText: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.element
  ]),
  centered: PropTypes.bool,
  closable: PropTypes.bool,
  closeIcon: PropTypes.element,
  destroyOnClose: PropTypes.bool,
  footer: PropTypes.oneOfType([
    PropTypes.element,
    PropTypes.object
  ]),
  keyboard: PropTypes.bool,
  mask: PropTypes.bool,
  maskclosable: PropTypes.bool,
  maskStyle: PropTypes.object,
  okText: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.element
  ]),
  title: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.element
  ]),
  visible: PropTypes.bool,
  width: PropTypes.string,
  onCancel: PropTypes.func,
  onOk: PropTypes.func
}

оprop-typesНа официальном сайте есть очень подробные кейсы.

.xModalWrap {
    position: fixed;
    z-index: 999;
    top: 0;
    left: 0;
    width: 100%;
    bottom: 0;
    overflow: hidden;
    .xModalContent {
        position: relative;
        z-index: 1000;
        margin-left: auto;
        margin-right: auto;
        position: relative;
        top: 100px;
        background-color: #fff;
        background-clip: padding-box;
        border-radius: 4px;
        -webkit-box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
        pointer-events: auto;
        animation: xSpread .3s;
        &.xCentered {
            top: 50%;
            transform: translateY(-50%);
        }
        .xModalHeader {
            padding: 16px 24px;
            color: rgba(0, 0, 0, 0.65);
            background: #fff;
            border-bottom: 1px solid #e8e8e8;
            border-radius: 4px 4px 0 0;
            .xModalTitle {
                margin: 0;
                color: rgba(0, 0, 0, 0.85);
                font-weight: 500;
                font-size: 16px;
                line-height: 22px;
                word-wrap: break-word;
            }
        }
        .xModalCloseBtn {
            position: absolute;
            top: 0;
            right: 0;
            z-index: 10;
            padding: 0;
            width: 56px;
            height: 56px;
            color: rgba(0, 0, 0, 0.45);
            font-size: 16px;
            line-height: 56px;
            text-align: center;
            text-decoration: none;
            background: transparent;
            border: 0;
            outline: 0;
            cursor: pointer;
        }
        .xModalBody {
            padding: 16px 24px;
        }
        .xModalFooter {
            padding: 10px 16px;
            text-align: right;
            background: transparent;
            border-top: 1px solid #e8e8e8;
            border-radius: 0 0 4px 4px;
            .xFooterBtn {
                .xFooterBtnCancel, .xFooterBtnOk {
                    margin-left: 6px;
                    margin-right: 6px;
                }
            }
        }
    }
    .xModalMask {
        position: fixed;
        z-index: 999;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        overflow: hidden;
        background-color: rgba(0,0,0, .5);
    }
}

@keyframes xSpread {
    0% {
        opacity: 0;
        // 之所以要再加translateY(-50%),是为了防止动画抖动
        transform: translateY(-50%) scale(0);
    }
    100% {
        opacity: 1;
        transform: translateY(-50%) scale(1);
    }
}

С помощью описанных выше шагов надежный модальный компонент завершен.Модальный компонент является умеренно сложным компонентом в библиотеке компонентов.Если вы не понимаете, вы можете задать вопросы в области комментариев, и автор ответит на них, как только ты видишь это.

2.5 Использование модальных компонентов

Мы можем использовать его следующим образом:

<Modal title="xui基础弹窗" centered mask={false} visible={false}>
    <p>我是弹窗内容</p>
    <p>我是弹窗内容</p>
    <p>我是弹窗内容</p>
    <p>我是弹窗内容</p>
</Modal>

Автор опубликовал реализованные компоненты в npm.Если вам интересно, вы можете установить и использовать их напрямую с npm следующим образом:

npm i @alex_xu/xui

// 导入xui
import { 
  Button,
  Skeleton,
  Empty,
  Progress,
  Tag,
  Switch,
  Drawer,
  Badge,
  Alert
} from '@alex_xu/xui'

Библиотека компонентов поддерживает импорт по требованию, нам нужно только настроить в проекте babel-plugin-import, конкретная конфигурация выглядит следующим образом:

// .babelrc
"plugins": [
  ["import", { "libraryName": "@alex_xu/xui", "style": true }]
]

Скриншот библиотеки npm выглядит следующим образом:

наконец

В дальнейшем автор продолжит реализовывать

  • badge(логотип),
  • table(лист),
  • tooltip(панель подсказок),
  • Skeleton(скелетный экран),
  • Message(глобальная подсказка),
  • form(форма формы),
  • switch(выключатель),
  • дата/календарь,
  • Компонент для чтения QR-кода

и другие компоненты, чтобы рассмотреть многолетний путь автора по компонентизации.

Если вы не знакомы с принципами проектирования компонентов react/vue, вы можете обратиться к моей предыдущей серии статей о дизайне компонентов:

Автор выпустил библиотеку компонентов для npm, и вы можете испытать компоненты через установку npm.

Если вы хотите получить полный исходный код серии статей о дизайне компонентов или хотите узнать большеигра Н5, webpack,node,gulp,css3,javascript,nodeJS,визуализация данных холстаВ ожидании передовых знаний и реальных сражений, добро пожаловать в нашу техническую группу в общедоступном аккаунте «Интересный передний конец», чтобы вместе учиться и обсуждать, а также вместе исследовать границы переднего плана.

больше рекомендаций