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