當ECMAScript 6將模組納入了規範,主流的常青瀏覽器最新版本已全數支援,然而試著於實作中使用時,卻常面對匯入地獄。

採用模組整合之類的工具,是簡單的解決方式,不過,在這麼做之前,也是個機會,可以好好思考模組在設計上是否合理。

ES6模組路徑

ES6納入模組規範之後,開發者關心的問題之一,自然是瀏覽器的支援度。有一個里程碑,是2018年5月的Firefox 60釋出,這個版本預設開啟了ES6模組的支援,在這之前,Chrome、Safari、Edge也已預設支援,若應用程式的使用對象是這幾個主流常青瀏覽器,就可以考慮使用ES6模組。

ES6模組在匯入時的路徑指定,可以是相對路徑或者是絕對路徑,在瀏覽器上,甚至可以使用URI來指定模組目的地,若是指定來源是外部網站,該網站只要支援跨域資源共享(CORS),就可以像傳統象用<script>標籤那樣,匯入(引用)外部網站上提供之模組。

在實作ToyLang語言之前,我正好也對ES6做了全面整理(https://goo.gl/rc3hQC),為了能更熟悉ES6的實際應用,採用了ES6來實作ToyLang,隨著語言實作內容的增加,程式碼必然要採用模組來進行管理,由於ToyLang實作不依賴其他程式庫,也就理所當然採用了ES6模組。

在模組都位於同一階層時,import {xxx} from './m1.js'這種相對路徑就行了,然而隨著模組的增加,開始需要階層性來組織模組時,問題就發生了,若要匯入頂層模組,就會產生import {orz} from '../../../../abc/xyz/m1.js'這樣的匯入地獄。

絕對路徑?相對路徑?

那麼,該採用絕對路徑匯入嗎?若開發的是個foo程式庫,在foo資料夾中有各個階層的模組,而且foo是放在Web網站根目錄,那麼,寫import {orz} from '/foo/abc/xyz/m1.js'不就好了?嗯!問題就在於,「foo是放在Web網站根目錄」這樣的假設,或者任何的擺放路徑要求都是不可行的。

為了程式庫使用上的彈性,在採用ES6模組的情況下,勢必要採用相對路徑的匯入方式,在一些語言中會提供相對路徑的模組匯入機制,就是為了能令某些模組獨立於階層設計,例如,為了避免修改了頂層的階層名稱,而導致模組匯入名稱也必須跟著修改的問題,在模組階層性相當複雜的情況下,若模組關心的是同一階層的模組或子模組,就可以採用相對匯入來避免複雜冗長的階層設定。

Python就提供了相對匯入的語法,然而PEP 8中建議使用絕對匯入,因為有較好的可讀性,模組行為也較容易掌握,雖然適當使用明確相對匯入語法是個可接受的方案,然而相對匯入會受到套件限制,只有同一套件中的模組才可以使用相對匯入。

因此,像import {orz} from '../../../../abc/xyz/m1.js'這類情況,有Python中是不允許的,必須使用絕對路徑,在Python中,可運用多種方式來設定模組路徑,像是設定PYTHONPATH或透過sys.path,雖說是絕對路徑,其實是相對於模組路徑的絕對路徑。

許多語言都可以設定模組或者程式庫路徑,因而,使用絕對路徑匯入相關語法,就不會有什麼問題,然而,ES6本身沒有對模組進行組態的方式,也就無法設定模組路徑,這就使得絕對路徑的匯入指定,真的就是完完全全的絕對路徑。

避免匯入地獄

有些JavaScript程式庫、框架,或者是模組整合、建構工具等,能夠透過本身的組態檔設定解析路徑選項,接著在.js中,就可以使用絕對路徑匯入(當然是相對於設定的解析路徑),就解決匯入地獄的問題來說,確實是個直接而不用多加思索的方案。

雖然ES6的import陳述指定模組時,使用了字串表示,然而不能動態組合字串來指定模組名稱,這是因為ES6模組是基於靜態設計,在編譯時期就確定模組的依賴。

實際上,ES6草案本來有個模組載入器API,但是,在正式規範當中,被拿掉了,而有新的載入器規範由WHATWG控管(https://goo.gl/xrR4RP),目前,有個ES Module Loader Polyfill(https://goo.gl/o8wubc)實現,可以作為動態載入模組的一個可能性。

然而,在試著採用工具或程式庫來解決匯入地獄之前,也可以先思考一下,目前為什麼會發生匯入地獄?

這往往是個訊號!例如:目前的模組是不是放錯位置了?模組的函式或類別,是否沒在它應該存在的模組之中?有沒有辦法調整模組階層,令目前的模組依賴於本身子階層的模組?而不是多層父階層外的其他子階層中之模組等。

如果開發的是個程式庫,盡可能地從關切點分離的角度,來思考模組階層以及模組之間的從屬關係,往往就能消去不少匯入地獄的問題,剩餘無法消去的部份,可能暗示著,該模組階層是一個可以獨立存在的程式庫。

這就等同於目前的應用程式依賴於另一個程式庫的問題了,應用程式的模組頂層,基本上,會與另一程式庫的頂層並列,如果應用程式中某個模組,需要程式庫中某階層的模組,那麼,相對匯入似乎不可避免?

其實,有個設計上的考量,往往被開發者忽略,當依賴於某程式庫時,應用程式實際上應該設計出一層介面,令應用程式依賴於該介面,接著,介面中的實作才實際取用程式庫,而不是在應用程式的原始碼中,直接撰寫該程式庫的API;因此,若匯入地獄實際上就是在使用程式庫深層次的模組,這就是暗示著,應用程式也許正在直接且深度地耦合該程式庫。

為程式庫創造出一層介面,有很多方式,在最簡單的情況下,也許只要有個export.js寫著:

export {Native, Null} from '../interpreter/ast/value.js';
export {StmtSequence} from '../interpreter/ast/statement.js';
export {Variable} from '../interpreter/ast/assignment.js';

然後,目前階層中的模組只要import {Native, Null} from './export.js',就可以匯入必要的模組,這避免了相對路徑散布至各模組之中,若要改變相對路徑,也只要改變export.js就可以了。

若依賴的API多,也許需要一層Facade,若依賴的API少,可以使用一組工廠方法,或者在應用程式運行時,註冊相關的模組,而在子模組中,透過查找名稱的方式來獲取模組,某些程度上,這就像是創造了簡單的動態模組載入機制。

也許是模組餿味

就目前來說,ES6確實沒有MODULEPATH之類的選項可以設定,直到最新的ES9(ECMAScript 2018)也沒有(或許是在等待WHATWG模組載入器規範),因此使用ES6模組時,還是有很多遇到匯入地獄的機會。

想要動手解決匯入地獄,實際上,有很多方式,使用模組整合之類的工具,只是其中之一,或許也應該是最後的選項,在這之前可先想想,目前的匯入地獄是否散發出模組設計上的餿味道。不假思索就使用模組整合之類的工具,看似可以輕鬆愉快地解決問題,實際上也失去許多重構模組的好機會!

作者簡介


Advertisement

更多 iThome相關內容