这些天在重构自己的个人主页,想着完全实现一套自己的组件库,刚好碰到了 Message
组件,分享一下我的实现
组件 API 及需求分析
目前市面上绝大多数组件库如 Antd
、Arco
等所使用的 Message
组件的调用方式基本上都是形似
1
|
message.success("text", 1000);
|
这样直接的函数调用,而不用像 Modal
组件那样需要在 JSX 中声明节点,十分方便,因此我们的实现也需要使用这样的 API 调用。
其次,Message
组件的显示形式是在页面顶部排列,新的消息框会在旧的消息框下方显示,旧的消息框会在指定时间(delay)后销毁
实现思路
我们调用 Message
组件时并不需要声明节点,因此我们需要实现动态挂载节点。
这个需求可以使用 ReactDom
为我们提供的 createRoot
API 实现,方式如下:
1
2
3
4
5
6
|
const root = document.getElementById("root")!;
const el = document.createElement("div");
root.appendChild(el);
const container = creatRoot(el);
wrapper.render(<MessageContainer />);
|
在这个过程中,我们在 root
节点下新创建了一个 el
节点,并将 <MessageContainer />
组件挂载到 el
节点下,我们将这个组件作为 Message
的容器,在其内部维护一个 Message List
,使用 map
渲染所有组件,就可以基本上实现我们的需求
1
2
3
4
5
6
7
8
9
10
11
|
const MessageContainer: FC = (): ReactElement => {
const [notices, setNotices] = useState<IMessage[]>([]);
/* ... */
return (
<div className="message-container">
{notices.map(({ text, key, type }) => (
<Notice type={type} text={text} />
))}
</div>
);
};
|
然后我们需要实现在函数组件外部调用其内部的添加方法,在外部声明一个 add
函数,在函数内对其赋值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
let add: (message: IMessage) => void;
const MessageContainer: FC = (): ReactElement => {
const [notices, setNotices] = useState<IMessage[]>([]);
/* ... */
add = (message: IMessage) => {
setNotices(prevNotices => [...prevNotices, message]);
setTimeout(() => {
remove(message);
}, timeout);
};
/* ... */
return (
<div className="message-container">
{notices.map(({ text, key, type }) => (
<Notice type={type} text={text} />
))}
</div>
);
};
|
这样就可以在函数外部调用 add
方法,改变其内部的状态了,当然,我们还需要能够在指定时间后移除单个消息,我们需要为每一个消息分配一个 key
值,以区分不同的消息,remove
函数如下:
1
2
3
4
|
const remove = (message: IMessage) => {
const { key } = message;
setNotices(prevNotices => prevNotices.filter(item => key !== item.key));
};
|
那么添加消息时,我们的调用接口就是如下所示:
1
2
|
let key = 0;
add({ text, key: key++, type: "success" });
|
如果我们还需要实现超过指定长度后自动删除前面的消息,则就可以在 MessageContainer
组件中添加一个 useEffect
来处理,实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
const MessageContainer: FC = (): ReactElement => {
const [notices, setNotices] = useState<IMessage[]>([]);
/* ... */
useEffect(() => {
if (notices.length > maxCount) {
const [firstNotice] = notices;
remove(firstNotice);
}
}, [notices]);
/* ... */
return (
<div className="message-container">
{notices.map(({ text, key, type }) => (
<Notice type={type} text={text} />
))}
</div>
);
};
|
最后我们使用一个类(class)来封装整个组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// index.tsx
export class Message {
private static el = document.getElementById("message-wrapper");
private static timeout = 3 * 1000;
private static maxCount = 10;
private static key = 0;
private static add: (message: IMessage) => void;
private static MessageContainer() {
/* ... */
}
public static success(text: string) {
Message.add({ text, key: Message.key++, type: "success" });
}
public static error(text: string) {
Message.add({ text, key: Message.key++, type: "error" });
}
public static info(text: string) {
Message.add({ text, key: Message.key++, type: "info" });
}
}
|
这样一来,我们就可以在外部使用 Message.xxxx("some text")
来调用组件了
但是这样还不够,在第一次调用这个组件时,add
方法是没有值的,因此我们需要为其处理首次调用时的情况:
1
2
3
4
5
6
7
8
9
10
11
|
private static add = (message: IMessage): void => {
if (!Message.el) {
Message.el = document.createElement("div")
Message.el.className = "message-wrapper"
Message.el.id = "message-wrapper"
root.appendChild(Message.el)
const _root = createRoot(Message.el)
_root.render(<Message.MessageContainer />)
setTimeout(() => Message.add(message))
}
}
|
不存在节点时,先创建节点,等待容器组件渲染完成,修改完 add
方法后再调用自身,这里需要使用 setTimeout
将这一步放到宏任务队列中,使得其在渲染完成后再执行。当然你也可以用 callback
实现
相关资料:
事件循环:微任务和宏任务 > React 18 用 createRoot 替换 render - 什么是渲染回调?
这样一来就可以实现第一次调用时自动注册节点并重新执行了
关于动画的实现
我们希望节点在添加和删除时显示动画,在 Vue 中我们可以直接使用 Transition
组件,在 React 中则没有原生实现,需要使用第三方库,其中 React Transition Group
就满足了其所有基本需求,原理大致是延迟 DOM 的删除时间,并在指定的时间节点动态添加 CSS 样式,我会在接下来的文章中实现一个基础版本的 Transition
组件
关于组件的使用我不多赘述,大体实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
return (
<div className="message-container">
<TransitionGroup>
{notices.map(({ text, key, type }) => (
<CSSTransition
classNames="message"
timeout={150}
unmountOnExit={true}
appear={true}
key={key}
>
<Notice type={type} text={text} />
</CSSTransition>
))}
</TransitionGroup>
</div>
);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
/* styles.css */
/* 出现前 */
.message-enter,
.message-appear {
opacity: 0;
transform: translateY(-50%);
}
/* 出现时 */
.message-enter-active,
.message-appear-active {
opacity: 100%;
transform: translateY(0);
transition: all 150ms cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
/* 出现后、退出前 */
.message-enter-done,
.message-appear-done,
.message-exit {
opacity: 100%;
transform: translateY(0);
}
/* 退出时 */
.message-exit-active {
opacity: 0;
transform: translateY(-50%);
transition: all 150ms cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
|
这样就可以实现丝滑的弹出动画啦