[译]Using a React 16 Portal to do something cool

原文: Using a React 16 Portal to do something cool
作者: David Gilbertson

React 16版本中增加了一个很有意思的功能“Portals”传送门。
Portals允许你将一小部分React管理的DOM放在父组件之外。React docs使用一个模态框的例子很好的解释了这个属性。它也能很好的用在tooltips上(这里是我之前写的栗子)。
但是这些都不是非常的有意思,让我们做一些更奇妙的事情……
因为传送门所做的事情是获得一个元素并将它添加到其他元素中去,你并不局限于在当前文档的其他地方使用它。你还可以把它添加到一个来自不同窗口的文档的body元素里。


你在上面看到的所有东西(除了树)都在下面这一个组件中。

class App extends React.PureComponent {
  constructor(props) {
    super(props);
    
    this.state = {
      counter: 0,
      showWindowPortal: false,
    };
    
    this.toggleWindowPortal = this.toggleWindowPortal.bind(this);
  }

  componentDidMount() {
    window.setInterval(() => {
      this.setState(state => ({
        ...state,
        counter: state.counter + 1,
      }));
    }, 1000);
  }
  
  toggleWindowPortal() {
    this.setState(state => ({
      ...state,
      showWindowPortal: !state.showWindowPortal,
    }));
  }
  
  render() {
    return (
      <div>
        <h1>Counter: {this.state.counter}</h1>
        
        <button onClick={this.toggleWindowPortal}>
          {this.state.showWindowPortal ? 'Close the' : 'Open a'} Portal
        </button>
        
        {this.state.showWindowPortal && (
          <MyWindowPortal>
            <h1>Counter in a portal: {this.state.counter}</h1>
            <p>Even though I render in a different window, I share state!</p>
            
            <button onClick={() => this.setState({ showWindowPortal: false })} >
              Close me!
            </button>
          </MyWindowPortal>
        )}
      </div>
    );
  }
}

你已经得到结论,<MyWindowPortal>有一点特别,并且它里面所有内容都渲染在了一个不同的窗口中。
你是正确的,此外<MyWindowPortal>还做两件事情:
1.渲染时打开一个新的浏览器窗口
2.创建一个“传送门”并添加props.children到新窗口的body元素

这不是一件非常酷的事情么?
我激动的想要出去走走。
。 。 。
我看见了一只鸭子!
。。。
(作者你够了...)

下面是之前提到的组件的主要内容。其中在React 16中新增的是在11行的ReactDOM.createPortal —— 那就是魔术产生的地方。

class MyWindowPortal extends React.PureComponent {
  constructor(props) {
    super(props);
    // STEP 1: 创建一个 <div>
    this.containerEl = document.createElement('div');
    this.externalWindow = null;
  }
  
  render() {
    // STEP 2: 在<div>中添加props.children,他不会在任何地方渲染
    return ReactDOM.createPortal(this.props.children, this.containerEl);
  }

  componentDidMount() {
    // STEP 3: 打开新的浏览器窗口,保存对他的引用
    this.externalWindow = window.open('', '', 'width=600,height=400,left=200,top=200');

    // STEP 4: 将<div>添加到新窗口中
    this.externalWindow.document.body.appendChild(this.containerEl);
  }

  componentWillUnmount() {
    // STEP 5: 父组件的this.state.showWindowPortal为false时触发
    // 我们关掉了这个窗口
    this.externalWindow.close();
  }
}

这个组件没有返回值,它做了一些其他的事情。
也许可以这么想:一个父组件对子组件说:“嘿,渲染一些DOM,然后添加给我”,然后子组件如是去做了。但实际上,这个任性的孩子说“不!我要在一个不同的窗口中渲染东西,再写一个关于他的博客文章!”
。。。
另一件你可能想到的事情是:什么产品会把一个没有任何样式的DOM注入到空窗口中?可能在Craigslist或者Wikipedia不会有人注意这个,但是你的网站很好看,你不允许自己的对话弹出框里全是times-new-romany字体。
那么,这里有个好消息!


一开始我希望有个简单的方法把样式复制到新窗口中。但是之后我注意到我的人生只不过是被一些毫无意义的事情填充,它们存在的目的只是分散我内心深处的空虚。(额不知道是不是这么翻译...)
因此自己来写这个方法会很有趣!

function copyStyles(sourceDoc, targetDoc) {
  Array.from(sourceDoc.styleSheets).forEach(styleSheet => {
    if (styleSheet.cssRules) { // for <style> elements
      const newStyleEl = sourceDoc.createElement('style');

      Array.from(styleSheet.cssRules).forEach(cssRule => {
        // write the text of each rule into the body of the style element
        newStyleEl.appendChild(sourceDoc.createTextNode(cssRule.cssText));
      });

      targetDoc.head.appendChild(newStyleEl);
    } else if (styleSheet.href) { // for <link> elements loading CSS from a URL
      const newLinkEl = sourceDoc.createElement('link');

      newLinkEl.rel = 'stylesheet';
      newLinkEl.href = styleSheet.href;
      targetDoc.head.appendChild(newLinkEl);
    }
  });
}
Comments
Write a Comment