Как реализовать набор элегантных компонентов Toast в React

React.js

предисловие

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

ключевой момент

Достаточно прост в использовании

Поскольку он используется очень часто и требует, чтобы он был доступен везде, мы хотим использовать только одну строку кода:

Toast.info('this is a toast', 1000);

Нет необходимости вручную вставлять контейнеры компонентов

Когда мы используем другие компоненты, такие как antd, большинство компонентов необходимо внедрить в бизнес-дом, например:

render() {
    return (
        <div>other components...</div>
        <Dropdown overlay={menu}>
            <a className="ant-dropdown-link" href="#">
              Hover me <Icon type="down" />
            </a>
        </Dropdown>,
    )
}

Однако, поскольку компонент Toast не обязательно должен находиться на странице, его можно выбросить, когда он используется, а место использования постоянно меняется. со временем это сильно повлияет на эффективность и удобство сопровождения бизнес-кода.

Несколько тостов не влияют друг на друга

В бизнесе часто бывают сценарии, когда несколько запросов выдаются одновременно. Например, если два интерфейса не могут выполнить запрос одновременно, коллегам необходимо указать причину ошибки, поэтому Toast не требует конфликта.

мини версия тост

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

выполнить

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

class App extends React.Component {

    state = {
        isToastShow: false, // 是否展示 Toast
        toastText: '', // Toast 文字内容
    }

    // 设置 Toast 属性
    handleToastShow = (toastText, showTime) => {
        this.setState({
            isToastShow: true,
            toastText
        });
        // 定时销毁 Toast Dom
        setTimeout(() => {
            this.setState({
                isToastShow: false
            })
        }, showTime)
    }

    // 显示 Toast
    handleShowToast = () => {
        this.handleToastShow('this is a toast', 3000)
    }

    render() {
        const { isToastShow, toastText } = this.state;
        return (
            <div>
                <button onClick={this.handleShowToast}>show toast</button>
                {isToastShow && <div className="toast-wrap">
                    <div className="toast-mask" />
                    <div className="toast-text">{toastText}</div>
                </div>}
            </div>
        )
    }
}

вопрос

Здесь мы обнаружили несколько проблем:

  1. Простой Toast на самом деле должен определять два состояния, что увеличивает менталитет поддержки бизнес-логики и снижает удобство сопровождения.
  2. Логику Toast и Dom, и даже стили необходимо внедрить в бизнес-код, чтобы уменьшить читабельность бизнес-кода.
  3. Несколько тостов не могут отображаться одновременно

В ответ на эти проблемы мы будем постепенно реализовывать простой и удобный Toast.

Реализация полной версии

Адрес исходного кода проекта:Vincedream/easy-toast

метод вызова

Прежде чем объяснять реализацию компонента, давайте кратко прочитаем метод вызова после реализации:

import React from 'react';
import Toast from './Toast';

function App () {
  const handleClick1 = () => {
    Toast.info('test111', 2000);
  }

  const handleClick2 = () => {
    Toast.info('test222', 1000, true);
  }

  const handleClick3 = () => {
    Toast.info('test333', 1000, true);
    Toast.info('test long duration', 4000, true);
  }

  const handleHideAllToast = () => {
    Toast.hide();
  }

  return(
    <div>
      <button onClick={handleClick1}>no mask Toast</button><br/>
      <button onClick={handleClick2}>with mask Toast</button><br/>
      <button onClick={handleClick3}>long duration</button><br/>
      <button onClick={handleHideAllToast}>hideAllToast</button>
    </div>
  )
}

export default App;

Эффект:

image

Здесь мы звонимToast.info()После этого компонент динамически внедряется в Dom, и никакая логика Toast не внедряется в Dom или Style в бизнес-контейнере.

Динамическое внедрение ключевых методов Dom

Как мы можем динамически внедрить Dom, не вторгаясь в контейнер?Это похоже на манипулирование Dom вручную десять лет назад в эпоху jQuery? Конечно нет. Вот ключевой метод:ReactDom.render(<组件/>, 真实 Dom), давайте рассмотрим пример:

class App extends React.Component {

    handleAddDom = () => {
        // 在真实 dom 上创建一个真的的 div 宿主节点,并将其加入到页面根节点 body 当中
        const containerDiv = document.createElement('div');
        document.body.appendChild(containerDiv);
        // 这里返回的是对该组件的引用
        const TestCompInstance = ReactDom.render(<TestComp />, containerDiv);
        console.log(TestCompInstance);
        // 这里可以调用任何 TestCompInstance 上的方法,并且能够访问到其 this
        TestCompInstance.sayName();
    }

    render() {
        return (
            <div>
                <button onClick={this.handleAddDom}>add Dom</button>
            </div>
        )
    }
}

Результаты:

image

Из приведенного выше примера видно, что мы можем напрямую создавать и внедрять компонент React в реальный дом в логическом коде js, а также можем манипулировать компонентом произвольно.Поняв это, мы получаем суть написания компонентов Toast.

Реализация

Во-первых, мы создаем компонент-контейнер Toast:

// ToastContainer.js
class ToastContainer extends Component {
    state = {
        isShowMask: false, // 当前 mask 是否显示
        toastList: [] // 当前 Toast item 列表
    }
    
    // 将新的 toast push 到 toastContainer 中
    pushToast = (toastProps) => {
        const { type, text, duration, isShowMask = false } = toastProps;
        const { toastList } = this.state;
        toastList.push({
            id: getUuid(),
            type,
            text,
            duration,
            isShowMask
        });
        this.setState({
            toastList,
            isShowMask
        });
    }


