在我们的项目中,以及大多数组件库中,大家使用Message组件,会选择使用指令式,因为可以在非JSX | TSX文件中使用,并且也可以在函数中直接使用,省去了很多状态控制。
大概说下思路。首先我们需要维护一个队列,这个队列里面存的是每一条message,但是需要一个id,因为这个id我们需要在remove的时候进行精确删除。在我们add message或者remove message手动去render创建的message容器。
1. add / remove message function
此处我引入了第三方依赖包UUID,每次新创建的时候就能保证id的唯一性了。每次add或者remove的时候我们都会手动去渲染message__container容器。
当我们渲染message__container容器时,如果没有,我们就会创建一个容器,然后使用React的createRoot方法,让react来接管这个dom结构,后续如果在进行Add / Remove 操作,则会重新渲染BaseMessage组件,因为作为key的id是唯一的,所以也会避免重复渲染问题。
const MESSAGE_QUEUE: Array<MessageQueueItem> = [];
let containerRoot: any;
function addMessage(params: MessageItem) {
const id = uuidv4();
MESSAGE_QUEUE.push({ ...params, id });
renderMessage([...MESSAGE_QUEUE]);
}
function removeMessage(id: string) {
const position = MESSAGE_QUEUE.findIndex((item) => item.id === id);
MESSAGE_QUEUE.splice(position, 1);
renderMessage([...MESSAGE_QUEUE]);
}
function createContainer() {
const container = document.createElement('div');
container.classList.add('message__container');
document.body.appendChild(container);
return container;
}
function renderMessage(messageQueue: Array<any>) {
if (!containerRoot) {
const container = createContainer();
containerRoot = createRoot(container);
}
const MessageComponents = messageQueue.map((props) => {
return <BaseMessage {...props} key={props.id} />;
});
containerRoot.render(MessageComponents);
}
2. BaseMesage Component
通过ref获取BaseMessage实例,当我们第一次渲染这个组件时,添加visible属性,添加动画样式,然后再设置setTimeout进行自动关闭。
const BaseMessage = ({ message, type = 'info', id }: MessageQueueItem) => {
const refMessage = useRef<HTMLDivElement>(null);
useEffect(() => {
refMessage.current?.classList.add('visible');
setTimeout(() => {
handleHidden();
}, 3000);
}, []);
const clear = () => removeMessage(id);
const handleHidden = () => {
if (refMessage.current) {
refMessage.current.addEventListener('animationend', clear, {
once: true,
});
}
refMessage.current?.classList.add('hidden');
};
const messageClass = classNames('message', {
[`${type}`]: type,
});
const renderIcon = () => {
switch (type) {
case 'info':
return <Icon icon="info-circle" />;
case 'warning':
return <Icon icon="exclamation-circle" />;
case 'error':
return <Icon icon="times-circle" />;
case 'success':
return <Icon icon="check-circle" />;
default:
return null;
}
};
return (
<div className={messageClass} ref={refMessage}>
{renderIcon()}
<span>{message}</span>
</div>
);
};
完整代码
import classNames from 'classnames';
import { useEffect, useRef } from 'react';
import { createRoot } from 'react-dom/client';
import { v4 as uuidv4 } from 'uuid';
import Icon from '../Icon/icon';
type MessageInput = (message: string) => void;
interface MessageItem {
message: string;
type: 'success' | 'error' | 'warning' | 'info';
}
interface MessageQueueItem extends MessageItem {
id: string;
}
interface IMessage {
info: MessageInput;
warn: MessageInput;
error: MessageInput;
success: MessageInput;
}
const MESSAGE_QUEUE: Array<MessageQueueItem> = [];
let containerRoot: any;
function addMessage(params: MessageItem) {
const id = uuidv4();
MESSAGE_QUEUE.push({ ...params, id });
renderMessage([...MESSAGE_QUEUE]);
}
function removeMessage(id: string) {
const position = MESSAGE_QUEUE.findIndex((item) => item.id === id);
MESSAGE_QUEUE.splice(position, 1);
renderMessage([...MESSAGE_QUEUE]);
}
function createContainer() {
const container = document.createElement('div');
container.classList.add('message__container');
document.body.appendChild(container);
return container;
}
function renderMessage(messageQueue: Array<any>) {
if (!containerRoot) {
const container = createContainer();
containerRoot = createRoot(container);
}
const MessageComponents = messageQueue.map((props) => {
return <BaseMessage {...props} key={props.id} />;
});
containerRoot.render(MessageComponents);
}
const BaseMessage = ({ message, type = 'info', id }: MessageQueueItem) => {
const refMessage = useRef<HTMLDivElement>(null);
useEffect(() => {
refMessage.current?.classList.add('visible');
setTimeout(() => {
handleHidden();
}, 3000);
}, []);
const clear = () => removeMessage(id);
const handleHidden = () => {
if (refMessage.current) {
refMessage.current.addEventListener('animationend', clear, {
once: true,
});
}
refMessage.current?.classList.add('hidden');
};
const messageClass = classNames('message', {
[`${type}`]: type,
});
const renderIcon = () => {
switch (type) {
case 'info':
return <Icon icon="info-circle" />;
case 'warning':
return <Icon icon="exclamation-circle" />;
case 'error':
return <Icon icon="times-circle" />;
case 'success':
return <Icon icon="check-circle" />;
default:
return null;
}
};
return (
<div className={messageClass} ref={refMessage}>
{renderIcon()}
<span>{message}</span>
</div>
);
};
const Message: IMessage = {
info: (message: string) => addMessage({ type: 'info', message }),
warn: (message: string) => addMessage({ type: 'warning', message }),
error: (message: string) => addMessage({ type: 'error', message }),
success: (message: string) => addMessage({ type: 'success', message }),
};
export { Message };
样式文件(scss)
@mixin message-style($background, $border, $color) {
color: $color;
background-color: $background;
border: 2px solid $border;
}
$message-padding: 4px 20px !default;
$message-margin: 15px auto !default;
$message-radius: 4px !default;
$message-box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.15) !default;
.message__container {
position: fixed;
top: 0;
width: 100vw;
pointer-events: none;
}
.message {
position: relative;
display: flex;
align-items: center;
column-gap: 6px;
width: max-content;
margin: $message-margin;
padding: $message-padding;
border-radius: $message-radius;
box-shadow: $message-box-shadow;
opacity: 0;
&.info {
@include message-style($primary, $primary, $white);
}
&.warning {
@include message-style($warning, $warning, $white);
}
&.error {
@include message-style($danger, $danger, $white);
}
&.success {
@include message-style($success, $success, $white);
}
&.visible {
opacity: 0;
animation: visible 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0s 1 forwards;
}
&.hidden {
opacity: 1;
animation: hidden 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0s 1 forwards;
}
}
@keyframes visible {
from {
opacity: 0;
top: -30px;
}
to {
opacity: 1;
top: 0;
}
}
@keyframes hidden {
from {
opacity: 1;
top: 0;
}
to {
opacity: 0;
top: -30px;
}
}