Ant design的Notification原始碼分析

水星的衛星發表於2019-02-25

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

相關文章