在 React 中使用新的门户功能

React v16 引入了一项名为 portals的新功能。

门户提供了一种第一类的方式来将孩子转化为存在于父母组件的DOM层次外的DOM节点。

通常情况下,功能组件或类组件会渲染一个 React 元素树(通常由 JSX 生成)。

在 v16 之前,只有少数儿童类型被允许渲染:

  • nullfalse (表示无处不在)。
  • JSX.
  • 反应元素
 1function Example(props) {
 2  return null;
 3}
 4function Example(props) {
 5  return false;
 6}
 7function Example(props) {
 8  return <p>Some JSX</p>;
 9}
10function Example(props) {
11  return React.createElement(
12    'p',
13    null,
14    'Hand coded'
15  );
16}

在v16中,更多的儿童类型可以渲染:

  • 數字(包括「無限」和「NaN」)。
  • 字符串
  • 回應端口
  • 可渲染的兒童數量

眾生,眾生,眾生,眾生,眾生,眾生。

 1function Example(props) {
 2  return 42;  // Becomes a text node.
 3}
 4function Example(props) {
 5  return 'The meaning of life.';  // Becomes a text node.
 6}
 7function Example(props) {
 8  return ReactDOM.createPortal(
 9    // Any valid React child type
10    [
11      'A string',
12      <p>Some JSX</p>,
13      'etc'
14    ],
15    props.someDomNode
16  );
17}

React 门户是通过调用 ReactDOM.createPortal来创建的。第一个参数应该是可渲染的孩子。第二个参数应该是可渲染的孩子将被渲染的 DOM 节点的参考。

请注意,createPortal 位于 ReactDOM 名称空间中,而不是 CreateElement 这样的 React 名称空间。

一些观察读者可能注意到ReactDOM.createPortal的签名与ReactDOM.render相同,这使其易于记住。

何时使用

React 门户非常有用,当一个主组件具有过流:隐藏的声明或具有影响 堆栈背景的属性,并且您需要视觉地打破其容器。

事件泡沫通过门户

React 文件非常清楚地解释了这一点。

尽管一个门户可以在DOM树的任何地方,但它在所有其他方面都表现得像一个正常的React孩子一样。 背景功能完全相同,无论孩子是否是一个门户,因为门户仍然存在于React树中,不管它在DOM树中的位置

这包括事件泡沫。

这使得在您的对话、浮动卡等中聆听事件,就像它们在与母组件相同的DOM树中进行渲染一样容易。

例子

在下面的示例中,我们将利用 React 门户和其事件泡沫功能。

标记开始如下。

1<div class="PageHolder">
2</div>
3<div class="DialogHolder is-empty">
4  <div class="Backdrop"></div>
5</div>
6<div class="MessageHolder">
7</div>

.PageHolder div是我们应用程序的主要部分生活的地方。 .DialogHolder div将是任何生成的对话进行渲染的地方。

由于我们希望所有对话都视觉上位于我们的应用程序的主要部分,所以.DialogHolder``div具有z-index: 1的声明,这将创建一个新的堆积环境,而不论是.PageHolder的堆积环境。

因为我们希望所有的消息都视觉上超过任何对话,所以.MessageHolder``divz-index: 1的声明,这将为.DialogHolder的堆积背景创建一个兄弟的堆积背景,尽管兄弟堆积背景的z-index具有相同的价值,但这仍然会反映出我们想要的样子,因为在DOM树中.MessageHolder之后。

下面的 CSS 概述了建立所需的堆栈背景所需的规则。

 1.PageHolder {
 2  /* Just use stacking context of parent element. */
 3  /* A z-index: 1 would still work here. */
 4}
 5
 6.DialogHolder {
 7  position: fixed;
 8  top: 0; left: 0;
 9  right: 0; bottom: 0;
10  z-index: 1;
11}
12
13.MessageHolder {
14  position: fixed;
15  top: 0; left: 0;
16  width: 100%;
17  z-index: 1;
18}

该示例将有一个页面组件,该组件将转换为.PageHolder

1class Page extends React.Component { /* ... */ }
2
3ReactDOM.render(
4  <Page/>,
5  document.querySelector('.PageHolder')
6)

由于我们的页面组件将将对话和消息渲染到.DialogHolder.MessageHolder,分别,它将需要在渲染时引用这些持有人div

我们可以在渲染页面组件之前解决这些持有者div的引用,并将它们作为属性传递给页面组件。

