Notification
全局通知提示框,和Message相比,Notification主要用于展示较为完整的通知内容,且允许带上交互。这里引用Antd官网的使用说明:
- 较为复杂的通知内容。
- 带有交互的通知,给出用户下一步的行动点。
- 系统主动推送。
Rc-Notification
Antd的Notification和Message组件的底层都是rc-notification, 它的执行流程如下:
核心在于维护一个task queue,里面存储着所有要渲染的通知框配置,组件会根据不同配置的placement,将配置分别派发至对应的渲染队列渲染通知框。
Notifications
当我们传入配置时,会在Notifications中将其写入configList state,而后根据它的Placement将其派发至不同的渲染队列,并调用createPortal将组件挂载到指定的container。
向外暴露了open, close, destroy方法,分别用于打开,关闭一个task,以及销毁整个队列的task。
typescript
// ========================= Refs =========================
React.useImperativeHandle(ref, () => ({
// Create
open: (config) => {
setConfigList((list) => {
let clone = [...list];
// Replace if exist
const index = clone.findIndex((item) => item.key === config.key);
const innerConfig: InnerOpenConfig = { ...config };
if (index >= 0) {
innerConfig.times = ((list[index] as InnerOpenConfig)?.times || 0) + 1;
clone[index] = innerConfig;
} else {
innerConfig.times = 0;
clone.push(innerConfig);
}
if (maxCount > 0 && clone.length > maxCount) {
clone = clone.slice(-maxCount);
}
return clone;
});
},
// Close a task
close: (key) => {
onNoticeClose(key);
},
// Destroy all tasks
destroy: () => {
setConfigList([]);
},
}));
NoticeList
渲染分发好的Task, 根据placement生成对应的css,根据传入的stack,计算出通知框的位移量,调用CSSMotionList为组件添加对应的移入移出动画。
Calculate the stack
typescript
// If dataIndex is -1, that means this notice has been removed in data, but still in dom
// Should minus (motionIndex - 1) to get the correct index because keys.length is not the same as dom length
const stackStyle: CSSProperties = {};
if (stack) {
const index = keys.length - 1 - (dataIndex > -1 ? dataIndex : motionIndex - 1);
const transformX = placement === "top" || placement === "bottom" ? "-50%" : "0";
if (index > 0) {
stackStyle.height = expanded
? dictRef.current[strKey]?.offsetHeight
: latestNotice?.offsetHeight;
// Transform
let verticalOffset = 0;
for (let i = 0; i < index; i++) {
verticalOffset += dictRef.current[keys[keys.length - 1 - i].key]?.offsetHeight + gap;
}
const transformY = (expanded ? verticalOffset : index * offset) *
(placement.startsWith("top") ? 1 : -1);
const scaleX = !expanded && latestNotice?.offsetWidth && dictRef.current[strKey]?.offsetWidth
? (latestNotice?.offsetWidth - offset * 2 * (index < 3 ? index : 3)) /
dictRef.current[strKey]?.offsetWidth
: 1;
stackStyle.transform = `translate3d(${transformX}, ${transformY}px, 0) scaleX(${scaleX})`;
} else {
stackStyle.transform = `translate3d(${transformX}, 0, 0)`;
}
}
Notice
渲染我们实际看到的通知框内容,以及实现duration倒计时,Pause On Hover等功能
倒计时
当duration大于0,且鼠标不在通知框上时,会开启倒计时。这里维护一个用于记录已经过时间的状态值: spentTime,当鼠标移到通知框上时,会停止倒计时,并记录下已经经过的时间值,下次倒计时的时间将会是duration - spentTime
后的时间。
typescript
const [spentTime, setSpentTime] = React.useState(0);
// ======================== Effect ========================
React.useEffect(() => {
if (!mergedHovering && duration > 0) {
// 记录开始时间(减去已经经过的时间的差值)
const start = Date.now() - spentTime;
const timeout = setTimeout(
() => {
onInternalClose();
},
duration * 1000 - spentTime,
);
return () => {
if (pauseOnHover) {
clearTimeout(timeout);
}
// 记录结束时当前时间和开始时间的差值
setSpentTime(Date.now() - start);
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [duration, mergedHovering, times]);
useNotification
调用Notifications,使用ref获取该组件暴露出来的方法:
typescript
const contextHolder = (
<Notifications
container={container}
ref={notificationsRef}
prefixCls={prefixCls}
motion={motion}
maxCount={maxCount}
className={className}
style={style}
onAllRemoved={onAllRemoved}
stack={stack}
renderNotifications={renderNotifications}
/>
);
维护一组api,传入配置时,会打上对应的type:
typescript
// ========================= Refs =========================
const api = React.useMemo<NotificationAPI>(() => {
return {
open: (config) => {
const mergedConfig = mergeConfig(shareConfig, config);
if (mergedConfig.key === null || mergedConfig.key === undefined) {
mergedConfig.key = `rc-notification-${uniqueKey}`;
uniqueKey += 1;
}
setTaskQueue((queue) => [
...queue,
{ type: "open", config: mergedConfig },
]);
},
close: (key) => {
setTaskQueue((queue) => [...queue, { type: "close", key }]);
},
destroy: () => {
setTaskQueue((queue) => [...queue, { type: "destroy" }]);
},
};
}, []);
随后遍历这个队列,根据不同的type,调用Notifications中暴露出的方法:
typescript
// ======================== Effect ========================
React.useEffect(() => {
// Flush task when node ready
if (notificationsRef.current && taskQueue.length) {
taskQueue.forEach((task) => {
switch (task.type) {
case "open":
notificationsRef.current.open(task.config);
break;
case "close":
notificationsRef.current.close(task.key);
break;
case "destroy":
notificationsRef.current.destroy();
break;
}
});
// React 17 will mix order of effect & setState in async
// - open: setState[0]
// - effect[0]
// - open: setState[1]
// - effect setState([]) * here will clean up [0, 1] in React 17
setTaskQueue((oriQueue) =>
oriQueue.filter((task) => !taskQueue.includes(task))
);
}
}, [taskQueue]);
最后返回api和contextHolder:
typescript
// ======================== Return ========================
return [api, contextHolder];