一 概要
在前端工程中,有時我們需要在瀏覽器編譯並執行一些程式碼,這種需求常見於低程式碼場景中。例如我們在搭建時需自定義一部分程式碼,這些程式碼需要在渲染時執行。為了方便起見,我們寫的程式碼一定是 ES6 語法,如果要在瀏覽器執行,那麼就必須經過編譯。下面是前端編譯 JS 程式碼的一些實踐。
二 需求描述
-
低碼搭建時需要自定義一部分程式碼 -
希望程式碼是以多檔案形式組織的 -
可以使用 ESModule 形式匯入/匯出
三 需求分析
1、在瀏覽器編譯程式碼必然需要使用 babel 完成;
2、如果只有一個 JS 檔案,那麼可以直接使用 babel 的 transform 函式編譯;
3、如果存在多檔案,則檔案內的變數必須相互隔離,且檔案之間能夠透過某種形式相互引用,並且需要考慮檔案之間的依賴關係;
四 核心設計
流程

1 變數隔離
由於我們的需求是多檔案編輯,各個檔案內的變數應該相互隔離。最簡單的辦法是將每個文的內容轉成一個閉包,再透過固定的介面將每個檔案連線起來。
假設有 a.js,內容如下:
const a = 1;
const b = 2;
functionsum(){
return a + b'
}
sum();
可以將其轉為如下形式:
(function(){
const a = 1;
const b = 2;
functionsum(){
return a + b'
}
sum();
})();
轉成這種形式之後,每個檔案內的變數就只會存在於各自的閉包之內,互不影響。
五 檔案引用
檔案之間的相互引用可以透過定義一種介面規則實現:
-
所有檔案的引用都將透過全域性變數 module 進行; -
每個檔案都將對應到 module 上的一個物件,key 根據檔名而定。
1 匯出
原檔案:
// a.js
exportconst a = 1;
編譯後:
(function() {
__filename = 'a.js';
const a = 1;
var mod = {};
mod.a = a;
module[__filename] = mod;
})()
2 匯入
原始檔
// b.js
import { hello } from'./a'
hello();
編譯後
(function() {
__filename = 'b.js';
var $$a = module['a.js'];
$$a.hello();
var mod = {};
module[__filename] = mod;
})()
六 依賴樹解析
假設有一堆檔案,我們透過解析(babel 或正則)後得到他們之間的關係如下:
他們之間存在迴圈依賴

根據這個依賴圖可以梳理出幾條依賴路線:
A -> B -> D -> C -> F -> 迴圈依賴B
A -> B -> E -> F -> 迴圈依賴 B
A -> C -> F -> B -> E -> 迴圈依賴 F
A -> C -> G
從開始出現的第一個迴圈依賴截斷依賴路線,分別統計統計每個節點的深度,按深度依次放入佇列中。
如果兩個節點深度相同,則分析兩個節點的依賴關係,被依賴的先進佇列,故最終形成的佇列如下:
F E B C D G A
為什麼要得到一個編譯順序呢?
以上得出的編譯順序是為了儘可能解決如下的引用情況,但也不能解決所有:
// a.js
exportconst a = 2
// b.js
import { a } from'a.js';
console.log(a + 2);
這時候,假設執行 b 的時候,a 還沒被執行,那麼 b 內部拿到的 a 實際上是 undefined,顯然不是我們所希望的。所以此時必須保證 a 先於 b 執行。
但這種使用方式在存在迴圈引用時無法解決,只能調整檔案組織形式。
事實上,假設存在迴圈依賴時,下面的在函式內或在類內引用方式是沒有問題的,有問題的只是直接使用:
// a.js
exportconst a = 2
// b.js
import { a } from'a.js';
exportfunctiontest () {
return a + 1;
}
這樣,即使 b 有依賴 a,test 只要不是立即執行函式也不會產生影響。
七 編譯
1 ESModule 轉換
此過程可以透過自定義一個 Babel 外掛完成,在語法編譯時將檔案編譯成一個閉包,同時處理好 ESModule 語法。
該 Babel 外掛很簡單,在此就不展開去寫了。
2 檔案佇列編譯
對單個檔案的編譯可封裝成一個方法,假設函式名為:compileFile
按照上面解析到的檔案佇列按照順序逐個呼叫 compileFile 進行編譯,並將結果直接拼接起來,形成一個巨大的字串,該字串的樣子應該是如下的格式:
(function() {
__filename = 'b.js';
var $$a = module['a.js'];
// ...
var mod = {};
module[__filename] = mod;
})();
(function() {
__filename = 'a.js';
var $$b = module['b.js'];
// ...
var mod = {};
module[__filename] = mod;
})();
// ...
3 JS 執行
最後一步,執行上面得到的編譯結果即可,此步驟可直接使用 new Function 的方式完成,例如:
(假設以上的字串內容儲存在 compiledScript 中)
const exec = new Functioon(`
varmodule = {};
${compiledScript};
returnmodule;
`);
constmodule = exec();
module['a.js'] // a.js 的匯出內容
module['b.js'] // b.js 的匯出內容
八 總結
至此,一個前端可執行的小型打包工具就已實現,可以直接在前端進行多檔案的編輯和執行。
即時上,此過程僅適用於不方便藉助伺服器的場景,如果有條件允許可以藉助伺服器,那麼編譯過程最好在服務端完成,甚至還可以藉助 webpack 或 rollup 等打包工具實現更好的編譯效果。
參考
目前我們在 ali-lowcode-engine 之上的原始碼外掛(@ali/lowcode-plugin-code-editor)內部實現了多檔案的支援,目前僅做了最簡單的實現:模組引用直接採用了 UMD 規範,暫時也沒有考慮迴圈依賴和執行順序。
後續會嚴格按照以上步驟進行最佳化。
資料分析系統之資料管理與資料倉庫
點選閱讀原文檢視詳情
關鍵詞
函式
程式碼
前端
節點
形式