如果要從1995年算起,JavaScript已是老牌語言,從1997年標準化以來,卻直到2015年ECMAScript 6,JavaScript才正式有官方的模組規範。此外,無論是在伺服端Node.js或瀏覽器,模組的支援,卻是在近期才有(實驗性的)實現。

在這段期間,出現了各式的業界規範,而各式的語法轉換器更加劇了這場混亂。

CommonJS模組規範的前後

2006年左右,因為在Ajax概念的實現獲得接受,JavaScript在瀏覽器這個舞臺上,才得以重新發揚光大,而當年倉促定義的語言特性也暴露許多缺點。

支撐JavaScript持續發展下去的,是語言本身有許多修補的可能性,開發者逐漸從經驗中,累積了許多避兇趨吉的模式,而在避免全域名稱這方面,也發展出物件階層、IIFE、全局匯入、模組匯出等模式,在過去許多程式庫上,像是Dojo、YUI、jQuery等,都可見相關模式的實現,有興趣回顧各模式的話,可以參考〈JavaScript Module Pattern: In-Depth(https://goo.gl/ccs2ZX)〉的內容。

在瀏覽器端,各家程式庫各自實現自己的模組方案,而為了在瀏覽器環境之外構築出JavaScript生態系,2009年一月,Mozilla的開發者Kevin Dangoor發起了ServerJS專案,在〈What Server Side JavaScript needs(https://goo.gl/Vhp5cq)〉中提到,JavaScript必須得有個模組的標準規範,同年九月專案更名為CommonJS,顯示其不想僅局促於伺服端的意圖。

儘管Node.js後來定位為SSJS(Server side JavaScript)而不再遵守CommonJS(一些核心開發者認為,Node.js本身就是伺服端JavaScript的業界標準了),然而Node.js仍被當成是CommonJS最為人知的實現之一,因而談及模組的文件,多半以Node.js作為介紹,在模組的實現上,一個.js是一個模組,透過module.exports來公開模組的功能性,而require介面用來載入模組。

雖然Node.js的主打特性是非同步,不過,require在載入模組時卻是同步的,於是,將CommonJS推至瀏覽器成為其模組規範時,就成了一大阻礙,因為,這意謂著require某個模組時,會阻斷瀏覽器對後續頁面的處理,如何能讓CommonJS也能適用於瀏覽器,造成了CommonJS模組規範的分岐。

非同步模組定義AMD

在打算將CommonJS模組規範推至瀏覽器端時,有一派主張制訂Modules/Transport規範,實作遵守規範的轉換工具,將模組轉換為適用瀏覽器環境即可,實現之一是Component,然而後來停止維護,並推薦使用webpack、browserify等其他轉換工具。

另外有一派認為瀏覽器有其特性,不應直接採用原本為伺服端而生的CommonJS,後來他們獨立建立了非同步模組定義AMD(Asynchronous Module Definition),定義模組時使用define介面,雖然也採用require介面載入模組,但為了不影響頁面的後續處理,載入的過程是非同步,在模組載入完成之後,才透過require時指定的回呼函式進行通知,而AMD最為人所知的實現之一是RequireJS。

如果伺服端與瀏覽器乾脆地使用兩種語言來開發程式就好了,偏偏JavaScript語法在伺服端與瀏覽器都能派上用場,而有些API在演算與特性上,並沒有哪一端的差別,因此模組方面產生的分岐,成為JavaScript生態圈極力弭平的對象。為了設法共用,採用轉換工具是一種方式,有的開發者則推薦使用UMD(Universal Module Definition)模式,簡單來說,就是事先撰寫程式碼,透過特性偵測來察覺有無define或export,以確認要採用哪個模組規範。

實際上,還有其他模組定義的出現,例如,Modules/Wrappings是後來試圖兼併CommonJS與AMD優點的規範,而由於AMD在定義模組時,若依賴的模組較多,要不就在define上寫出全部的模組,要不就在define中寫出一堆require與回呼函式而不易撰寫與閱讀,為了解決這個問題,AMD又提出了部份相容於Modules/Wrappings的寫法,稱為Simplified CommonJS wrapping,而RequireJS也實現了該規範,另外,也還有聲稱改進Modules/Wrappings的CMD等模組定義。

ECMAScript 6模組規範

規範是為了統一,JavaScript的模組規範卻不斷分岐,然而,JavaScript官方標準ECMA-262規範,對模組這方面並非沒有行動。早在ES4 Harmony,就有過packages、namespaces的討論,不過ES4 Harmony後來因為各種衝突而腰斬,使得ES3.1成了ES5,而模組規範則在ECMAScript 6完成了定案。

在ES6中,一個模組就是一個.js文件,import用來載入模組,export用來公開名稱,如果是在瀏覽器中,可以透過<script type="module" src="xxx.js"></script>來表示.js是個模組(Chrome 61之後支援),而模組中的import語句是非同步載入的,不會阻斷後續頁面的呈現,等到頁面呈現完畢之後,再依模組於頁面中定義的順序,依序執行模組的內容。

ES6的模組定義對瀏覽器來說,比較有利,然而對於CommonJS來說,有許多的不同點。例如CommonJS的export,與ES6中的export行為不同──ES6中export的是名稱,名稱本身參考的值變動,透過名稱取得的也就是變動後的名稱,ES6中的模組不是個物件,而非同步載入這方面,與CommonJS的require語義完全不同,在〈An Update on ES6 Modules in Node.js(https://goo.gl/uNJk5V)〉中,就談到Node.js載入模組時與ES6的差異性,以及需要克服的許多困難點。

雖說如此,Node.js正實驗性地納入ES6模組,在8.5版本之後,可以使用--experimental-modules開啟該功能。

由於ES6模組與Node.js模組有個重要不同,就是ES6在執行模組之前就會解析模組中的import與export宣告,這對Node.js來說,就等於要在載入檔案前,就能區別載入的是哪種模組,以便採用不同的解析方式,為此,Node.js(目前有點不情願地)採用了.mjs來表示一個ES6模組,而.js保留給既有的Node.js模組(.mjs被戲為Michael Jackson Script)。

混亂中的方向

儘管ES6自2015年就正式推出,在模組方面的實現,卻是直到近來才看得到。無論如何,就目前來看,瀏覽器與伺服端的霸主Node.js,都有意支援ES6模組,儘管規範還有待完善,然而JavaScript社群對ES6模組多採取正面的態度,看來透過ES6模組來終結這場混亂,會是未來主要的方向。

雖然還存在著瀏覽器相容性,以及Node.js中ES6模組尚在實驗性質等問題,但JavaScript生態最不缺乏的,就是語法轉換器,也因此解決了一些問題。

如果真的想堅持一種模組規範,就長遠來看應使用ES6模組,然後透過Babel之類的工具,將之轉換為CommonJS或AMD等風格,然而,就個人偏好而言,其實並不愛好語法轉換器這類東西,原因之一就在於它們多半就是語法轉換,至於在行為上是否符合規範,必須深入瞭解!

這點就算透過Babel等工具,將ES6模組轉換為CommonJS模組,也不例外。

就像ES6的export的是名稱,語法轉換過去之後行為上就不是了,因而要用語法轉換器的話,轉換的方向必須一致,像是打算以ES6終結混亂的路上,暫時採用語法轉換器這個必要之惡,而不單是為了便宜行事之用(只是懶得從CommonJS改AMD或反過來之類的)。

若是為了便宜行事,那麼分岐只會深化或甚至增加,而ES6模組就只會是分岐的其中一個規範罷了!

專欄作者

熱門新聞

Advertisement