<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          React Playground 實現原理揭秘

          共 13304字,需瀏覽 27分鐘

           ·

          2024-05-06 09:56

          大家應該都用過在線寫代碼的工具,比如 vue 的 playground:

          左邊寫代碼,右邊實時預覽。

          右邊還可以看到編譯后的代碼:


          這是一個純前端項目。

          類似的,也有 React Playground。

          那它是怎么實現的呢?我們自己能實現一個么?

          可以的,今天我們來分析下實現思路。

          首先是編譯:

          編譯用的 @babel/standalone,這個是 babel 的瀏覽器版本。

          可以用它實時把 tsx 代碼編譯為 js。

          試一下:

          npx create-vite

          進入項目安裝 @babel/standalone 和它的 ts 類型:

          npm install
          npm i --save @babel/standalone
          npm i --save-dev @types/babel__standalone

          去掉 index.css 和 StrictMode:

          改下 App.tsx

          import { useRef, useState } from 'react'
          import { transform } from '@babel/standalone';

          function App({

            const textareaRef = useRef<HTMLTextAreaElement>(null);

            function onClick({
              if(!textareaRef.current) {
                return ;
              }

              const res = transform(textareaRef.current.value, {
                presets: ['react''typescript'],
                filename'guang.tsx'
              });
              console.log(res.code);
            }

            const code = `import { useEffect, useState } from "react";

            function App() {
              const [num, setNum] = useState(() => {
                const num1 = 1 + 2;
                const num2 = 2 + 3;
                return num1 + num2
              });
            
              return (
                <div onClick={() => setNum((prevNum) => prevNum + 1)}>{num}</div>
              );
            }
            
            export default App;
            `

            return (
              <div>
                <textarea ref={textareaRef} style={{ width: '500px', height: '300px'}} defaultValue={code}></textarea>
                <button onClick={onClick}>編譯</button>
              </div>

            )
          }

          export default App

          在 textarea 輸入內容,設置默認值 defaultValue,用 useRef 獲取它的 value。

          然后點擊編譯按鈕的時候,拿到內容用 babel.transform 編譯,指定 typescript 和 react 的 preset。

          打印 res.code。

          可以看到,打印了編譯后的代碼:

          但現在編譯后的代碼也不能跑啊:

          主要是 import 語句這里:

          運行代碼的時候,會引入 import 的模塊,這時會找不到。

          當然,我們可以像 vite 的 dev server 那樣做一個根據 moduleId 返回編譯后的模塊內容的服務。

          但這里是純前端項目,顯然不適合。

          其實 import 的 url 可以用 blob url。

          在 public 目錄下添加 test.html:

          <!DOCTYPE html>
          <html lang="en">
          <head>
              <meta charset="UTF-8">
              <meta name="viewport" content="width=device-width, initial-scale=1.0">
              <title>Document</title>
          </head>
          <body>

          <script>
              const code1 =`
              function add(a, b) {
                  return a + b;
              }
              export { add };
              `
          ;

              const url = URL.createObjectURL(new Blob([code1], { type'application/javascript' }));
              const code2 = `import { add } from "${url}";

              console.log(add(2, 3));`
          ;

              const script = document.createElement('script');
              script.type="module";
              script.textContent = code2;
              document.body.appendChild(script);
          </script>
          </body>
          </html>

          瀏覽器訪問下:

          這里用的就是 blob url:

          我們可以把一段 JS 代碼,用 URL.createObjectURL 和 new Blob 的方式變?yōu)橐粋€ url:

          URL.createObjectURL(new Blob([code], { type'application/javascript' }))

          那接下來的問題就簡單了,左側寫的所有代碼都是有文件名的。

          我們只需要根據文件名替換下 import 的 url 就好了。

          比如 App.tsx 引入了 ./Aaa.tsx

          import Aaa from './Aaa.tsx';

          export default function App({
              return <Aaa></Aaa>
          }

          我們維護拿到 Aaa.tsx 的內容,然后通過 Bob 和 URL.createObjectURL 的方式把 Aaa.tsx 內容變?yōu)橐粋€ blob url,替換 import 的路徑就好了。

          這樣就可以直接跑。

          那怎么替換呢?

          babel 插件呀。

          babel 編譯流程分為 parse、transform、generate 三個階段。

          babel 插件就是在 transform 的階段增刪改 AST 的:

          通過 astexplorer.net 看下對應的 AST:

          只要在對 ImportDeclaration 的 AST 做處理,把 source.value 替換為對應文件的 blob url 就行了。

          比如這樣寫:

          import { transform } from '@babel/standalone';
          import type { PluginObj } from '@babel/core';

          function App({

              const code1 =`
              function add(a, b) {
                  return a + b;
              }
              export { add };
              `
          ;

              const url = URL.createObjectURL(new Blob([code1], { type'application/javascript' }));

              const transformImportSourcePlugin: PluginObj = {
                  visitor: {
                      ImportDeclaration(path) {
                          path.node.source.value = url;
                      }
                  },
              }


            const code = `import { add } from './add.ts'; console.log(add(2, 3));`

            function onClick({
              const res = transform(code, {
                presets: ['react''typescript'],
                filename'guang.ts',
                plugins: [transformImportSourcePlugin]
              });
              console.log(res.code);
            }

            return (
              <div>
                <button onClick={onClick}>編譯</button>
              </div>

            )
          }

          export default App

          這里插件的類型用到了 @babel/core 包的類型,安裝下:

          npm i --save-dev @types/babel__core

          我們用 babel 插件的方式對 import 的 source 做了替換。

          把 ImportDeclaration 的 soure 的值改為了 blob url。

          這樣,瀏覽器里就能直接跑這段代碼。

          那如果是引入 react 和 react-dom 的包呢?這些也不是在左側寫的代碼呀

          這種可以用 import maps 的機制:

          在 public 下新建 test2.html

          <!DOCTYPE html>
          <html lang="en">
          <head>
              <meta charset="UTF-8">
              <meta name="viewport" content="width=device-width, initial-scale=1.0">
              <title>Document</title>
          </head>
          <body>
              <script type="importmap">
                  {
                      "imports": {
                          "react""https://esm.sh/[email protected]",
                      }
                  }
              
          </script>
              <script type="module">
                  import React from "react";

                  console.log(React);
              
          </script>
          </body>
          </html>

          訪問下:

          可以看到,import react 生效了。

          為什么會生效呢?

          你訪問下可以看到,返回的內容也是 import url 的方式:

          這里的 esm.sh 就是專門提供 esm 模塊的 CDN 服務:

          這是它們做的 react playground:

          這樣,如何引入編輯器里寫的 ./Aaa.tsx 這種模塊,如何引入 react、react-dom 這種模塊我們就都清楚了。

          分別用 Blob + URL.createBlobURL 和 import maps + esm.sh 來做。

          那編輯器部分如何做呢?

          這個用 @monaco-editor/react

          安裝下:

          npm install @monaco-editor/react

          試一下:

          import Editor from '@monaco-editor/react';

          function App({

              const code =`import { useEffect, useState } from "react";

          function App() {
              const [num, setNum] = useState(() => {
                  const num1 = 1 + 2;
                  const num2 = 2 + 3;
                  return num1 + num2
              });

              return (
                  <div onClick={() => setNum((prevNum) => prevNum + 1)}>{num}</div>
              );
          }

          export default App;
          `
          ;

              return <Editor height="500px" defaultLanguage="javascript" defaultValue={code} />;
          }

          export default App;

          Editor 有很多參數,等用到的時候再展開看。

          接下來看下預覽部分:

          這部分就是 iframe,然后加一個通信機制,左邊編輯器的結果,編譯之后傳到 iframe 里渲染就好了。

          import React from 'react'

          import iframeRaw from './iframe.html?raw';

          const iframeUrl = URL.createObjectURL(new Blob([iframeRaw], { type'text/html' }));

          const Preview: React.FC = () => {

            return (
              <iframe
                  src={iframeUrl}
                  style={{
                      width: '100%',
                      height: '100%',
                      padding: 0,
                      border: 'none'
                  }}
              />

            )
          }

          export default Preview;

          iframe.html:

          <!doctype html>
          <html lang="en">
          <head>
            <meta charset="UTF-8"/>
            <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
            <title>Preview</title>
            <style>
              * {
                padding0;
                margin0;
              }
            
          </style>
          </head>
          <body>
          <script type="importmap">
            {
              "imports": {
                "react""https://esm.sh/[email protected]",
                "react-dom/client""https://esm.sh/[email protected]"
              }
            }
          </script>
          <script>

          </script>
          <script type="module">
            import React, {useState, useEffect} from 'react';
            import ReactDOM from 'react-dom/client';

            const App = () => {
              return React.createElement('div'null'aaa');
            };

            window.addEventListener('load', () => {
              const root = document.getElementById('root')
              ReactDOM.createRoot(root).render(React.createElement(App, null))
            })
          </script>

          <div id="root">
            <div style="position:absolute;top: 0;left:0;width:100%;height:100%;display: flex;justify-content: center;align-items: center;">
              Loading...
            </div>
          </div>

          </body>
          </html>

          這里路徑后面加個 ?raw 是通過字符串引入(webpack 和 vite 都有這種功能),用 URL.createObjectURL + Blob 生成 blob url 設置到 iframe 的 src 就好了:

          渲染的沒問題:

          這樣,我們只需要內容變了之后生成新的 blob url 就好了。

          至此,從編輯器到編譯到預覽的流程就理清了。

          案例代碼上傳了react 小冊倉庫。

          總結

          我們分析了下 react playground 的實現思路。

          編輯器部分用 @monaco-editor/react 實現,然后用 @babel/standalone 在瀏覽器里編譯。

          編譯過程中用自己寫的 babel 插件實現 import 的 source 的修改,變?yōu)?URL.createObjectURL + Blob 生成的 blob url,把模塊內容內聯(lián)進去。

          對于 react、react-dom 這種包,用 import maps 配合 esm.sh 網站來引入。

          然后用 iframe 預覽生成的內容,url 同樣是把內容內聯(lián)到 src 里,生成 blob url。

          這樣,react playground 整個流程的思路就理清了。

          瀏覽 109
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  青青草成人在线免费观看 | 国产福利91精品 | 国产精品V亚洲精品V日韩精品 | 91在线无码精品秘 入口色 | 姝姝窝人体色www国产 |