React 16 已经发布了,其中一个很有趣的是添加了 “Portals”。
Portals 可以让你在父组件外渲染 react 受控 DOM 节点。react doc 也使用了一个模态框的例子很好地解释了它。用也做 tooltips 也很合适(这是我早前做的一个例子)。
因为所有 portal 做的,都是将一个元素放到其他元素上,你并没有被限制放在 当前 document 的任何地方。你可以添加到另一个 document 的 body 中,可能是完全不同浏览器窗口的一个 document。
下面我有一个基本的页面(左边),包含一个计数器及深红色按钮。另一个窗口(右边)同样是 react app 的一部分。
右边的窗口是属于 同一个 React app 这个事实应该让你感到惊讶。
上面图片中能看到的所有东西(除了树)都是在下面代码展示的同一个组件中:
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> | |
); | |
} | |
} |
你已经成功了,
有一点特殊,里面的所有东西都会被渲染到另一个 window 下。
尤其是,
做了两件事。
- 1、 当组件渲染时打开一个浏览器窗口。
- 2、 创建一个
portal
并将props.children
添加到新窗口的 body。
这不是最酷的事情吗?
我是多么兴奋,必须得冷静一下。
…
I saw a duck!
…
上面提到的 body中的组件就是下面这样。 React 16 新增的部分 ReactDOM.createPortal
在 11 行 —— 那就是黑魔法。
class MyWindowPortal extends React.PureComponent { | |
constructor(props) { | |
super(props); | |
// STEP 1: create a container <div> | |
this.containerEl = document.createElement('div'); | |
this.externalWindow = null; | |
} | |
render() { | |
// STEP 2: append props.children to the container <div> that isn't mounted anywhere yet | |
return ReactDOM.createPortal(this.props.children, this.containerEl); | |
} | |
componentDidMount() { | |
// STEP 3: open a new browser window and store a reference to it | |
this.externalWindow = window.open('', '', 'width=600,height=400,left=200,top=200'); | |
// STEP 4: append the container <div> (that has props.children appended to it) to the body of the new window | |
this.externalWindow.document.body.appendChild(this.containerEl); | |
} | |
componentWillUnmount() { | |
// STEP 5: This will fire when this.state.showWindowPortal in the parent component becomes false | |
// So we tidy up by closing the window | |
this.externalWindow.close(); | |
} | |
} |
那有意义吗?组件并没有返回什么,只是做了一些其他事情。
换一个角度来想可能是这样的:按理来说,一个父组件对子组件说:“嘿,渲染 DOM, 然后把结果添加到我这里。”,然后子组件照做了。但是这次,任性的子组件说,“不!我要将东东渲染到另一个窗口,然后写一篇关于它的博客。”
现在,我知道你在想什么。
你有点渴,并且想知道你是否该喝点水了。是的,去吧。
另一件你可能在想的事情是:将一些 DOM 注入到无样式的空白窗口有什么好处?可能 Craigslist 或 Wikipedia 也不知道。但你的网站很美,任何时候都不能弹出小窗口。
好吧,好消息来了,大家注意了!
起初,我还在想是否有一个简单的方式将样式复制到新窗口。然后我想起我的生活只是被一些毫无意义的事件填充,它们唯一的目的只是让我分心,阻止我内心的空虚寂寞冷。
所以亲自写这个函数就是乐趣!
看下面:
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); | |
} | |
}); | |
} |
事实上,我并不是很了解 styleSheet
。我期待从评论中得知有什么奇技淫巧。
现在我可以在打开新窗口的时候很简单地复制样式,像这样:
this.externalWindow = window.open(/* ... */); | |
copyStyles(document, this.externalWindow.document); |
我不认为这能在 Medium app 的 iOS/Android 平台上运行,所以请在浏览器打开。
好了,我认为这篇文章该结束了。
Bye!