【问题标题】:How to render a React component styled with Emotion in an iframe in Gatsby site?如何在 Gatsby 网站的 iframe 中渲染带有 Emotion 样式的 React 组件?
【发布时间】:2020-03-24 16:42:10
【问题描述】:

我在一个组件库和一个演示站点上工作。

组件的样式使用Emotion,演示站点使用Gatsby构建。

出于预览目的,我想在 iframe 中呈现组件。这将确保来自网站的样式不会级联到组件,从而更容易处理响应式布局等。

我还想在 iframe 中保留热重载。

Here,您可以看到一个示例,说明来自网站的line-height 如何级联到Button 组件,导致它非常高。

如何在 iframe 中呈现 Button 及其所有样式?

【问题讨论】:

    标签: reactjs webpack iframe gatsby emotion


    【解决方案1】:

    forked您的沙盒来显示解决方案。
    步骤:

    1. 为 iframe 使用或编写组件。在沙盒中,我使用react-frame-component (https://github.com/ryanseddon/react-frame-component)。它将为我们呈现带有任何传递内容的 iframe。
    2. 找到一种方法来获得由情感创造的风格。情感只是创建style 节点,所以我们将复制它。在沙箱中,我编写了非常原始的代码来检查这个想法并且它正在工作,但在生产中你应该编写一些更高级的东西:
            <Frame>
              <FrameContextConsumer>
                {// Callback is invoked with iframe's window and document instances
                ({ document }) => {
                  if (isFirstRender) {
                    setTimeout(() => {
                      // find styles in main document
                      const styles = Array.from(
                        window.document.head.querySelectorAll("style[data-emotion]")
                      )
                      // and add it to the child
                      styles.forEach(s =>
                        document.head.appendChild(s.cloneNode(true))
                      )
                      isFirstRender = false
                    }, 100)
                  }
                  // Render Children
                  return <Button>Primary</Button>
                }}
              </FrameContextConsumer>
            </Frame>
    

    注意:我不熟悉emotion,但我认为它不会在生产中创建style 节点(通过webpack ofc),但会创建一个文件,类似于styles.css。 然后你应该将它的链接添加到子文档:

                  if (isFirstRender) {
                    setTimeout(() => {
                      const link = document.createElement("link");
                      link.href = "styles.scss";
                      link.rel = "stylesheet";
    
                      document.head.appendChild(link);
    
                      isFirstRender = false
                    }, 100)
                  }
    

    【讨论】:

    • 感谢您的解决方案。手动复制样式感觉很hacky。我想知道是否有更好的方法来生成包含样式的 HTML,例如使用html-webpack-plugin
    • @MishaMoroshko html-webpack-plugin 在这里没有帮助,因为它在构建阶段工作,但 iframe 将在运行时创建。我会尝试寻找其他解决方案。
    • 为什么不能在构建时创建iframe
    • AFAIK iframe 可以在现代浏览器中使用srcdoc 在构建时创建。我认为可以在构建过程中将组件渲染为字符串,但我不确定它会如何在客户端重新水化
    【解决方案2】:

    我认为这里的问题是将emotion 生成的样式应用于放置在 iframe 内的按钮。

    我发现 Mitchell(情感核心团队)的这个出色的例子完全符合您的需要:github

    这里是您的代码箱的一个分支,其中包含复制的代码,以及一个基本的自制 &lt;Iframe&gt; 元素:codesandbox

    以下是相关代码:

    // src/components/Iframe.js
    
    import React, { useRef, useEffect, useState } from 'react'
    import { createPortal } from 'react-dom'
    
    import { CacheProvider } from '@emotion/core'
    import createCache from '@emotion/cache'
    import weakMemoize from '@emotion/weak-memoize'
    
    // literally copied from Mitchell's codesandbox
    // https://github.com/emotion-js/emotion/issues/760#issuecomment-404353706
    let memoizedCreateCacheWithContainer = weakMemoize(container => {
      let newCache = createCache({ container });
      return newCache;
    });
    
    
    /* render Emotion style to iframe's head element */
    function EmotionProvider({ children, $head }) {
      return (
        <CacheProvider value={memoizedCreateCacheWithContainer($head)}>
          {children}
        </CacheProvider>
      )
    }
    
    /* hack-ish: force iframe to update */
    function useForceUpdate(){
      const [_, setValue] = useState()
      return () => setValue(0)
    }
    
    /* rudimentary Iframe component with Portal */
    export function Iframe({ children, ...props }) {
      const iFrameRef = useRef(null)
      const [$iFrameBody, setIframeBody] = useState(null)
      const [$iFrameHead, setIframeHead] = useState(null)
      const forceUpdate = useForceUpdate()
    
      useEffect(function(){
        if (!iFrameRef.current) return
    
        const $iframe = iFrameRef.current
        $iframe.addEventListener('load', onLoad)
    
        function onLoad() {
          // TODO can probably attach these to ref itself?
          setIframeBody($iframe.contentDocument.body)
          setIframeHead($iframe.contentDocument.head)
    
          // force update, otherwise portal children won't show up
          forceUpdate()
        }
    
        return function() {
          // eslint-disable-next-line no-restricted-globals
          $iframe.removeEventListener('load', onload)
        }
      })
    
      return (<iframe {...props} title="s" ref={iFrameRef}>
          {$iFrameBody && $iFrameHead && createPortal((
            <EmotionProvider $head={$iFrameHead}>{children}</EmotionProvider>
          ), $iFrameBody)}
        </iframe>)
    }
    

    如果您希望在 gatsby build 期间预渲染 iFrame,则需要做更多工作。

    对于styled-components 用户,我发现Stephen Haney 的这个sn-p 看起来比emotion 优雅得多:

    [...] styled-components 包含一个 StyleSheetManager 组件 可以带一个目标道具。目标需要一个 DOM 节点,它会 将其动态创建的样式表附加到该节点。

    react-frame-component 使用 React 的新版 Context API 来 公开FrameContextProvider。它包括IFrame 文档和 上下文中的窗口。

    您可以将这两个API组合如下使用styled-components 在您的 IFrame 中:

        {
          frameContext => (
            <StyleSheetManager target={frameContext.document.head}>
              <React.Fragment>
                {/* your children here */}
              </React.Fragment>
            </StyleSheetManager>
          )
        }   </FrameContextConsumer> </Frame> 
    
    

    这与 react v16.4.1、styled-components v3.3.3 和 react-frame-component v4.0.0 完美配合。

    【讨论】:

    • 感谢@Derek 挖掘这个。我试图运行你的代码框,但 iframe 正文没有显示出来。我尝试添加控制台日志,看起来 onLoad 从未被调用(即使附加了事件侦听器)。我想知道这是某种浏览器怪癖还是超时问题。你用的是什么浏览器?
    • 嗨 Misha,该演示适用于最新版本的 Chrome 和 Firefox(刚刚再次尝试),我想知道这是否是一个挑剔的超时事情。我可能会尝试稍后进行本地构建,因为我发现代码和框有时不可靠
    • @DerekNguyen,您是否尝试过使用 gatsby build 解决方案?
    猜你喜欢
    • 2015-03-09
    • 1970-01-01
    • 2019-02-24
    • 1970-01-01
    • 2017-07-01
    • 2020-09-08
    • 2020-12-08
    • 2021-07-16
    • 2021-03-19
    相关资源
    最近更新 更多