Ant design的Notification源碼分析

notification簡介

notification
notification就是通知提醒框,在系統四個角顯示通知提醒信息。常常用於如下狀況:

  • 較爲複雜的通知內容。
  • 帶有交互的通知,給出用戶下一步的行動點。
  • 系統主動推送。

先來看一下notification的API。css

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的官網api

notification的結構

在分析代碼以前,咱們先來看下notification的結構,通知組件主要分爲三層,由外到內分別是數組

NotificationApi => Notification => n*Notice。antd

NotificationApi

NotificationApi是一個封裝的接口,提供統一調用的API,如info(),warn()等。app

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組件也採用了一樣的設計。

相關文章
相關標籤/搜索