使用 react 16 portal 做一些很酷的事情

React 16 已经发布了,其中一个很有趣的是添加了 “Portals”。

Portals 可以让你在父组件外渲染 react 受控 DOM 节点。react doc 也使用了一个模态框的例子很好地解释了它。用也做 tooltips 也很合适(这是我早前做的一个例子)。

但这些都并不是很有趣,让我们做一些更不可思议的事情…

因为所有 portal 做的,都是将一个元素放到其他元素上,你并没有被限制放在 当前 document 的任何地方。你可以添加到另一个 document 的 body 中,可能是完全不同浏览器窗口的一个 document。

下面我有一个基本的页面(左边),包含一个计数器及深红色按钮。另一个窗口(右边)同样是 react app 的一部分。

右边的窗口是属于 同一个 React app 这个事实应该让你感到惊讶。

https://cdn-images-1.medium.com/max/2000/1*ogsV-9IGNtaVjne2fb_oEA.png

上面图片中能看到的所有东西(除了树)都是在下面代码展示的同一个组件中:

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>
);
}
}
view raw App.jsx hosted with ❤ by GitHub

你已经成功了, 有一点特殊,里面的所有东西都会被渲染到另一个 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 也不知道。但你的网站很美,任何时候都不能弹出小窗口。

好吧,好消息来了,大家注意了!

https://cdn-images-1.medium.com/max/1000/1*eU-7ArIucnG5OreaPIJlEg.png

起初,我还在想是否有一个简单的方式将样式复制到新窗口。然后我想起我的生活只是被一些毫无意义的事件填充,它们唯一的目的只是让我分心,阻止我内心的空虚寂寞冷。

所以亲自写这个函数就是乐趣!

看下面:

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);
}
});
}
view raw copyStyles.js hosted with ❤ by GitHub

事实上,我并不是很了解 styleSheet。我期待从评论中得知有什么奇技淫巧。

现在我可以在打开新窗口的时候很简单地复制样式,像这样:

this.externalWindow = window.open(/* ... */);
copyStyles(document, this.externalWindow.document);

我不认为这能在 Medium app 的 iOS/Android 平台上运行,所以请在浏览器打开。

好了,我认为这篇文章该结束了。

Bye!

0 comments
Anonymous
Markdown is supported

Be the first person to leave a comment!

0%