简易React Playground实现 - 4.预览更新

1.背景

每次代码转译后iframe元素都需要重新渲染,这部分可以优化。此外,我们对于转译报错的情况,没有做容错处理,需要优化。

2.需求分析

我们可以给宿主(当前应用)和iframe通过postMessage去实现通信。每一次代码转译后,将代码通过消息发送给iframe,iframe有消息的接收事件,当接收到之后,eval执行代码。

3.代码实现

3.1iframe部分

iframe部分我们移除对script的渲染,通过iframe接收message事件,传输的代码eval执行。

const htmlStr =  `
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Playground</title>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
</head>
<body>
      <div id="root"></div>
      <script>
      // 监听来自父窗口的消息
      window.addEventListener('message', (event) => {
          try {
            eval(event.data)
          } catch(error) {
           // 发送信息给父窗口
           window.parent.postMessage('error', '*'); 

          }
      });
  </script>
</body>
</html>
`;

2.宿主代码

React应用这边,当代码转译后,通过postMessage将转译后代码发送给iframe

这样做不必每次都转Blob再转url

import React, { useEffect, useRef } from "react";
import { transform } from "@babel/standalone";


const initValue = `
const App = () => {
  return (
      <div>Hello</div>
  )
}
ReactDOM.render(<App />,document.getElementById("root"))
`;

const blob = new Blob([htmlStr], {
  type: "text/html",
});
const url = URL.createObjectURL(blob);

export default function App() {
  const textAreaRef = useRef<HTMLTextAreaElement>(null);
  const iframeRef = useRef<HTMLIFrameElement>(null);

  const handleClick = () => {
    const value = textAreaRef.current?.value;
    if (value) {
      try {
        const { code } = transform(value, {
          presets: ["react"],
        });
        if (code) {
          handleUpdatePreview(code)
        } 
      } catch(error) {
        handleUpdatePreview("")
      }
    }
  };

  // 代码更新,发送信息
  const handleUpdatePreview = (code) => {
    if (iframeRef.current) {
      iframeRef.current.contentWindow?.postMessage(code, "*");
    } 
  }


  useEffect(() => {
    window.addEventListener("message",(e)=>{
      console.log("message>>",e)
    })
    return () => {
      window.removeEventListener("message",(e)=>{
        console.log("message>>",e)
      })
    }
  },[])

  return (
    <>
      <textarea ref={textAreaRef} defaultValue={initValue} />
      <button onClick={handleClick}>transform</button>
      <iframe src={url} ref={iframeRef}/>
    </>
  );
}