Как отрендерить компонент React со стилем Emotion в iframe на сайте Gatsby?

Я работаю над библиотекой компонентов и демонстрационным сайтом.

Компоненты оформлены с использованием стиля Emotion, а демонстрационный сайт построен с использованием Gatsby.

В целях предварительного просмотра я хотел бы отображать компоненты в iframe. Это гарантирует, что стили с веб-сайта не будут передаваться каскадом в компоненты, упростит работу с адаптивными макетами и т. Д.

Я также хотел бы сохранить горячую перезагрузку внутри iframe.

Здесь вы можете увидеть пример того, как line-height с веб-сайта каскадно передается на компонент Button, из-за которого он стал очень высоким.

введите здесь описание изображения

Как я могу отобразить Button со всеми его стилями внутри iframe?


person Misha Moroshko    schedule 29.11.2019    source источник


Ответы (2)


Я думаю, что проблема здесь в применении стиля, созданного emotion, к кнопке, размещенной внутри iframe.

Я нашел этот отличный пример от Митчелла (основная группа эмоций), который делает именно то, что вам нужно: github

Вот вилка вашего кодаcodeandbox с скопированным кодом с рудиментарным самодельным элементом <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>)
}

Это требует дополнительной работы, если вы хотите, чтобы ваши iFrames были предварительно обработаны во время gatsby build.

Для styled-components пользователей я нашел этот фрагмент от Стивена Хейни, который выглядит намного элегантнее. чем emotion:

[...] styled-components включает компонент StyleSheetManager, который может принимать целевую опору. Цель ожидает узел DOM и будет прикреплять свои динамически созданные таблицы стилей к этому узлу.

react-frame-component использует новую версию API контекста React для отображения FrameContextProvider. Он включает IFrame документ и окно в контексте.

Вы можете объединить эти два API следующим образом, чтобы использовать styled-components внутри своих IFrames:

    {
      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.

person Derek Nguyen    schedule 02.12.2019
comment
Спасибо @Derek за это. Я попытался запустить ваш codeandbox, но тело iframe не отображается. Я попытался добавить журналы консоли, и похоже, что onLoad никогда не вызывается (хотя прослушиватель событий подключен). Интересно, это какая-то причуда браузера или проблема с тайм-аутом. Какой браузер вы используете? - person Misha Moroshko; 03.12.2019
comment
Привет, Миша, демонстрация работает в последней версии Chrome и Firefox (попробовал только что снова), мне действительно интересно, если это привередливый тайм-аут. Я мог бы попытаться сделать локальную сборку позже, так как иногда нахожу коды и ящик ненадежными - person Derek Nguyen; 03.12.2019
comment
@DerekNguyen, вы пробовали свое решение с помощью сборки gatsby? - person GWorking; 09.10.2020

Я разветвил вашу песочницу, чтобы показать решение.
Шаги:

  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. Затем вы должны добавить ссылку на него в дочерний документ:

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

                  document.head.appendChild(link);

                  isFirstRender = false
                }, 100)
              }
person nickbullock    schedule 01.12.2019