1let dialogHolder = document.querySelector('.DialogHolder');
2let messageHolder = document.querySelector('.MessageHolder');
3
4ReactDOM.render(
5  <Page dialogHolder={dialogHolder} messageHolder={messageHolder}/>,
6  document.querySelector('.PageHolder')
7);

我们可以将选项转移到页面组件作为属性,然后在组件WillMount中解决初始渲染的引用,如果选项改变,则在组件WillReceiveProps中重新解决。

 1class Page extends React.Component {
 2
 3  constructor(props) {
 4    super(props);
 5    let { dialogHolder = '.DialogHolder',
 6          messageHolder = '.MessageHolder' } = props
 7
 8    this.state = {
 9      dialogHolder,
10      messageHolder,
11    }
12  }
13
14  componentWillMount() {
15    let state = this.state,
16        dialogHolder = state.dialogHolder,
17        messageHolder = state.messageHolder
18
19    this._resolvePortalRoots(dialogHolder, messageHolder);
20  }
21
22  componentWillReceiveProps(nextProps) {
23    let props = this.props,
24        dialogHolder = nextProps.dialogHolder,
25        messageHolder = nextProps.messageHolder
26
27    if (props.dialogHolder !== dialogHolder ||
28        props.messageHolder !== messageHolder
29    ) {
30      this._resolvePortalRoots(dialogHolder, messageHolder);
31    }
32  }
33
34  _resolvePortalRoots(dialogHolder, messageHolder) {
35    if (typeof dialogHolder === 'string') {
36      dialogHolder = document.querySelector(dialogHolder)
37    }
38    if (typeof messageHolder === 'string') {
39      messageHolder = document.querySelector(messageHolder)
40    }
41    this.setState({
42      dialogHolder,
43      messageHolder,
44    })
45  }
46
47}

现在我们已经确保我们将为门户提供DOM引用,我们可以将页面组件转换为对话和消息。

与 React 元素一样,React 门户基于组件属性和状态进行渲染。对于这个例子,我们将有两个按钮。一个将创建对话门户,在点击时在对话持有器中进行渲染,另一个将创建消息门户,在消息持有器中进行渲染。

 1class Page extends React.Component {
 2  // ...
 3
 4  constructor(props) {
 5    super(props);
 6    let { dialogHolder = '.DialogHolder',
 7          messageHolder = '.MessageHolder' } = props
 8
 9    this.state = {
10      dialogHolder,
11      dialogs: [],
12      messageHolder,
13      messages: [],
14    }
15  }
16
17  render() {
18    let state = this.state,
19        dialogs = state.dialogs,
20        messages = state.messages
21
22    return (
23      <div className="Page">
24        <button onClick={evt => this.addNewDialog()}>
25          Add Dialog
26        </button>
27        <button onClick={evt => this.addNewMessage()}>
28          Add Message
29        </button>
30        {dialogs}
31        {messages}
32      </div>
33    )
34  }
35
36  addNewDialog() {
37    let dialog = ReactDOM.createPortal((
38        <div className="Dialog">
39          ...
40        </div>
41      ),
42      this.state.dialogHolder
43    )
44    this.setState({
45      dialogs: this.state.dialogs.concat(dialog),
46    })
47  }
48
49  addNewMessage() {
50    let message = ReactDOM.createPortal((
51        <div className="Message">
52          ...
53        </div>
54      ),
55      this.state.messageHolder
56    )
57    this.setState({
58      messages: this.state.messages.concat(message),
59    })
60  }
61
62  // ...
63}

为了证明事件会从 React 门户组件到母组件的泡沫,让我们在 .Page div 上添加一个点击处理器。

 1class Page extends React.Component {
 2  // ...
 3
 4  render() {
 5    let state = this.state,
 6        dialogs = state.dialogs,
 7        messages = state.messages
 8
 9    return (
10      <div className="Page" onClick={evt => this.onPageClick(evt)}>
11        ...
12      </div>
13    )
14  }
15
16  onPageClick(evt) {
17    console.log(`${evt.target.className} was clicked!`);
18  }
19
20  // ...
21}

当单击对话或消息时,将呼叫onPageClick事件处理器(只要另一个处理器没有停止传播)。

看一看,一看,一看,一看,一看,一看。

👉在遇到过流:隐藏或堆积背景问题时使用 React 门户!

Published At
Categories with 技术
Tagged with
comments powered by Disqus