1. 使用场景
全局提示通知提醒信息
- 较为复杂的通知内容
- 带有交互的通知,给出用户下一步的行动点
- 系统主动推送
2. Notification API
notification.success(config)
notification.error(config)
notification.info(config)
notification.warning(config)
notification.warn(config)
notification.open(config)
notification.close(key: String)
notification.destroy()
3. 源码分析
3.1 设计思路
使用 notification class
中的方法生成对应的位置的 notification component
,
使用notification component
渲染 notice components
- notificationInstance
来进行对创建 notification component
实例的缓存,然后在销毁时将缓存的实例删除
const notificationInstance = {};
// 销毁实例
destroy() {
Object.keys(notificationInstance).forEach(cacheKey => {
notificationInstance[cacheKey].destroy()
delete notificationInstance[cacheKey]
})
}
// 缓存实例 单例模式 防止实例重复生成
getNotificationInstance(
{
//.......省略部分代码........
},
callback: (n: any) => void,
) {
const cacheKey = `${prefixCls}-${placement}`;
if (notificationInstance[cacheKey]) {
callback(notificationInstance[cacheKey]);
return;
}
(Notification as any).newInstance(
{
//.......省略部分代码........
},
(notification: any) => {
// 缓存实例
notificationInstance[cacheKey] = notification;
callback(notification);
},
);
}
复制代码
-
noticeInstance
class Notification extends Component<NotificationProps, NotificationState> { state: NotificationState = { notices: [], // notice实例缓存 } add = (notice) => { const key = notice.key = notice.key || getUuid(); this.setState(previousState => { const notices = previousState.notices; if (!notices.filter(v => v.key === key).length) { return { notices: notices.concat(notice), }; } }); // 销毁实例 remove = (key) => { this.setState(previousState => { return { notices: previousState.notices.filter(notice => notice.key !== key), }; }); } render() { const { notices } = this.state; const noticeNodes = notices.map((notice, index) => { //.......省略部分代码........ return ( <Notice prefixCls={prefixCls} closeIcon={closeIcon} {...notice} key={key} update={update} onClose={onClose} onClick={notice.onClick} > {notice.content} </Notice> ); }); return ( <> {noticeNodes} </> ); } } 复制代码
3.2 实现细节
分析代码之前我们先来看下 notification 组件的结构,主要分成三层
- NotificationApi
调用堆栈如图
notificationApi 组件的封装,提供了类似notification.success notification.error notification.open notification.destroy 等api接口
const api: any = {
open: notice,
close(key: string) {
Object.keys(notificationInstance).forEach(cacheKey =>
notificationInstance[cacheKey].removeNotice(key),
);
},
config: setNotificationConfig,
destroy() {
Object.keys(notificationInstance).forEach(cacheKey => {
notificationInstance[cacheKey].destroy();
delete notificationInstance[cacheKey];
});
},
};
['success', 'info', 'warning', 'error'].forEach(type => {
api[type] = (args: ArgsProps) =>
api.open({
...args,
type,
});
});
复制代码
看上图NotificationApi 暴露了几个接口,大部分是通过类似open的一个工厂函数实现的,open方法又指向了notice,来看下notice
function notice(args: ArgsProps) {
const outerPrefixCls = args.prefixCls || 'ant-notification';
//.......省略部分代码........
const { placement, top, bottom, getContainer } = args;
// 生成实例方法 调用实例暴露出来的接口
getNotificationInstance(
{
//.......省略部分代码........
},
(notification: any) => {
notification.notice({
content: (),
duration,
......
});
},
);
}
复制代码
getNotificationInstance 命名方式是典型的单例模式,使用单例模式不仅节省了内存空间,而且单例延迟执行的特性也保证了在没有通知的情况下不会生成notification组件,保证了性能
// 单例模式 防止实例重复生成
function getNotificationInstance({
prefixCls,
placement,
getContainer,
}, callback: (n: any) => void) {
const cacheKey = `${prefixCls}-${placement}`;
if (notificationInstance[cacheKey]) {
callback(notificationInstance[cacheKey]);
return;
}
// 实例化Notification组件
(Notification as any).newInstance(
{
//.......配置信息省略........
},
(notification: any) => {
// 缓存实例
notificationInstance[cacheKey] = notification;
callback(notification);
},
);
}
复制代码
getNotificationInstance 第一个参数是配置信息,比如css前缀,弹出位置,通知栏的容器等。根据前两个条件自由组合生成cacheKey,也就是不同的弹出位置/css前缀 会生成多个的 notification实例, 其他情况不管调用多少次都只会生成一个notification 实例,notificationInstance做缓存,调用notification.destroy会用到
-
Notification
antd 强依赖 react-component组件库,notificationApi 依赖 rc-notification, 该组件主要是暴露 notification实例以及实例方法,通过ReactDOM.render将Notification组件渲染到页面上,可以选择渲染到传入的container或者body中。通过ref将notification实例传入callback回调函数。
Notification.newInstance = function newNotificationInstance(properties, callback) { const { getContainer, ...props } = properties || {}; const div = document.createElement('div'); if (getContainer) { const root = getContainer(); root.appendChild(div); } else { document.body.appendChild(div); } let called = false; // 对外暴露实例的方法 function ref(notification: Notification) { if (called) { return; } called = true; callback({ notice(noticeProps) { notification.add(noticeProps); }, removeNotice(key) { notification.remove(key); }, component: notification, destroy() { // 销毁notification实例容器 ReactDOM.unmountComponentAtNode(div); div.parentNode.removeChild(div); }, }); } // React版本升级到16.x以后 不支持 ReactDOM.render方法生成实例,antd 3.x版本进行修复,使用ref代替 // 2.x版本解决方案 我也有输出在第4部分内容里 ReactDOM.render(<Notification {...props} ref={ref} />, div); }; 复制代码
Notification组件主要是维护 notice生成,notice移除,notification实例渲染的 wrapper,noticelist渲染等
class Notification extends Component<NotificationProps, NotificationState> { // 维护了notices列表 state: NotificationState = { notices: [], }; add = (notice: NoticeContent) => { // notificationApi对外暴露了key key是区分生成notice的唯一性 如果没有设置 每次都生成唯一key notice.key = notice.key || getUuid(); const { key } = notice; const { maxCount } = this.props; // maxCount是对notice生成实例个数限制 rc-notification内部是暴露了maxCount。 // antd组件内部并没有用到maxCount也没有对外暴露 this.setState(previousState => { const { notices } = previousState; const noticeIndex = notices.map(v => v.key).indexOf(key); const updatedNotices = notices.concat(); if (noticeIndex !== -1) { updatedNotices.splice(noticeIndex, 1, notice); } else { if (maxCount && notices.length >= maxCount) { notice.updateKey = updatedNotices[0].updateKey || updatedNotices[0].key; updatedNotices.shift(); } updatedNotices.push(notice); } return { notices: updatedNotices, }; }); }; // 移除notice remove = (key: React.Key) => { this.setState(previousState => ({ notices: previousState.notices.filter(notice => notice.key !== key), })); }; render() { const { notices } = this.state; const { prefixCls, className, closeIcon, style } = this.props; const noticeNodes = notices.map((notice, index) => { //.......省略一大串代码........ const onClose = createChainedFunction( this.remove.bind(this, notice.key), notice.onClose, ) as any; return ( <Notice prefixCls={prefixCls} closeIcon={closeIcon} {...notice} key={key} update={update} onClose={onClose} onClick={notice.onClick} > {notice.content} </Notice> ); }); return ( <> {noticeNodes} </> ); } } 复制代码
createChainedFunction
- 能够删除当前的notification的缓存值
- 执行外部传入的关闭回调函数
// rc-utils 按照顺序链式调用函数去执行 export default function createChainedFunction() { const args = [].slice.call(arguments, 0); if (args.length === 1) { return args[0]; } return function chainedFunction() { for (let i = 0; i < args.length; i++) { if (args[i] && args[i].apply) { args[i].apply(this, arguments); } } }; } 复制代码
-
Notice
export default class Notice extends Component<NoticeProps> { componentDidUpdate(prevProps: NoticeProps) { if (this.props.duration !== prevProps.duration || this.props.update) { this.restartCloseTimer(); } } close = (e?: React.MouseEvent<HTMLAnchorElement>) => { if (e) { e.stopPropagation(); } this.clearCloseTimer(); this.props.onClose(); }; startCloseTimer = () => { if (this.props.duration) { this.closeTimer = window.setTimeout(() => { this.close(); }, this.props.duration * 1000); } }; clearCloseTimer = () => { if (this.closeTimer) { clearTimeout(this.closeTimer); this.closeTimer = null; } }; restartCloseTimer() { this.clearCloseTimer(); this.startCloseTimer(); } render() { return ( <></> ); } 复制代码