Ant design的Notification源码分析

2,591 阅读6分钟

notification简介

notification
notification就是通知提醒框,在系统四个角显示通知提醒信息。经常用于以下情况:

  • 较为复杂的通知内容。
  • 带有交互的通知,给出用户下一步的行动点。
  • 系统主动推送。

先来看一下notification的API。

API

  • notification.success(config)
  • notification.error(config)
  • notification.info(config)
  • notification.warning(config)
  • notification.warn(config)
  • notification.close(key: String)
  • notification.destroy()

可以看到,notification的API在antd的组件中可以说是非常特别的,看着是不是有点眼熟,很像经常使用的Console的API,调用起来十分简单。

  • console.log()
  • console.error()
  • console.info()
  • console.warn()

config的配置也比较简单,主要是标题,内容,关闭时的延时和回调等。详见ANTD的官网

notification的结构

在分析代码之前,我们先来看下notification的结构,通知组件主要分为三层,由外到内分别是

NotificationApi => Notification => n*Notice。

NotificationApi

NotificationApi是一个封装的接口,提供统一调用的API,如info(),warn()等。

Notification

Notification是一个Notice容器,就是用来容纳Notice列表的父组件,提供了添加,删除等操作Notice的方法。

Notice

Notice就是我们所看到的通知标签了。

源码分析

先从入口index.js入手,因为这是一个notification的API封装,不是一个组件,所以没有render方法。

//.......省略部分代码........

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];
    });
  },
};

//.......省略部分代码........

//不同类型通过open传入参数实现
['success', 'info', 'warning', 'error'].forEach((type) => {
  api[type] = (args: ArgsProps) => api.open({
    ...args,
    type,
  });
});

api.warn = api.warning;

//封装后的接口
export interface NotificationApi {
  success(args: ArgsProps): void;
  error(args: ArgsProps): void;
  info(args: ArgsProps): void;
  warn(args: ArgsProps): void;
  warning(args: ArgsProps): void;
  open(args: ArgsProps): void;
  close(key: string): void;
  config(options: ConfigProps): void;
  destroy(): void;
}
export default api as NotificationApi;

接口比较清晰,可以看出API提供的不同的方法实际是通过一个类似工厂方法的open函数实现的,open函数的具体实现是notice,那么看下这个notice函数。

function notice(args: ArgsProps) {
  const outerPrefixCls = args.prefixCls || 'ant-notification';
  const prefixCls = `${outerPrefixCls}-notice`;
  const duration = args.duration === undefined ? defaultDuration : args.duration;

//生成icon组件
  let iconNode: React.ReactNode = null;
  if (args.icon) {
    iconNode = (
      <span className={`${prefixCls}-icon`}>
        {args.icon}
      </span>
    );
  } else if (args.type) {
    const iconType = typeToIcon[args.type];
    iconNode = (
      <Icon
        className={`${prefixCls}-icon ${prefixCls}-icon-${args.type}`}
        type={iconType}
      />
    );
  }

  const autoMarginTag = (!args.description && iconNode)
    ? <span className={`${prefixCls}-message-single-line-auto-margin`} />
    : null;

//得到Notification实例
  getNotificationInstance(outerPrefixCls, args.placement || defaultPlacement, (notification: any) => {
    notification.notice({
      content: (
        <div className={iconNode ? `${prefixCls}-with-icon` : ''}>
          {iconNode}
          <div className={`${prefixCls}-message`}>
            {autoMarginTag}
            {args.message}
          </div>
          <div className={`${prefixCls}-description`}>{args.description}</div>
          {args.btn ? <span className={`${prefixCls}-btn`}>{args.btn}</span> : null}
        </div>
      ),
      duration,
      closable: true,
      onClose: args.onClose,
      key: args.key,
      style: args.style || {},
      className: args.className,
    });
  });
}

这段代码主要的部分就是调用了getNotificationInstance函数,看名字应该是得到Notification的实例,命名方式是典型的单例模式,作为列表的容器组件,使用单例模式不仅节省了内存空间,而且单例延迟执行的特性也保证了在没有通知的情况下不会生成notification组件,提升了页面的性能。

function getNotificationInstance(prefixCls: string, placement: NotificationPlacement, callback: (n: any) => void)

查看定义,第一个参数是css前缀,第二个参数是notification的弹出位置,分为topLeft topRight bottomLeft bottomRight,第三个参数是一个回调,回调的参数是notification实例,可以看到,在回调中调用了notification的notice方法,notice方法的参数是一个对象,content看名字应该是通知标签的内容,其他的参数也是调用notification中传入的config参数。 接下来看下getNotificationInstance的实现

function getNotificationInstance(prefixCls: string, placement: NotificationPlacement, callback: (n: any) => void) {
  const cacheKey = `${prefixCls}-${placement}`;
  if (notificationInstance[cacheKey]) {
    callback(notificationInstance[cacheKey]);
    return;
  }

  //---实例化Notification组件
  (Notification as any).newInstance({
    prefixCls,
    className: `${prefixCls}-${placement}`,
    style: getPlacementStyle(placement),
    getContainer: defaultGetContainer,
  }, (notification: any) => {
    notificationInstance[cacheKey] = notification;
    callback(notification);
  });
}

