在React中,点击后在新窗口中打开一个组件

31

我有一个显示数据的组件。当从父组件点击一个按钮/链接时,我需要在一个新窗口中打开这个组件。

export default class Parent extends Component {
    construtor(props) {
        super(props);
    }

    viewData = () => {
        window.open('childcomponent.js','Data','height=250,width=250');
    }

    render() {
        return (
            <div> <a onclick={this.viewData}>View Data</a></div>
        )
    }
}

我不知道如何调用另一个组件并在指定大小的新窗口中显示它。

实际上,我需要向该子组件发送一个props,它将从数据库中获取数据并呈现它。

7个回答

33
你可以使用ReactDOM.createPortal在一个新窗口中渲染一个组件,正如David Gilbertson在他的post中所解释的那样。
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();
  }
}

7
这个解决方案已经很好地封装在一个npm模块中,链接在这里:https://www.npmjs.com/package/react-new-window。 - Dane Jordan
1
样式存在问题,因为这些样式未应用于新窗口中的所有组件。可能与新 DOM 的 head 现在为空有关,而 react 应用程序的 head 完全填充了相同的样式和脚本,react 应用程序正在填充/更新。 - Pepe Alvarez

30

投票赞成的答案非常好!

为了方便未来搜索,这里提供一个函数组件版本。

const RenderInWindow = (props) => {
  const [container, setContainer] = useState(null);
  const newWindow = useRef(null);

  useEffect(() => {
    // Create container element on client-side
    setContainer(document.createElement("div"));
  }, []);

  useEffect(() => {
    // When container is ready
    if (container) {
      // Create window
      newWindow.current = window.open(
        "",
        "",
        "width=600,height=400,left=200,top=200"
      );
      // Append container
      newWindow.current.document.body.appendChild(container);

      // Save reference to window for cleanup
      const curWindow = newWindow.current;

      // Return cleanup function
      return () => curWindow.close();
    }
  }, [container]);

  return container && createPortal(props.children, container);
};

1
我们如何携带应用程序的样式和数据。 - Rahul
@Rahul 请看这里 - amiregelz
2
@rob-gordon 你有没有想法为什么在我的create-react-app生产构建中这个不起作用? - amiregelz
1
很难猜测,没有看到具体情况 - 但我了解如果你正在进行SSR,有时服务器上可能无法使用document.createElement,从而导致失败。在这种情况下,您可以在useEffect中创建div。不过我不知道这是否适用于您的情况。 - rob-gordon
@rob-gordon 这个解决方案对我很有效。但是当我关闭新打开的窗口,然后再次点击按钮打开新窗口时什么也没发生。每次我点击按钮都应该打开一个新窗口。 - rocco
1
@rocco 当你想要控制窗口的打开/关闭状态时,情况会变得有点复杂。请查看 https://stackblitz.com/edit/react-zpazdi?file=src/App.js - rob-gordon

10
这个答案是基于David Gilbertson的帖子的。它已经被修改以在Edge中运行。为了使其在Edge中工作,divstyle元素必须使用将被渲染的窗口创建
class MyWindowPortal extends React.PureComponent {
  constructor(props) {
    super(props);
    this.containerEl = null;
    this.externalWindow = null;
  }

  componentDidMount() {
    // STEP 1: Create a new window, a div, and append it to the window. The div 
    // *MUST** be created by the window it is to be appended to (Edge only)
    this.externalWindow = window.open('', '', 'width=600,height=400,left=200,top=200');
    this.containerEl = this.externalWindow.document.createElement('div');
    this.externalWindow.document.body.appendChild(this.containerEl);
  }

  componentWillUnmount() {
    // STEP 2: This will fire when this.state.showWindowPortal in the parent component
    // becomes false so we tidy up by just closing the window
    this.externalWindow.close();
  }
  
  render() {
    // STEP 3: The first render occurs before componentDidMount (where we open the
    // new window) so container may be null, in this case render nothing.
    if (!this.containerEl) {
      return null;
    } 
    
    // STEP 4: Append props.children to the container <div> in the new window
    return ReactDOM.createPortal(this.props.children, this.containerEl);  
  }
}

完整修改后的源代码可以在这里找到 https://codepen.io/iamrewt/pen/WYbPWN

3
在窗口挂载后,我遇到了第二次渲染时的问题。除非this.containerEl为空,否则没有任何东西告诉它重新渲染。为了解决这个问题,在componentDidMount的末尾,我添加了setState({mounted:true})。 - Kenny Hammerlund
我尝试了上面的代码示例步骤,但在IE Edge中仍无法正常工作。还有其他解决方案吗? - Karthik
2
我可以使用这个方法在弹出窗口中加载React组件。但是样式没有被传递过去。在弹出窗口中加载的组件不包含应用程序中定义的样式。有没有一种方法可以将样式传递到弹出窗口中? - Vignesh M

5

您不会直接打开组件。您需要一个新的页面/视图来显示组件。当您打开窗口时,然后将其指向适当的URL。

至于大小,您需要在“open”函数的第三个参数中提供它作为字符串,您已经做得正确:

window.open('http://example.com/child-path','Data','height=250,width=250');

请注意,由于各种原因,浏览器可能不会遵守您的宽度和高度请求。因此,建议您还应用适当的CSS来获得正确大小的空间,以防它比您想要的打开得更大。

2

对于任何在复制样式方面遇到问题的人,参考这个答案,但是在生产环境中无法正常工作。

如果您使用window.open('', ...)打开新窗口,则新窗口很可能具有about:blank作为URL,并且不会找到CSS文件,因为它们具有相对路径。您需要将href属性设置为绝对路径:

function copyStyles(src, dest) {
  Array.from(src.styleSheets).forEach((styleSheet) => {
    const styleElement = styleSheet.ownerNode.cloneNode(true);
    styleElement.href = styleSheet.href;
    dest.head.appendChild(styleElement);
  });
  Array.from(src.fonts).forEach((font) => dest.fonts.add(font));
}

2

在当前答案的基础上,要将原始窗口的样式复制到弹出窗口中,可以执行以下操作:

function copyStyles(src, dest) {
    Array.from(src.styleSheets).forEach(styleSheet => {
        dest.head.appendChild(styleSheet.ownerNode.cloneNode(true))
    })
    Array.from(src.fonts).forEach(font => dest.fonts.add(font))
}

然后添加。
copyStyles(window.document, popupWindow.document);

在调用window.open之后,并且引用已初始化。

0
如果有人在为新窗口添加样式时遇到麻烦,诀窍是将整个DOM头从父级复制到新的弹出窗口中。 首先,您需要建立整个HTML框架。
newWindow.document.write("<!DOCTYPE html");
newWindow.document.write("<html>");
newWindow.document.write("<head>");
newWindow.document.write("</head>");
newWindow.document.write("<body>");
newWindow.document.write("</body>");
newWindow.document.write("</html>");
// Append the new container to the body of the new window
newWindow.document.body.appendChild(container);

现在在新窗口的DOM中,我们有一个空的head标签,我们遍历父head标签并将其子元素附加到新的head中。

const parentHead= window.document.querySelector("head").childNodes;
parentHead.forEach( item =>{
    newWindow.document.head.appendChild(item.cloneNode(true)); // deep copy
})

就是这样。新窗口具有与父级相同的头部子元素,现在所有样式都应该正常工作。

P.S. 有些样式可能会有点顽固,无法正常工作,我发现的解决方法是在componentDidMount或useEffect中添加一个正确时间的setTimeout,以便您可以使用父级头部更新您的新头部。类似于这样:

setTimeout(() => {
  updateHead();
}, 5000);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接