Tango 低代碼引擎沙箱實現解析
Tango 基本介紹
Tango 是一個用于快速構建低代碼平臺的低代碼設計器框架,并以源代碼為中心,執行和渲染前端視圖,并為用戶提供低代碼可視化搭建能力,用戶的搭建操作會轉為對源代碼的修改。借助于 Tango 構建的低代碼工具或平臺,可以實現 源碼進,源碼出的效果,無縫與企業內部現有的研發體系進行集成。
開源進展
目前 Tango 設計器引擎部分已經開源,正在積極推進中,可以通過如下的信息了解到我們的最新進展:
- 開源代碼庫:https://github.com/NetEase/tango
- 文檔地址:https://netease.github.io/tango-site/
- 社區討論組:https://github.com/NetEase/tango/discussions
此外,Tango 的文檔現已全面更新,歡迎瀏覽。
歡迎大家加入到我們的社區中來,一起參與到 Tango 低代碼引擎的開源建設中。有任何問題都可以通過 Github Issues 反饋給我們,我們會及時跟進處理。
往期系列文章
- 網易云音樂 RN 低代碼體系建設思考與實踐
- 手把手帶你走進Babel的編譯世界
- 網易云音樂低代碼體系建設思考與實踐
- 云音樂低代碼:基于 CodeSandbox 的沙箱性能優化
- 云音樂低代碼 ChatGPT 實踐方案與思考
- 網易云音樂 Tango 低代碼引擎實現揭秘
- 網易云音樂 Tango 低代碼引擎正式開源
- 低代碼在云音樂數據業務中的落地實踐與思考
為什么 Tango 需要沙箱
傳統的基于 DSL 的低代碼方案通常需要實現一套對應的 DSL 語法與渲染器,在渲染器內渲染給定的組件、綁定事件等。與此不同,Tango 是基于 AST 驅動的面向源碼的低代碼方案。相較于 DSL 方案,Tango 的寫法更加靈活,但也帶來了支持源代碼實時運行的挑戰。此外,為了與團隊內已有的物料集成,Tango 支持添加業務組件,因此設計器還需要考慮三方依賴的加載與運行。因此,Tango 需要一個獨立的沙箱來運行源碼,提供可以媲美本地開發的代碼運行時。
在初期,Tango 曾調研了幾種方案,如基于 Sea.js 這類 AMD 加載方案。然而,這類方案的問題在于依賴比較固定,需要將依賴預先構建出符合規范的產物(如 UMD 資源),因此不能靈活地添加依賴。至于 SystemJS 和 ViteSandbox 這類 ESM 方案,由于 Tango 期望支持直接使用已有的組件物料,而它們的產物主要以 CommonJS 為主,缺少 ESM 產物。此外,我們后續對沙箱的改造優化大幅減少了沙箱初始化的時間,因此沒有采用該方案。
Tango 目前采用的沙箱方案是基于 CodeSandbox 提供的沙箱能力實現的。它的優勢在于提供了更完整、接近本地開發的運行時環境,支持直接拉取 npm 包并運行。它借助 Babel 將 ESM 和瀏覽器不支持的新語法轉譯為 CommonJS,模擬了 CommonJS 的運行環境,實現了源碼在瀏覽器上直接運行。這樣即便依賴沒有提供可供瀏覽器使用的預構建產物,也能在沙箱內實時轉譯并運行。此外,CodeSandbox 的沙箱運行在一個 iframe 內,可以隔離代碼的運行時環境,避免污染設計器的全局變量。
Tango 沙箱的基本結構
CodeSandbox 是一個在線運行 JavaScript 代碼的平臺,它的沙箱借助 Babel 與 Web Worker 等能力,在瀏覽器上實時轉譯與運行代碼。你可以把它的沙箱能力想象成一個在瀏覽器上運行的 webpack,比如它的轉譯器 Transpiler 就和 webpack 的 loader 比較接近。。
由于 CodeSandbox 自己實現了各個模板的轉譯規則,整個轉譯流程均由自己把控,因此它整體上會比 webpack 輕量些。例如 CodeSandbox 在初始化依賴時能忽略掉絕大多數的 devDependencies,從而大幅減少項目的依賴初始化時間與轉譯時間。
結合 Tango 后的沙箱可以簡化為三個部分:
- 沙箱前端組件:一個開箱即用的沙箱組件,只需要傳入代碼和配置就可以完成應用的渲染
- 在線打包器:提供搭建產物的瀏覽器端構建能力,類似于一個瀏覽器版本的 webpack,最終形態是一個獨立的 iframe
- 沙箱后端服務:對依賴的資源進行預構建,以及提供資源合并等服務,用來加速沙箱內部的構建打包過程
它的工作流程可以簡述如下:
- 代碼準備:平臺引用沙箱組件,通過 postMessage 將代碼傳遞給沙箱
- 依賴初始化:沙箱處理傳入的文件,根據 package.json 的 dependencies 調用 Packager 打包服務獲取依賴
- 轉譯代碼:解析代碼的依賴關系,將依賴的代碼通過對應的 Transpiler 轉譯
- 執行代碼:在沙箱中初始化 html 等,然后從代碼的入口文件開始執行轉譯后的代碼
- 上述執行周期內和執行完成后,沙箱會拋出事件讓平臺感知
Tango 沙箱的工作流程
本部分主要參考了 CodeSandbox 如何工作? 上篇 的部分內容,并在此基礎上進行了修改。如果你對 CodeSandbox 底層的更多細節感興趣,不妨閱讀下這篇文章。
依賴的初始化
如前所述,CodeSandbox 在內部實現了核心的轉譯邏輯(例如 Babel 與 less 轉譯),整個轉譯流程都由自己控制,因此在初始化依賴時可以相對輕量一些,只需獲取 dependencies 里必要的依賴,忽略掉 devDependencies 以及 @types 開頭的只在本地開發時才會用上的依賴。
CodeSandbox 是如何獲取依賴的呢?CodeSandbox 實現了兩套方案,一套是默認的遠程在線打包方案,另一套是從 unpkg/jsdelivr 等 npm 包資源的 CDN 獲取依賴的兜底方案。
CodeSandbox 設計了一個 Serverless 服務 dependency-packager,這個服務負責在線拉取依賴,然后一次性返回包括子依賴在內的所有需要的文件。當服務接收到接口請求后,會解析 URL 中的包名與版本號,并在服務端執行 yarn install 安裝 npm 包,然后從入口文件開始逐一解析依賴的文件以及各個包之間的依賴關系,最后將被依賴的文件一次性返回。由于該服務僅返回被依賴的文件,在減少網絡請求的資源大小的同時,沙箱可以避免轉譯 .d.ts 或測試用例這樣運行時不需要的文件。
不過由于 packager 返回的文件是從包的入口文件開始計算的被引入的文件,因此在實際使用中,一些未被引入的文件可能也會被項目使用。當項目引入了被排除的資源時,沙箱會在前端請求 unpkg/jsdelivr 作為兜底方案,從而順利完成轉譯。當然,缺點就是如果缺失的文件比較多,實時獲取的方案會多出很多的網絡請求開銷。因此 CodeSandbox 還使用了 Service Worker 作資源緩存,減少二次復訪的網絡請求。
轉譯與構建
當 CodeSandbox 開始轉譯時,會調用 compile() 方法開始轉譯,整個轉譯流程大致如下:
傳入沙箱的參數除了代碼外,還需要傳入 template 參數,該參數用于指定沙箱轉譯時需要使用的 Preset。Preset 就像 webpack 的配置文件一樣,內部定義了如何預處理依賴、不同的文件該使用哪些 Transpiler、在代碼執行前做一些其他的操作等。
Preset 初始化好后,沙箱將初始化一個 Manager 實例,這個 Manager 實例會被 compile() 使用,用于控制整個轉譯流程的生命周期。然后,Manager 會按照上一節提到的方式初始化項目的依賴。如果傳入的依賴發生了變更,沙箱會重新初始化一個新的 Manager 實例,避免運行時被舊的 Manager 依賴影響。
依賴準備好后,傳入沙箱的代碼會被傳入 Manager,Manager 會將代碼實例化為 TranspiledModule,解析各模塊的依賴關系,計算是否被更新或刪除等。然后沙箱將從代碼的入口模塊開始,根據 Preset 里定義的規則,對每一個模塊遞歸調用指定的 Transpiler 轉譯。這里 Transpiler 就像 webpack 的 loader 一樣,負責將文件轉譯為需要的產物。對于復雜的 Transpiler——例如負責轉譯 JavaScript 的 BabelTranspiler——還會使用 Web Worker 隊列來提升轉譯效率。
當相關的模塊都被轉譯好后,Manager 會進入代碼執行階段。
代碼執行
沙箱的運行時模擬了 CommonJS 所需的環境,如 require、module、exports、global 等方法與變量。當所有需要的模塊都被轉譯好后,Manager 會進入代碼執行階段。代碼執行的核心代碼如下:
const allGlobals: { [key: string]: any } = { require, module, exports, process, global, ...globals,};const allGlobalKeys = Object.keys(allGlobals);const globalsCode = allGlobalKeys.length ? allGlobalKeys.join(', ') : '';const globalsValues = allGlobalKeys.map(k => allGlobals[k]);const newCode = `(function $csb$eval(` globalsCode `){` code `n})`;// @ts-ignore(0, eval)(newCode).apply(allGlobals.global, globalsValues);return module.exports;
沙箱會從入口模塊開始執行,執行時會將代碼封裝為上述的立即執行函數,然后調用 eval() 執行并傳入上述 CommonJS 的方法與變量。若代碼引用了其他文件,執行時調用的 require() 方法會按照相同的邏輯遞歸執行并返回執行后的產物。
經過上述流程后,項目中的代碼就會被轉譯并執行,最終渲染在沙箱里,你就能看到代碼的實際效果了。
沙箱的優化改造
在 Tango 上開發的應用是一個完整的項目,并非像 CodeSandbox 網站上那樣主要用于承載簡單的示例或代碼片段。因此用戶對沙箱自身的構建性能與加載速度有較高的要求,以滿足日常的開發體驗。
關于我們對 CodeSandbox 優化的具體細節,可以參考我們之前的這篇 云音樂低代碼:基于 CodeSandbox 的沙箱性能優化 ,修改后的 CodeSandbox 代碼也可以在 GitHub 上找到。
接入 Tango 沙箱
Tango 低代碼設計器除了需要讓沙箱運行源碼、渲染頁面以外,還需要實現可視化搭建的拖拽能力,因此設計器需要感知到用戶在沙箱內的操作。但是,由于沙箱運行在一個獨立的 iframe 內,并且部署在獨立的域名下,兩者之間是跨域的,因此需要做跨域兼容。通過將設計器平臺與沙箱的 document.domain 均設為相同的父域名,并針對 Chrome 的安全策略 在平臺與沙箱添加 Origin-Agent-Cluster: ?0 的 HTTP 響應頭,就能實現平臺與沙箱的跨域通信。
為了簡化沙箱的使用成本,我們封裝了一個 React 組件 @music163/tango-sandbox 供設計器使用,相關代碼可以在 Tango 的 GitHub 倉庫里找到。它主要分為如下三個部分:
- IFrameProtocol:負責與沙箱通信。通過監聽 message 事件接收從沙箱傳出的消息,以獲取沙箱主動傳出的生命周期。通過在 iframe 內部調用 postMessage() 方法向沙箱傳遞事件,從而控制沙箱。
- PreviewManager:負責管理沙箱的基本渲染。其借助上面的 IFrameProtocol 與沙箱通信,當代碼發生變化時,會向沙箱發送 compile 消息,從而觸發沙箱的構建與渲染。
- Sandbox:用于渲染沙箱的 React 組件。除了掛載沙箱的 iframe 外,還包括了沙箱配置、注冊事件監聽函數、消息傳遞、路由管理等功能。當組件傳入的 props 發生變化時,會相應地更新沙箱代碼、更新 iframe 路由等。
Tango 低代碼引擎通過向 Sandbox 組件傳入 files 來實現代碼的渲染,并傳入 eventHandler 來監聽用戶在沙箱內的拖拽操作,最終實現了設計器的組件拖拽搭建能力。
不過,沙箱獲取依賴的基本能力主要是 CodeSandbox 提供的 packager 與 JSDelivr、unpkg 提供的,如果需要使用團隊內部的私有 registry 就需要將相關服務私有化部署了。限于篇幅就不在此做過多贅述,關于 Tango 沙箱的具體接入文檔,以及上述第三方服務私有化部署需要做的修改,可以參考我們提供的 沙箱接入文檔。
總結
本文簡單介紹了 Tango 低代碼引擎的沙箱能力,并分析了 CodeSandbox 的基本結構和工作流程。通過 CodeSandbox 強大的沙箱能力與優化,Tango 低代碼引擎實現了可視化預覽與搭建能力,為開發者提供了便捷高效的開發體驗。
Tango 開源計劃
目前我們已經完成了 Tango 核心實現的基本代碼庫的開源,包括核心引擎內核、沙箱、設置器、應用框架、物料協議等等,并發布了 RC 版本。在今年,我們將持續推進云音樂低代碼核心能力的開源,包括基本的服務端能力,前端組件庫等,并持續優化和完善開源文檔。并且,隨著其他能力的穩定和時間的成熟,我們還將會持續向社區開源更多的內部實踐。
參考資料
- CodeSandbox 如何工作? 上篇
- 云音樂低代碼:基于 CodeSandbox 的沙箱性能優化
- 搭建一個屬于自己的在線 IDE
- 網易云音樂 Tango 低代碼引擎實現揭秘
作者:0xcc
來源:微信公眾號:網易云音樂技術團隊
出處:https://mp.weixin.qq.com/s/GqSoZR3bSeuLWiULHt8Zvg