如何在 Gatsby 站点的 iframe 中呈现带有 Emotion 样式的 React 组件?

How to render a React component styled with Emotion in an iframe in Gatsby site?

我在开发组件库和演示站点。

组件的样式使用 Emotion and the demo site is built with Gatsby

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

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

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

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

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

  1. 使用或编写 iframe 组件。在沙盒中,我使用 react-frame-component (https://github.com/ryanseddon/react-frame-component)。它将为我们呈现带有任何传递内容的 iframe。
  2. 找到一种方法来获得由情感创造的风格。 Emotion 仅创建 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. 然后你应该将 link 添加到子文档中:

              if (isFirstRender) {
                setTimeout(() => {
                  const link = document.createElement("link");
                  link.href = "styles.scss";
                  link.rel = "stylesheet";

                  document.head.appendChild(link);

                  isFirstRender = false
                }, 100)
              }

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

我发现 Mitchell(情感核心团队)的这个优秀示例完全满足您的需求:github

这是您的 codesandbox 的一个分支,其中复制了代码,带有基本的自制 <Iframe> 元素: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 的这个片段看起来比 emotion:

优雅得多

[...] styled-components includes a StyleSheetManager component that can take a target prop. The target expects a DOM node, and it will attach its dynamically created stylesheets to that node.

react-frame-component uses React’s new version of its Context API to expose a FrameContextProvider. It includes the IFrame document and window in the context.

You can combine these two APIs as follows to use styled-components inside your IFrames:

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

This works perfectly with react v16.4.1, styled-components v3.3.3, and react-frame-component v4.0.0.