農林漁牧網

您現在的位置是:首頁 > 農業

前端實現多檔案編譯器

2022-03-29由 阿里技術 發表于 農業

結點結構是什麼

前端實現多檔案編譯器

一 概要

在前端工程中,有時我們需要在瀏覽器編譯並執行一些程式碼,這種需求常見於低程式碼場景中。例如我們在搭建時需自定義一部分程式碼,這些程式碼需要在渲染時執行。為了方便起見,我們寫的程式碼一定是 ES6 語法,如果要在瀏覽器執行,那麼就必須經過編譯。下面是前端編譯 JS 程式碼的一些實踐。

二 需求描述

低碼搭建時需要自定義一部分程式碼

希望程式碼是以多檔案形式組織的

可以使用 ESModule 形式匯入/匯出

三 需求分析

1、在瀏覽器編譯程式碼必然需要使用 babel 完成;

2、如果只有一個 JS 檔案,那麼可以直接使用 babel 的 transform 函式編譯;

3、如果存在多檔案,則檔案內的變數必須相互隔離,且檔案之間能夠透過某種形式相互引用,並且需要考慮檔案之間的依賴關係;

四 核心設計

流程

前端實現多檔案編譯器

1 變數隔離

由於我們的需求是多檔案編輯,各個檔案內的變數應該相互隔離。最簡單的辦法是將每個文的內容轉成一個閉包,再透過固定的介面將每個檔案連線起來。

假設有 a。js,內容如下:

const

a =

1

const

b =

2

function

sum

()

{

return

a +

b‘

}

sum();

可以將其轉為如下形式:

function

()

{

const

a =

1

const

b =

2

function

sum

()

{

return

a +

b’

}

sum();

})();

轉成這種形式之後,每個檔案內的變數就只會存在於各自的閉包之內,互不影響。

五 檔案引用

檔案之間的相互引用可以透過定義一種介面規則實現:

所有檔案的引用都將透過全域性變數 module 進行;

每個檔案都將對應到 module 上的一個物件,key 根據檔名而定。

1 匯出

原檔案:

// a。js

export

const

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

export

const

a =

2

// b。js

import

{ a }

from

‘a。js’

console

。log(a +

2

);

這時候,假設執行 b 的時候,a 還沒被執行,那麼 b 內部拿到的 a 實際上是 undefined,顯然不是我們所希望的。所以此時必須保證 a 先於 b 執行。

但這種使用方式在存在迴圈引用時無法解決,只能調整檔案組織形式。

事實上,假設存在迴圈依賴時,下面的在函式內或在類內引用方式是沒有問題的,有問題的只是直接使用:

// a。js

export

const

a =

2

// b。js

import

{ a }

from

‘a。js’

export

function

test

{

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(`

var

module

= {};

${compiledScript};

return

module

`);

const

module

= exec();

module

‘a。js’

// a。js 的匯出內容

module

‘b。js’

// b。js 的匯出內容

八 總結

至此,一個前端可執行的小型打包工具就已實現,可以直接在前端進行多檔案的編輯和執行。

實時上,此過程僅適用於不方便藉助伺服器的場景,如果有條件允許可以藉助伺服器,那麼編譯過程最好在服務端完成,甚至還可以藉助 webpack 或 rollup 等打包工具實現更好的編譯效果。

參考

目前我們在 ali-lowcode-engine 之上的原始碼外掛(@ali/lowcode-plugin-code-editor)內部實現了多檔案的支援,目前僅做了最簡單的實現:模組引用直接採用了 UMD 規範,暫時也沒有考慮迴圈依賴和執行順序。

後續會嚴格按照以上步驟進行最佳化。

資料分析系統之資料管理與資料倉庫