Concis组件库封装——Message 全局提示

1,256 阅读3分钟

您好,如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~

前言

React-View-UI已经更新了25个组件了,今天来写一下Message组件,这个组件目前还是很常用的,组件效果如下: 在这里插入图片描述

组件库文档如下:

在这里插入图片描述 在这里插入图片描述 具体的交互可以在react-view-ui.com:92/#/common/me…进行体验。

API能力

一共提供了如下的API: 在这里插入图片描述 组件主要调用方式如下:

直接调用:

Message.info('This is an info message!')

传入对象调用(扩展API都需要传入对象):

Message.info({
      content: 'This is an info message!',
      duration: 3000,
      position: 'bottom'
      clearable: true,
      style: {
      	fontSize: '12px'
	  }
});

组件类型接口

import { CSSProperties } from 'react';

interface MessageProps<T> {
  /**
   * @description 对象类型传参时的内容
   */
  content?: T;
  /**
   * @description Message类型
   */
  type?: 'info' | 'success' | 'warning' | 'error' | 'normal' | 'loading';
  /**
   * @description 显示时间
   * @default 3000ms
   */
  duration?: number;
  /**
   * @description 显示位置
   * @default top
   */
  position?: 'top' | 'bottom';
  /**
   * @description 出现可清除按钮
   * @default false
   */
  clearable?: boolean;
  /**
   * @description 自定义样式
   * @default {}
   */
  style?: CSSProperties;
}

export type { MessageProps };

组件源码

使用者所调用的Message.xxx函数其实就是在调用组件内部所定义的addInstance函数,往页面中不断加入div(Message组件),并在一定时延后自动关闭,这是组件的整体设计思路。 通过add和remove两个方法对消息窗进行添加和删除,并在删除后进行整体布局(高度)重排,即可实现自动合并的效果。

import React, { useState, useEffect, useMemo, useRef, CSSProperties } from 'react';
import ReactDOM from 'react-dom';
import { MessageProps } from './interface';
import './index.module.less';
import {
  ExclamationCircleFilled,
  CheckCircleFilled,
  CloseCircleFilled,
  LoadingOutlined,
  CloseOutlined,
} from '@ant-design/icons';

let container: HTMLDivElement | null;
let topMessageNum: number = 0;
let bottomMessageNum: number = 0;

