从 0 到 1 实现浏览器端沙盒运行环境
作者:easonruan,騰訊 CSIG 前端開發工程師
本文的瀏覽器端 Sandbox 沙盒運行環境,大家可以快速理解為類似 CodeSandbox 一樣,所有頁面代碼編譯都在前端完成(不依賴后端),并且具備實時熱更新功能。
而本文終極目標就是實現這樣的瀏覽器端 Sandbox 沙盒運行環境,可以輕松接入到大部分平臺(尤其低代碼平臺),提升應用的預覽速度和開發體驗,效果如下:
為什么需要瀏覽器端 Sandbox 沙盒運行環境?
原因一:Demo 體驗流程的轉變:繁瑣痛苦 → 快速便捷
如果你要體驗 Ant Design 組件庫里面 Tree 樹組件的一個例子,并想修改部分參數查看效果,你需要做以下步驟:
Step1. 安裝 Node.js (已安裝可忽略)?
Step2. 初始化 react 項目 npx create-react-app antd-tree-demo (必須)?
Step3. 添加 Ant Design 并安裝依賴 npm install (必須)?
Step4. 修改項目代碼為 Demo 例子代碼 (必須)?
Step5. 啟動項目 npm start (必須)
而當有了瀏覽器端的前端 Sandbox 沙盒運行環境,只需一個步驟:
Step1. 點擊打開一個鏈接
即可快速體驗到 Demo,并且修改代碼可實時看到效果。因此 Ant Design 組件庫的每個組件例子都附帶了 CodeSandbox 的鏈接:
原因二:低代碼平臺場景需要實時查看并調試當前應用的真實效果
用戶在低代碼平臺開發時,如果應用實時預覽的效果是與本地構建出來的效果是一致的,同時可以點擊跳轉到其他頁面,查看整個業務流程的效果,那么整個開發體驗都會有大幅度提升。
比如家庭健康碼流程,包含 3 個頁面:首頁入口 → 健康碼列表 → 健康碼詳情(詳見開頭視頻動圖)
第一個小目標:在瀏覽器上直接運行 React 源碼文件渲染出 Hello, Sandbox!
源碼如下:
import?React?from?'react'; import?ReactDOM?from?'react-dom';ReactDOM.render(<div>Hello,?Sandbox!</div>,document.getElementById('root') );問題一:如何讓源代碼在瀏覽器上直接執行?
直接在瀏覽器上面執行可以嗎?顯然不行
原因 1:瀏覽器不支持直接 import NPM 模塊 (目前支持加載服務端文件 '/xx/xx.jsx')
原因 2:瀏覽器無法識別 React 的 JSX 語法
雖然最新瀏覽器 (Chrome 67 版本開始) 已支持 ESM 模塊的加載方式,但需要有以下兩個前提條件:
條件 1:需要對源代碼進行改造,改為相對或絕對路徑,比如:import React from 'react' 改成 import React from '/@module/react'
條件 2:需要本地啟動服務器端 Server,返回對應代碼內容
當 import 其他文件時,比 import App from './App.jsx' ,因為 import 是系統關鍵詞,我們無法直接模擬或者代理 import,此時瀏覽器會直接發起一個請求,
如果不依賴服務端,就必須另起一個 service worker 進行攔截。
而 service worker 的注冊必須要加載單獨的 js 文件(靜態服務),無法將 sandbox 整套方案打包成一個 NPM 庫來使用,更新迭代較為繁瑣,不適用于我目前開發的低代碼平臺項目。
因此本文介紹的是更容易實現和管理的 CommonJS 格式規范,以 require 模塊的形式來模擬執行環境。
問題二:如何將 ESM 格式轉換成 CommonJS 格式?
沒錯,就是 Babel,Babel 有在線轉譯的 Try it out 版本,大家可以點擊 https://babeljs.io/repl 鏈接體驗
其代碼轉換效果如下:
利用 @babel/plugin-transform-modules-commonjs 插件,將 ESM 語法轉換成 CommonJS 格式規范
解決瀏覽器不支持直接 import NPM 模塊的問題
利用 @babel/plugin-transform-react-jsx Babel 插件,將 <div /> 轉換成 React.createElement('div') 函數
解決瀏覽器無法直接識別 React JSX 語法的問題
有了思路,我們立刻開始執行:
<!DOCTYPE?html> <html> <head><!--?①?依賴?--><script?src="https://unpkg.com/@babel/standalone@7.13.12/babel.min.js"></script> </head> <body><div?id="root"></div><script>const?code?=?` import?React?from?'react'; import?ReactDOM?from?'react-dom';ReactDOM.render(<React.StrictMode><div>Hello,?Sandbox!</div></React.StrictMode>,document.getElementById('root') );`//?②?轉譯//?此時代碼已轉為?CJS?格式,import?變成了?require?函數const?transpiledCode?=?Babel.transform(code,?{plugins:?[['transform-modules-commonjs'],['transform-react-jsx'],]}).code//?③?執行eval(transpiledCode)</script> </body> </html>執行 Babel 轉換后 CommonJS 規范的代碼,發現吃了個閉門羹:
原來是 require 函數沒有定義,因為 CommonJs 規范就是利用 require 來加載模塊的,既然現在沒有定義,那我們就定義一個
問題三:如何實現 require 函數?
因為 require 是要引入 react, react-dom 兩個 NPM 依賴庫的,所以實現 require 函數之前,先插入已打包為 UMD 規范的文件路徑,以獲取 React, ReactDom 全局變量。
<!DOCTYPE?html> <html> <head><!--?①?依賴?--><script?src="https://unpkg.com/@babel/standalone@7.13.12/babel.min.js"></script><script?src="https://unpkg.com/react@16.14.0/umd/react.development.js"></script><script?src="https://unpkg.com/react-dom@16.14.0/umd/react-dom.development.js"></script><!--?此時?react,?react-dom?庫已掛載到?window['React'],?window['ReactDOM']?--> </head> <body><div?id="root"></div><script>const?externals?=?{react:?'React','react-dom':?'ReactDOM'}function?require(moduleName)?{return?window[externals[moduleName]]}</script> </body> </html>實現 require 函數也非常簡單,需要拿哪個 NPM 依賴庫,就直接把已加載到全局的庫,返回回去即可。
其中的 externals 是什么?
相信熟悉 webpack 的同學應該比較了解,簡單來說就是配置哪些庫是在運行時(runtime),再去外部(全局)獲取這些擴展依賴。詳情請點擊
前期準備工作已經做完,我們將以下文件保存為 index.html ,然后本地打開看看效果
<!DOCTYPE?html> <html> <head><!--?①?依賴?--><script?src="https://unpkg.com/@babel/standalone@7.13.12/babel.min.js"></script><script?src="https://unpkg.com/react@16.14.0/umd/react.development.js"></script><script?src="https://unpkg.com/react-dom@16.14.0/umd/react-dom.development.js"></script> </head> <body><div?id="root"></div><script>const?externals?=?{react:?'React','react-dom':?'ReactDOM'}function?require(moduleName)?{return?window[externals[moduleName]]}const?code?=?` import?React?from?'react'; import?ReactDOM?from?'react-dom';ReactDOM.render(<React.StrictMode><div>Hello,?Sandbox!</div></React.StrictMode>,document.getElementById('root') );`//?②?轉譯const?transpiledCode?=?Babel.transform(code,?{plugins:?[['transform-modules-commonjs'],['transform-react-jsx'],]}).code//?③?執行eval(transpiledCode)</script> </body> </html>可以看到,第一個小目標已經完美完成!
總結:Sandbox 核心方法論
經過上面簡單例子的驗證,不能發現,最小的例子都要不開以下三步,因此本文總結了瀏覽器端 Sandbox 沙盒的核心方法論:
Step1. 加載依賴
加載 Babel, React, ReactDOM
Step2. 轉譯模塊
利用 Babel 將 ESM 轉 CommonJS,轉 JSX 語法
Step3. 執行代碼
構造 CommonJS 環境,如 require 加載模塊函數
所以看過本文的同學,其他知識點記不住沒關系,將本文的 Sandbox 方法論三部曲記住就行,記住就已經算掌握一半瀏覽器端沙盒原理了。
重要的事情說三次:
Step1. 加載依賴,Step2. 轉譯模塊,Step3. 執行代碼?
Step1. 加載依賴,Step2. 轉譯模塊,Step3. 執行代碼?
Step1. 加載依賴,Step2. 轉譯模塊,Step3. 執行代碼
下面我們用 Vue 創建一個業務項目,讓 Vue 中用 Sandbox 沙盒(Iframe 形式)來加載另一個 React 應用,同時驗證上述 Sandbox 方法論。
第二個小目標:從 0 到 1 實現一個瀏覽器端的 Sandbox 沙盒運行環境
由于我目前研發的是 WeDa 低代碼平臺(專有版),因此暫時起名 WeSandbox 。
WeDa 低代碼平臺(專有版) 由于內網環境問題暫不放鏈接,后續合適時期將開放給公司內部體驗,目前大家可以先體驗 WeDa 公有云版本
第二個小目標最終效果其有以下特點:
可在 Vue 應用 Sandbox 里運行 React 代碼
React useState 等功能均正常
修改 JSON 數據可熱更新 React 組件(不丟失狀態)
修改 CSS 數據可熱更新樣式
上圖運行的是 Vue 應用,里面有個 iframe 承載著 WeSandbox 核心功能,其可以轉譯并運行 React 的代碼。
Vue 應用代碼
<template><div class="app-wrapper"><div class="editor-wrapper"><template v-for="item in Object.values(codeMap)"><div class="file-name">{{item.path}}</div><textarea class="code-editor" @change="noticeSandboxUpdate" v-model="codeMap[item.path].code" /></template></div><div class="sandbox-wrapper"><iframe id="sandbox" @load="noticeSandboxUpdate" src="/sandbox.html" frameborder="0" /></div></div> </template><script> export default {data() {return {codeMap: {'/src/index.js': {code: ` import React from 'react'; import ReactDOM from 'react-dom'; import App from './App';ReactDOM.render(<React.StrictMode><App /></React.StrictMode>,document.getElementById('root') );`.trim(),path: '/src/index.js'},'/src/App.jsx': {code: ` import React, { useState } from 'react' import { title } from './data.json' import './App.css'export default function App() {const [count, setCount] = useState(0)return (<div className="App"><header className="App-header"><p>Hello {title}!</p><p><button onClick={() => setCount((count) => count + 1)}>count is: {count}</button></p><p>Edit <code>App.jsx</code> and save to test HMR updates.</p><p><aclassName="App-link"href="https://reactjs.org"target="_blank"rel="noopener noreferrer">Learn React</a></p></header></div>) } `.trim(),path: '/src/App.jsx',style: {flex: 1}},'/src/data.json': {code: `{ "title": "Mini Sandbox - Json Data" }`,path: '/src/data.json'},'/src/App.css': {code: ` body {padding: 0;margin: 0; } .App {text-align: center; }.App-header {background-color: #282c34;min-height: 100vh;display: flex;flex-direction: column;align-items: center;justify-content: center;font-size: calc(10px + 2vmin);color: white; }.App-link {color: #61dafb; }button {font-size: calc(10px + 2vmin); } `.trim(),path: `/src/App.css`}}}},methods: {noticeSandboxUpdate() {document.querySelector('#sandbox').contentWindow.postMessage({codeMap: JSON.parse(JSON.stringify(this.codeMap)),entry: '/src/index.js',dependencies: {},externals: {react: 'React','react-dom': 'ReactDOM',}})}} } </script>下面我們帶著問題來一一查看部分功能的核心源碼:
問題一:如何轉譯代碼?
本文第一個小目標已經分析過,可以利用 Babel 進行轉譯,第二個小目標我們加個文件類型判斷:
//?Step2.?轉譯代碼 function?Transpile(packageInfo)?{const?codeMap?=?packageInfo.codeMapObject.keys(codeMap).map(path?=>?{const?code?=?codeMap[path].code//?Babel?Loaderif?(/\.jsx?$/.test(path))?{codeMap[path].transpiledCode?=?Babel.transform(code,?{plugins:?[['transform-modules-commonjs'],['transform-react-jsx'],]}).code}})return?codeMap }問題二:如何模擬 CommonJS 執行環境?
由于本文上部分只引入了 React,沒有引入 js(x) 源代碼文件,而源代碼文件一般會利用 module.exports 導出該模塊的值的,因此我們需要構造出 module 和 exports 來存儲代碼模塊 eval 執行后的結果,其核心代碼如下:
//?transpiledCode?轉譯后的源代碼 //?require?自定義的獲取模塊函數,看下文 //?module?是與當前源代碼綁定的執行結果(一開始為空對象,eval執行后賦值) function?evaluateCode(transpiledCode,?require,?module)?{//?#1.?構建?require,?module,?exports?當前函數的上下文全局數據const?allGlobals?=?{require,module,exports:?module.exports,};const?allGlobalKeys?=?Object.keys(allGlobals).join(',?')const?allGlobalValues?=?Object.values(allGlobals);try?{//?#2.?源代碼外面加一層函數,構建函數的入參為?require,?module,?exportsconst?newCode?=?`(function?evaluate(`?+?allGlobalKeys?+?`)?{`?+?transpiledCode?+?`\n})`;//?#3.?利用?eval?執行此函數,并傳入?require,?module,?exportseval(newCode).apply(this,?allGlobalValues);return?module.exports;}?catch?(e)?{//} }const?defaultExternals?=?{react:?'React','react-dom':?'ReactDOM', } function?evaluateCodeModule(codeModule)?{codeModule.module?=?codeModule.module?||?getNewModule()function?require(moduleName)?{const?extLib?=?window[defaultExternals[moduleName]]if?(extLib)?{return?extLib}}return?evaluateCode(codeModule.transpiledCode,?require,?codeModule.module) }function?getNewModule()?{const?exports?=?{}return?{exports,} }至此,我們已經 CommonJS 必備三套件
require 獲取依賴模塊函數
module 存儲模塊執行結果
exports 存儲模塊執行結果
但演示例子的代碼存在 import x from './x' 的寫法,
import?React?from?'react'; import?ReactDOM?from?'react-dom'; import?App?from?'./App';ReactDOM.render(<App?/>,document.getElementById('root') )顯然目前這么簡單的 require 函數還是不夠的。
問題三:如何處理 import x from './x' 引入其他代碼模塊文件?
核心思路:由于我們知道是哪個模塊(知道模塊路徑 path)引用該代碼文件的,因此我們可以結合引用者模塊的代碼絕對路徑 + 引用相對路徑 = 獲取真正的代碼絕對路徑,比如:'./App.js' => '/src/App.js'
function?require(moduleName)?{//?#1?針對項目文件if?(/^[./]/.test(moduleName))?{//?獲取真正的代碼路徑,比如:'./App.js'?=>?'/src/App.js'const?modulePath?=?resolveModulePath(moduleName,?codeModule,?moduleGraph)const?requiredModule?=?moduleGraph.getModule(modulePath)if?(requiredModule.module)?{return?requiredModule.module.exports}requiredModule.module?=?getNewModule()return?evaluateCodeModule(requiredModule,?moduleGraph)}//?#2?針對外部(全局)依賴//?... }//?獲取真正的代碼路徑,比如:'./App.js'?=>?'/src/App.js' function?resolveModulePath(moduleName,?codeModule,?moduleGraph)?{//?#1?針對?/let?modulePath?=?moduleName//?#2?針對?.if?(moduleName.startsWith('.'))?{const?currentDir?=?path.dirname(codeModule.path?||?codeModule.id)modulePath?=?path.resolve(currentDir,?moduleName)}if?(moduleGraph.getModule(modulePath))?{return?modulePath}const?FILE_EXTNAME?=?['.js',?'.jsx',?'.css',?'.json',?'/index.js']FILE_EXTNAME.some(ext?=>?{const?withExtPath?=?`${modulePath}${ext}`if?(moduleGraph.getModule(withExtPath))?{modulePath?=?withExtPathreturn?true}})return?modulePath }問題四:如何處理 JSON 代碼模塊?
此處先給 1 分鐘讀者思考一下,
好,估計你已經想出來了,沒錯,就是在 Sandbox 核心方法論 的 Step2. 轉譯代碼 步驟添加一個簡單的 JSON Loader 就行
//?Step2.?轉譯代碼 function?Transpile(moduleGraph)?{const?moduleMap?=?moduleGraph.moduleMapmoduleMap.forEach(codeModule?=>?{const?code?=?codeModule.codeconst?path?=?codeModule.path//?Babel?Loader//?...//?JSON?Loaderif?(/\.json$/.test(path))?{codeModule.transpiledCode?=?`module.exports?=?${code}`}}) }問題五:如何處理 CSS 代碼模塊?
這個問題應該難不倒可以舉一反三的你,我們直接看答案:
//?Step2.?轉譯代碼 function?Transpile(moduleGraph)?{const?moduleMap?=?moduleGraph.moduleMapmoduleMap.forEach(codeModule?=>?{const?code?=?codeModule.codeconst?path?=?codeModule.path//?Babel?Loader//?...//?JSON?Loader//?...//?CSS?Loaderif?(/\.css$/.test(path))?{codeModule.transpiledCode?=?insertCss(path,?code)}}) }function?insertCss(id,?css)?{return?` function?createStyleNode(id,?content)?{var?styleNode?=document.getElementById(id)?||?document.createElement('style');styleNode.setAttribute('id',?id);styleNode.type?=?'text/css';if?(styleNode.styleSheet)?{styleNode.styleSheet.cssText?=?content;}?else?{styleNode.innerHTML?=?'';styleNode.appendChild(document.createTextNode(content));}document.head.appendChild(styleNode); }createStyleNode(${JSON.stringify(id)},${JSON.stringify(css)} ); ` }問題六:如何處理 Less 代碼模塊?
原理和上述一樣,將 Less 文件轉換成 css 文件之后再經過 CSS Loader 即可。
這是一道課外題,本文就不給出答案了,讀者可以自行嘗試。
問題七:如何實現熱更新 React ?
這道是難題,但 React 官方有 react-refresh 標準答案,我們直接拿來抄。感興趣的同學可以自行點擊查看詳情。
本文翻譯并梳理下步驟以及重難點:
確保 React 版本是在 16.9.0+ 以上
并且 React 必須是 development 開發模式的版本(本人在此踩過坑)
把 react-refresh/babel 加到你的 Babel plugins 插件里面
必須在加載 react-dom 庫之前加載以下代碼:
const?runtime?=?require('react-refresh/runtime'); runtime.injectIntoGlobalHook(window); window.$RefreshReg$?=?()?=>?{}; window.$RefreshSig$?=?()?=>?type?=>?type;然后在你 React 實際業務代碼前后插入以下代碼:
//?BEFORE?EVERY?MODULE?EXECUTESvar?prevRefreshReg?=?window.$RefreshReg$; var?prevRefreshSig?=?window.$RefreshSig$; var?RefreshRuntime?=?require('react-refresh/runtime');window.$RefreshReg$?=?(type,?id)?=>?{//?Note?module.id?is?webpack-specific,?this?may?vary?in?other?bundlersconst?fullId?=?module.id?+?'?'?+?id;RefreshRuntime.register(type,?fullId); } window.$RefreshSig$?=?RefreshRuntime.createSignatureFunctionForTransform;try?{//?!!!//?...?你的?React?業務代碼?...//?!!!}?finally?{window.$RefreshReg$?=?prevRefreshReg;window.$RefreshSig$?=?prevRefreshSig; }而 Sandbox 中可以按以下步驟處理:
在 html 頂部引入 react-refresh-runtime, react-refresh-babel 兩個庫
<script?src="./lib/react-refresh-runtime.js"></script> <script?src="./lib/react-refresh-babel.js"></script> <script>ReactRefreshRuntime.injectIntoGlobalHook(window);window.$RefreshReg$?=?()?=>?{};window.$RefreshSig$?=?()?=>?type?=>?type; </script> <script?src="https://unpkg.com/react@16.14.0/umd/react.development.js"></script> <script?src="https://unpkg.com/react-dom@16.14.0/umd/react-dom.development.js"></script>在引入 react-dom 之前執行上述代碼
確保 React 是 development 版本并且是 16.9.0+ 以上
于引入 react-refresh-babel 庫,已經存在全局對象 ReactFreshBabelPlugin,因此可以直接將其加到 Babel 插件列表里面
然后在 Babel 返回結果前后加上官方指定代碼
//?Step2.?轉譯代碼 function?Transpile(moduleGraph)?{const?moduleMap?=?moduleGraph.moduleMapmoduleMap.forEach(codeModule?=>?{const?code?=?codeModule.codeconst?path?=?codeModule.pathif?(/\.jsx?$/.test(path))?{codeModule.transpiledCode?=?getReactRefreshWrapperCode(babelTransform(code),?path)}}) }function?babelTransform(code)?{return?Babel.transform(code,?{plugins:?[['transform-modules-commonjs'],['transform-react-jsx'],[ReactFreshBabelPlugin]]}).code }function?getReactRefreshWrapperCode(sourceCode,?moduleId)?{return?`//?react?refresh?code?before${sourceCode}//?react?refresh?code?after ` }至此,React 熱更新的核心步驟已經完成,接下來就是收集代碼已改變的模塊列表,并重新執行該代碼模塊,即可達到熱更新的效果。
問題八:如何實現模塊互相引用的熱更新?
簡單來說就是,App.jsx 引用了 data.json 里面的數據,當 data.json 更新時,如何實現讓 App.jsx 進行熱更新?
答案是:收集模塊依賴 (initiators 發起者) 。
我們可以在 require 函數引用模塊的時候,收集當前模塊是被誰引用過,稱為initiators 發起者 ,然后等熱更新執行模塊時,先執行自身變化的代碼模塊,再執行該模塊的 initiators 發起模塊,即可達到互相引用熱更新效果。
function?evaluateCodeModule(codeModule,?moduleGraph)?{codeModule.module?=?codeModule.module?||?getNewModule()function?require(moduleName)?{if?(/^[./]/.test(moduleName))?{const?modulePath?=?resolveModulePath(moduleName,?codeModule,?moduleGraph)const?requiredModule?=?moduleGraph.getModule(modulePath)if?(requiredModule.module)?{return?requiredModule.module.exports}requiredModule.module?=?getNewModule()//?收集模塊之間的依賴關系,以便熱更新requiredModule.initiators.add(codeModule)return?evaluateCodeModule(requiredModule,?moduleGraph)}//?...}codeModule.isChanged?=?falsereturn?evaluateCode(codeModule.transpiledCode,?require,?codeModule.module) }function?StepThree_Evaluate(message,?moduleGraph)?{const?{?entry?}?=?message//?#1?從入口開始執行const?entryModule?=?moduleGraph.getModule(entry)if?(entryModule.isChanged)?{evaluateCodeModule(entryModule,?moduleGraph)return}//?#2?熱更新const?simpleHotModules?=?[]moduleGraph.moduleMap.forEach(codeModule?=>?{if?(codeModule.isChanged)?{evaluateCodeModule(codeModule,?moduleGraph)codeModule.initiators.forEach(module?=>?{simpleHotModules.push(module)})}})simpleHotModules.forEach(module?=>?{evaluateCodeModule(module,?moduleGraph)}) }問題九:如何獲取 NPM 依賴包,dayjs 為例?
這個是難題,同學可以先主動思考下 ????,
如果要實現一個可用于生產環境的 WeSandbox,還有很多細節和問題需要考慮,
比如上面 NPM 依賴包、轉譯性能問題、如何便捷更新調試 等等
WeSandbox 即將用于 WeDa 低代碼平臺(專用版)生成環境
盡管 WeDa 低代碼平臺對于 Sandbox 的大部分已經攻克并實現,但本文篇幅有限,將在下一篇文章講解,敬請期待~
下面 WeSandbox Mini 版僅僅是為了展示沙盒運行環境的核心思路,后續會給大家介紹正式版本。
我們再次回顧第二個小目標,其功能都已經實現:
[x] 可在 Vue 應用 Sandbox 里運行 React 代碼
[x] React useState 等功能均正常
[x] 修改 JSON 數據可熱更新 React 組件(不丟失狀態)
[x] 修改 CSS 數據可熱更新樣式
如果本文對你有幫助,請幫頂,收藏,打賞,一鍵三連 ~ ????
最后,附上 WeSandbox Mini 版代碼,共 280 行
<html?lang="en"> <head><meta?charset="UTF-8"/><title>Mini?Sandbox</title><script?src="https://unpkg.com/@babel/standalone@7.13.12/babel.min.js"></script><script?src="./lib/react-refresh-runtime.js"></script><script?src="./lib/react-refresh-babel.js"></script><script>ReactRefreshRuntime.injectIntoGlobalHook(window);window.$RefreshReg$?=?()?=>?{};window.$RefreshSig$?=?()?=>?type?=>?type;</script><!--??①?加載依賴??--><script?src="https://unpkg.com/react@16.14.0/umd/react.development.js"></script><script?src="https://unpkg.com/react-dom@16.14.0/umd/react-dom.development.js"></script><script?src="./lib/path-browserify.js"></script><script>class?ModuleNode?{constructor(path)?{this.path?=?paththis.type?=?path.endsWith('css')???'css'?:?'js'this.initiators?=?new?Set()this.isChanged?=?truethis.module?=?nullthis.transformResult?=?{code:?''}}}class?ModuleGraph?{moduleMap?=?new?Map()getModule(id)?{return?this.moduleMap.get(id)}}const?globalModuleGraph?=?new?ModuleGraph()//?監聽父級應用發送過來的消息window.addEventListener('message',?async?(event)?=>?{const?message?=?event.dataconsole.log('sandbox?receive?mes',?message)updateCodeModule(message,?globalModuleGraph)StepTwo_Transpile(globalModuleGraph)StepThree_Evaluate(message,?globalModuleGraph)})function?updateCodeModule(message,?moduleGraph)?{const?{?codeMap?}?=?messagelet?finalFileMap?=?codeMapObject.keys(finalFileMap).forEach(path?=>?{const?codeFile?=?finalFileMap[path]let?module?=?moduleGraph.getModule(path)if?(!module)?{const?newModule?=?new?ModuleNode(path)newModule.code?=?codeFile.codenewModule.isChanged?=?truenewModule.transpiledCode?=?codeFile.transpiledCode?||?nullmoduleGraph.moduleMap.set(path,?newModule)return}if?(module.code?!==?codeFile.code)?{module.code?=?codeFile.codemodule.transpiledCode?=?nullmodule.module?=?nullmodule.isChanged?=?true}})}//?②?轉譯模塊function?StepTwo_Transpile(moduleGraph)?{const?moduleMap?=?moduleGraph.moduleMapmoduleMap.forEach(codeModule?=>?{const?code?=?codeModule.codeif?(/\.jsx?$/.test(codeModule.path))?{codeModule.transpiledCode?=?getReactRefreshWrapperCode(babelTransform(code),?codeModule.path)}if?(/\.json$/.test(codeModule.path))?{codeModule.transpiledCode?=?`module.exports?=?${codeModule.code}`}if?(/\.css$/.test(codeModule.path))?{codeModule.transpiledCode?=?insertCss(codeModule.path,?codeModule.code)}})}function?insertCss(id,?css)?{return?` function?createStyleNode(id,?content)?{var?styleNode?=document.getElementById(id)?||?document.createElement('style');styleNode.setAttribute('id',?id);styleNode.type?=?'text/css';if?(styleNode.styleSheet)?{styleNode.styleSheet.cssText?=?content;}?else?{styleNode.innerHTML?=?'';styleNode.appendChild(document.createTextNode(content));}document.head.appendChild(styleNode); }createStyleNode(${JSON.stringify(id)},${JSON.stringify(css)} ); `}function?babelTransform(code)?{return?Babel.transform(code,?{plugins:?[['transform-modules-commonjs'],['transform-react-jsx'],[ReactFreshBabelPlugin]]}).code}function?getReactRefreshWrapperCode(sourceCode,?moduleId)?{return?` var?prevRefreshReg?=?window.$RefreshReg$,prevRefreshSig?=?window.$RefreshSig$,RefreshRuntime?=?require("react-refresh/runtime");window.$RefreshReg$?=?(type,?id)?=>?{const?s?=?${JSON.stringify(moduleId)}?+?"?"?+?id;RefreshRuntime.register(type,?s) }; window.$RefreshSig$?=?RefreshRuntime.createSignatureFunctionForTransform;try?{${sourceCode} }?finally?{window.$RefreshReg$?=?prevRefreshReg,?window.$RefreshSig$?=?prevRefreshSig }function?debounce(func,?wait,?immediate)?{var?timeout;return?function()?{var?context?=?this,?args?=?arguments;var?later?=?function()?{timeout?=?null;if?(!immediate)?func.apply(context,?args);};var?callNow?=?immediate?&&?!timeout;clearTimeout(timeout);timeout?=?setTimeout(later,?wait);if?(callNow)?func.apply(context,?args);}; }; const?enqueueUpdate?=?debounce(RefreshRuntime.performReactRefresh,?30); enqueueUpdate()`;}//?③?執行代碼function?StepThree_Evaluate(message,?moduleGraph)?{const?{?entry?}?=?messageconst?entryModule?=?moduleGraph.getModule(entry)if?(entryModule.isChanged)?{evaluateCodeModule(entryModule,?moduleGraph)return}const?simpleHotModules?=?[]moduleGraph.moduleMap.forEach(codeModule?=>?{if?(codeModule.isChanged)?{evaluateCodeModule(codeModule,?moduleGraph)codeModule.initiators.forEach(module?=>?{simpleHotModules.push(module)})}})simpleHotModules.forEach(module?=>?{evaluateCodeModule(module,?moduleGraph)})}const?defaultExternals?=?{react:?'React','react-dom':?'ReactDOM','react-refresh/runtime':?'ReactRefreshRuntime'}function?evaluateCodeModule(codeModule,?moduleGraph)?{codeModule.module?=?codeModule.module?||?getNewModule()function?require(moduleName)?{//?#1?針對項目文件if?(/^[./]/.test(moduleName))?{//?獲取真正的代碼路徑,比如:'./App.js'?=>?'/src/App.js'const?modulePath?=?resolveModulePath(moduleName,?codeModule,?moduleGraph)const?requiredModule?=?moduleGraph.getModule(modulePath)if?(requiredModule.module)?{return?requiredModule.module.exports}requiredModule.module?=?getNewModule()requiredModule.initiators.add(codeModule)return?evaluateCodeModule(requiredModule,?moduleGraph)}const?extLib?=?window[moduleName]?||?window[defaultExternals[moduleName]]if?(extLib)?{return?extLib}}codeModule.isChanged?=?falsereturn?evaluateCode(codeModule.transpiledCode,?require,?codeModule.module)}function?resolveModulePath(moduleName,?codeModule,?moduleGraph)?{//?#1?針對?/let?modulePath?=?moduleName//?#2?針對?.if?(moduleName.startsWith('.'))?{const?currentDir?=?path.dirname(codeModule.path?||?codeModule.id)modulePath?=?path.resolve(currentDir,?moduleName)}if?(moduleGraph.getModule(modulePath))?{return?modulePath}const?FILE_EXTNAME?=?['.js',?'.jsx',?'.css',?'.json',?'/index.js']FILE_EXTNAME.some(ext?=>?{const?withExtPath?=?`${modulePath}${ext}`if?(moduleGraph.getModule(withExtPath))?{modulePath?=?withExtPathreturn?true}})return?modulePath}function?getNewModule()?{const?exports?=?{}return?{exports,}}function?evaluateCode(code,?require,?module)?{const?exports?=?module.exportsconst?allGlobals?=?{require,module,exports,};const?allGlobalKeys?=?Object.keys(allGlobals).join(',?')const?globalsValues?=?Object.values(allGlobals);try?{const?newCode?=?`(function?evaluate(`?+?allGlobalKeys?+?`)?{`?+?code?+?`\n})`;//?@ts-ignoreeval(newCode).apply(allGlobals.window?||?this,?globalsValues);return?module.exports;}?catch?(e)?{let?error?=?e;if?(typeof?e?===?'string')?{error?=?new?Error(e);}error.isEvalError?=?true;throw?error;}}</script> </head> <body> <div?id="root"></div> </body> </html>視頻號最新視頻
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生總結
以上是生活随笔為你收集整理的从 0 到 1 实现浏览器端沙盒运行环境的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一文带你理解云原生
- 下一篇: NCMMSC2021喊你开赛!汉语长短视