    render() {
        const { toastList, isShowMask } = this.state;
        return (
            <div className="toast-container">
                {isShowMask && <div className="mask"/>}
                <div className="toast-wrap">
                    {toastList.reverse().map((item) => (
                        <Toast {...item} key={item.id} />
                    ))}
                </div>
            </div>
        );
    }
}

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

Затем мы создаем компонент TOAST ITEM, который действительно отображается:

// ToastItem.js
class ToastItem extends Component {
    render() {
        const { text } = this.props;
        return (
            <div className="toast-item">
                {text}
            </div>
        );
    }
}

Два ключевых компонента созданы, нам нужно «динамически внедрить» их в дом, используя описанное выше.ReactDom.render()метод, для этого мы создаем файл унифицированной записи Toast:

// index.js
import React from 'react';
import ReactDom from 'react-dom';

import ToastContainer from './ToastContainer';

// 在真实 dom 中创建一个 div 节点,并且注入到 body 根结点中,该节点用来存放下面的 React 组件
const toastContainerDiv = document.createElement('div');
document.body.appendChild(toastContainerDiv);

// 这里返回的是 ToastContainer 组件引用
const getToastContainerRef = () => {
    // 将 <ToastContainer /> React 组件,渲染到 toastContainerDiv 中,并且返回了 <ToastContainer /> 的引用
    return ReactDom.render(<ToastContainer />, toastContainerDiv);
}

// 这里是 <ToastContainer /> 的引用
let toastContainer = getToastContainerRef();


export default {
    info: (text, duration, isShowMask) => (toastContainer.pushToast({type: 'info', text, duration, isShowMask})),
};

Здесь мы следуем вышеизложенномуReactDom.render()метод, будет<ToastContainer />Рендерится в дом и получает ссылку, нам просто нужно вызвать его здесь<ToastContainer />серединаpushToastспособ отображения всплывающей подсказки.

На данный момент мы завершили упрощенную версиюВерсия компонента Toast с динамическим внедрением, в следующем разделе мы добавим к нему следующие две функции:

  1. Тост скрыть по времени
  2. Принудительно скрыть тост

Улучшить функцию

Тост скрыть по времени

Сначала мы трансформируем компонент-контейнер ToastContainer, добавляем метод скрытой маски и передаем его в<ToastItem />середина:

class ToastContainer extends Component {
    ...

    // 将被销毁的 toast 剔除
    popToast = (id, isShowMask) => {
        const { toastList } = this.state;
        const newList = toastList.filter(item => item.id !== id);
        this.setState({
            toastList: newList,
        });
        // 该 toast item 是否为 toastList 中 duration 最长的 item
        let isTheMaxDuration = true;
        // 该 toast item 的 duration
        const targetDuration = toastList.find(item => item.id === id).duration;
        // 遍历 toastList 检查是否为最长 duration
        toastList.forEach(item => {
            if (item.isShowMask && item.duration > targetDuration) {
                isTheMaxDuration = false
            }
            return null;
        });

        // 隐藏 mask
        if (isShowMask && isTheMaxDuration) {
            this.setState({
                isShowMask: false
            })
        }
    }

    render() {
        ...
        <ToastItem onClose={this.popToast} {...item} key={item.id} />
        ...     
    }
}

Далее мы преобразовываем<ToastItem />, посколькуcomponentDidMountУстановите таймер в соответствии с параметром входящей длительности, установите таймер, чтобы скрыть всплывающее уведомление, и очистите таймер до того, как компонент будет уничтожен.

// ToastItem.js
class ToastItem extends Component {
    componentDidMount() {
        const { id, duration, onClose, isShowMask } = this.props;
        this.timer = setTimeout(() => {
            if (onClose) {
                onClose(id, isShowMask);
            }
        }, duration)
    }
    // 卸载组件后,清除定时器
    componentWillUnmount() {
        clearTimeout(this.timer)
    }
    render() {
       ...
    }
}

Здесь мы завершили функцию скрытия Toast, подробности которой объясняются в коде и здесь повторяться не будут.

Принудительно скрыть тост

Как принудительно скрыть уже появившийся Toast? Здесь мы все еще используемReactDomAPI:ReactDom.unmountComponentAtNode(container), функция этого метода — выгрузить компонент из Дом, а его обработчики событий (обработчики событий) и состояние будут очищены вместе:

// index.js
...
// 这里返回的是 ToastContainer 组件引用
const getToastContainerRef = () => {
    // 将 <ToastContainer /> React 组件,渲染到 toastContainerDiv 中,并且返回了 <ToastContainer /> 的引用
    return ReactDom.render(<ToastContainer />, toastContainerDiv);
}
// 这里是 <ToastContainer /> 的引用
let toastContainer = getToastContainerRef();
const destroy = () => {
    // 将 <ToastContainer /> 组件 unMount,卸载组件
    ReactDom.unmountComponentAtNode(toastContainerDiv);
    // 再次创建新的 <ToastContainer /> 引用,以便再次触发 Toast
    toastContainer = getToastContainerRef();
}


export default {
    ...
    hide: destroy
};

Обратите внимание, что удаление<ToastContainer />После этого нужно создать новый, пустой<ToastContainer />компонент, чтобы Toast можно было снова вызвать позже.

Суммировать

В этой статье мы использовали новый метод для создания специального компонента React и практиковали некоторые методы ReactDom, которые вы, возможно, не использовали.Помимо компонента Toast, мы также можем использовать ту же идею для написания других компонентов, таких как Modal , Уведомление и другие компоненты.

Ссылаться на:

ReactDOM

Адрес исходного кода проекта:Vincedream/easy-toast