代码很简短,可以看到确实是使用了单例模式,因为存在4个弹出位置,所以将每个位置的notification实例存放在notificationInstance[cacheKey]数组里,cacheKey是css前缀和弹出位置的组合,用以区分每个实例。接下来进入newInstance方法来看下是怎么使用单例模式生成notification实例的。

实例化Notification

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) {
    if (called) {
      return;
    }
    called = true;
    callback({
      notice(noticeProps) {
        notification.add(noticeProps);
      },
      removeNotice(key) {
        notification.remove(key);
      },
      component: notification,
      destroy() {
        ReactDOM.unmountComponentAtNode(div);
        div.parentNode.removeChild(div);
      },
    });
  }
  ReactDOM.render(<Notification {...props} ref={ref} />, div);
};

主要完成了两件事

  • 通过ReactDOM.render将Notification组件渲染到页面上,可以选择渲染到传入的container或者body中。
  • 通过ref将notification实例传入callback回调函数。 可以看到传入callback的参数对notification又做了一层封装,目的是为了封装destroy函数,其中
    • notice():添加一个notice组件到notification
    • removeNotice():删除指定notice组件。
    • destroy():销毁notification组件。

添加Notice

再回过头来看回调函数的内容。

 getNotificationInstance(outerPrefixCls, args.placement || defaultPlacement, (notification: any) => {
    notification.notice({
      content: (
        <div className={iconNode ? `${prefixCls}-with-icon` : ''}>
          {iconNode}
          <div className={`${prefixCls}-message`}>
            {autoMarginTag}
            {args.message}
          </div>
          <div className={`${prefixCls}-description`}>{args.description}</div>
          {args.btn ? <span className={`${prefixCls}-btn`}>{args.btn}</span> : null}
        </div>
      ),
      duration,
      closable: true,
      onClose: args.onClose,
      key: args.key,
      style: args.style || {},
      className: args.className,
    });
  });

调用了notification的notice方法,由前面的代码可知notice其实是调用了Notification组件的add方法,记下来看下add方法是怎样将标签添加进Notification的。

//省略部分代码

 state = {
  notices: [],
};

//省略部分代码

  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),
      };
    }
  });
}

Notification将要显示的通知列表存在state的notices中,同通过add函数动态添加,key是该notice的唯一标识,通过filter将已存在的标签过滤掉。可以想见,Notification就是将state中的notices通过map渲染出要显示的标签列表,直接进入Notification组件的render方法。

  render() {
  const props = this.props;
  const noticeNodes = this.state.notices.map((notice) => {
    const onClose = createChainedFunction(this.remove.bind(this, notice.key), notice.onClose);
    return (<Notice
      prefixCls={props.prefixCls}
      {...notice}
      onClose={onClose}
    >
      {notice.content}
    </Notice>);
  });
  const className = {
    [props.prefixCls]: 1,
    [props.className]: !!props.className,
  };
  return (
    <div className={classnames(className)} style={props.style}>
      <Animate transitionName={this.getTransitionName()}>{noticeNodes}</Animate>
    </div>
  );
}
}

根据state的notices生成Notice组件列表noticeNodes,然后将noticeNodes插入到一个Animate的动画组件中。其中createChainedFunction的作用是一次调用传入的各函数,其中remove方法是移除state中相应的节点,onClose是传入的关闭标签后的回调函数。 看到这里Notification的结构已经比较清晰了,最后再来看下Notice组件的实现。

export default class Notice extends Component {
  static propTypes = {
    duration: PropTypes.number,
    onClose: PropTypes.func,
    children: PropTypes.any,
  };

  static defaultProps = {
    onEnd() {
    },
    onClose() {
    },
    duration: 1.5,
    style: {
      right: '50%',
    },
  };

  componentDidMount() {
    this.startCloseTimer();
  }

  componentWillUnmount() {
    this.clearCloseTimer();
  }

  close = () => {
    this.clearCloseTimer();
    this.props.onClose();
  }

  startCloseTimer = () => {
    if (this.props.duration) {
      this.closeTimer = setTimeout(() => {
        this.close();
      }, this.props.duration * 1000);
    }
  }

  clearCloseTimer = () => {
    if (this.closeTimer) {
      clearTimeout(this.closeTimer);
      this.closeTimer = null;
    }
  }
  render() {
    const props = this.props;z
    const componentClass = `${props.prefixCls}-notice`;
    const className = {
      [`${componentClass}`]: 1,
      [`${componentClass}-closable`]: props.closable,
      [props.className]: !!props.className,
    };
    return (
      <div className={classNames(className)} style={props.style} onMouseEnter={this.clearCloseTimer}
        onMouseLeave={this.startCloseTimer}
      >
        <div className={`${componentClass}-content`}>{props.children}</div>
          {props.closable ?
            <a tabIndex="0" onClick={this.close} className={`${componentClass}-close`}>
              <span className={`${componentClass}-close-x`}></span>
            </a> : null
          }
      </div>
    );
  }
}

这个组件比较简单,主要是实现标签显示一段时间后自动消失,通过setTimeout设置一段时间后调用close方法,也就是上一段代码中实现的移除state中的相应节点以及调用相应的回调函数。

总结

看到这里antd的通知组件的实现已经比较清晰了,代码并没有特别复杂的部分,但是这种使用单例模式动态添加组件的设计十分值得借鉴,在实现类似通知组件或者需要动态添加的组件的时候可以参考这种设计模式,antd的Message组件也采用了同样的设计。