function addInstance(
  type: 'info' | 'success' | 'warning' | 'error' | 'normal' | 'loading',
  props: string | MessageProps<string>,
) {
  let style: CSSProperties = {},
    duration: number = 3000,
    content,
    position: 'top' | 'bottom' = 'top',
    clearable = false;
  if (typeof props === 'object') {
    style = props.style || {};
    duration = props.duration || 3000;
    content = props.content;
    position = props.position ? props.position : 'top';
    clearable = props.clearable ? props.clearable : false;
  } else if (typeof props === 'string') {
    content = props;
  }
  const div = document.createElement('div');
  const messageBoxId = String(Math.floor(Math.random() * 1000));
  div.setAttribute('class', `${position}-${messageBoxId}`);
  if (container) {
    container.appendChild(div);
  } else {
    container = document.createElement('div');
    container.setAttribute('class', 'all-container');
    document.body.appendChild(container);
    container.appendChild(div);
  }
  setTimeout(() => {
    if (Array.prototype.slice.call(container?.childNodes).includes(div)) {
      changeHeight(Array.prototype.slice.call(container?.childNodes), position);
      container?.removeChild(div);
      if (position === 'top') {
        topMessageNum--;
      } else {
        bottomMessageNum--;
      }
    }
  }, duration + 200);
  ReactDOM.render(
    <Message
      style={style}
      content={content}
      type={type}
      duration={duration}
      position={position}
      clearable={clearable}
      messageBoxId={messageBoxId}
    />,
    div,
  );
}
function remove(id: string, position: string) {
  //重排节点下元素高度
  const container = document.querySelector('.all-container');
  const children = Array.prototype.slice.call(container?.childNodes);
  for (let key in children) {
    if (children[key].getAttribute('class') === `${position}-${id}`) {
      const removeDom = children[key];
      container?.removeChild(removeDom);
      if (position === 'top') {
        topMessageNum--;
      } else {
        bottomMessageNum--;
      }
      changeHeight(children.slice(Number(key)), position);
    }
  }
}
function changeHeight(children: Array<HTMLElement>, position: any) {
  for (let key in children) {
    const child = children[key].childNodes[0] as HTMLElement;
    if (children[key].getAttribute('class')?.startsWith(position)) {
      child.style[position] = Number(child.style[position].split('p')[0]) - 70 + 'px';
    }
  }
}
const Message = (props: MessageProps<string>) => {
  const { style, content, type, duration, position, clearable, messageBoxId } = props;
  const [opac, setOpac] = useState(1);
  const messageDom = useRef<any>(null);

  useEffect(() => {
    if (position === 'top') {
      topMessageNum++;
    } else {
      bottomMessageNum++;
    }
    setTimeout(() => {
      (messageDom.current as HTMLElement).style.transition = '0.2s linear';
      (messageDom.current as HTMLElement).style.animation = 'none';
    }, 500);
    setTimeout(() => {
      setOpac(0);
    }, duration);
  }, []);
  useEffect(() => {
    const transform = position || 'top';
    (messageDom?.current as HTMLElement).style[transform] =
      (transform === 'top' ? topMessageNum : bottomMessageNum) * 70 + 'px';
  }, [topMessageNum, bottomMessageNum]);

  const messageIcon = useMemo(() => {
    if (type === 'info') {
      return <ExclamationCircleFilled style={{ color: '#1890ff', fontSize: '16px' }} />;
    } else if (type === 'error') {
      return <CloseCircleFilled style={{ color: '#f53f3f', fontSize: '16px' }} />;
    } else if (type === 'normal') {
      return <></>;
    } else if (type === 'success') {
      return <CheckCircleFilled style={{ color: '#19b42a', fontSize: '16px' }} />;
    } else if (type === 'warning') {
      return <ExclamationCircleFilled style={{ color: '#fa7d00', fontSize: '16px' }} />;
    } else if (type === 'loading') {
      return <LoadingOutlined style={{ color: '#1890ff', fontSize: '16px' }} />;
    }
  }, [type]);
  const closeMessage = () => {
    remove(messageBoxId as string, position as string);
  };

  return (
    <div className="message-container" style={{ opacity: opac, ...style }} ref={messageDom}>
      {messageIcon}
      <span className="toast-content">{content}</span>
      {clearable && <CloseOutlined onClick={closeMessage} />}
    </div>
  );
};

Message.info = (props: string | MessageProps<string>) => {
  return addInstance('info', props);
};
Message.success = (props: string | MessageProps<string>) => {
  return addInstance('success', props);
};
Message.error = (props: string | MessageProps<string>) => {
  return addInstance('error', props);
};
Message.normal = (props: string | MessageProps<string>) => {
  return addInstance('normal', props);
};
Message.warning = (props: string | MessageProps<string>) => {
  return addInstance('warning', props);
};
Message.loading = (props: string | MessageProps<string>) => {
  return addInstance('loading', props);
};

export default Message;


组件测试

Message组件的单元测试代码如下:

import React from 'react';
import Message from '../../Message/index';
import Enzyme from '../setup';
import mountTest from '../mountTest';

const { mount } = Enzyme;

describe('Message', () => {
  beforeEach(() => {
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.runAllTimers();
  });

  it('test base Message show correctly', () => {
    Message.info('this is a test');
    expect(document.querySelectorAll('.all-container')).toHaveLength(1);
    expect(document.querySelectorAll('.message-container .toast-content')[0].innerHTML).toBe(
      'this is a test',
    );
  });

  it('test click five nums Message show num correctly', () => {
    for (let i = 0; i < 5; i++) {
      Message.info('content');
    }
    expect(document.querySelectorAll('.all-container')[0].childNodes.length).toBe(5);
  });

  it('test bottom transform Message show correctly', () => {
    Message.info({
      content: 'this is a test',
      duration: 3000,
      position: 'bottom',
    });
    expect(
      document.querySelectorAll('.message-container')[0].getAttribute('style')?.includes('bottom:'),
    );
  });

  it('test clearable Message correctly', () => {
    Message.info({
      content: 'this is a test',
      duration: 3000,
      clearable: true,
    });
    expect(document.querySelectorAll('.message-container')[0].childNodes.length).toBe(3);
  });
});

组件库地址

开源不易,欢迎学习和体验,喜欢请多多支持,有问题请留言。