談談Javascript 模塊現狀,並為絲滑過渡做好準備
我們webpack團隊正在努力進行一些工作,幫助開發者絲滑過渡。為了做到這一點,我們計劃在Node.js的ESM支持成熟後,模擬Node.js導入CJS的方式。
最近在twitter上有很多關於ES Module現狀的討論,尤其是在Node.js上,因為他們計劃引入*.mjs文件擴展名。人們的擔憂和不確定性是情有可原的,因為這個主題很複雜,接下來我會儘力來闡述這個問題。
來自遠古的恐懼
大多數前端開發者應該還記得javascript依賴管理的黑暗時期。那個時候,你需要把一個庫複製粘貼到vendor文件夾,然後作為一個全局變數引入,要自己去按次序組合所有東西,可能還要管理命名空間。
在過去的那些年,我們能深刻體會到公共模塊格式化和中央模塊管理的價值。
在今天,不管是發布還是使用一個庫都要容易得多,只需要使用npm publish和npm install命令就行。這就是人們會那麼緊張兩種模塊系統兼容性問題的原因:他們不想失去已有的舒適區。
接下來我會解釋和總結現有實現的情況,以及為什麼Node生態遷移到ES Module(ESM會那麼難。在最後,總結這些變化對於webpack使用者和模塊作者有什麼影響。
現有實現
目前,ESM有三種方式的實現:
瀏覽器
webpack以及類似的構建工具
Node(未完成,但可能在年底作為一個實驗功能)
為了更好地理解現在的討論,首先要知道ES2015包含兩種模式:
script用於具有全局命名空間的常規腳本
module用於具有明確導入和導出的模塊化代碼
如果你試圖在script標籤使用import或者export 語句,會拋出一個SyntaxError。這種語句在全局環境下沒有任何意義。另一方面,module模式即意味著嚴格模式,禁止使用某些語言特性,比如with語句。因此,需要在腳本被解析和執行之前定義模式。
瀏覽器中的ESM
截至到2017年5月,所有主流瀏覽器都開始做了ESM 的實現工作。不過,大部分仍處於在實驗性質。這裡不會做詳細介紹,因為Jake Archibald已經寫了一篇很厲害的文章:
https://jakearchibald.com/2017/es-modules-in-browsers/
除了一些輕微困難,在瀏覽器中實現起來非常容易,因為以前並沒有模塊系統。想要指定module模式,需要在script標籤添加type="module"屬性,如下所示:
在一個模塊中,當前只能使用有效的URL作為模塊標識符。模塊標識符是用於require或import其他模塊的字元串。為了確保未來兼容CJS模塊標識符,「bare」導入標誌符(如 import "lodash")現在還不支持。模塊標識符必須是絕對URL或者是以/,./, ../開頭:
同樣需要注意的是,一旦處在一個模塊中,每個導入也將被解析為module,而且沒有辦法import一個script。
ESM與webpack
類似webpack這樣的構建工具通常會嘗試用module 模式解析代碼,有問題再切回到script模式。這些工具的生成結果是一段script,通常是在一定程度上模擬CJS和ESM行為的模塊運行時。
我們以這兩個簡單的ESM為例:
webpack使用函數包裝器封裝模塊範圍和對象引用來模擬ESM實時綁定。每次編譯,還包括一個模塊運行時,負責引導和緩存模塊。此外,將模塊標識轉換為數字模塊ID。這樣可以減少打包的大小和引導時間。
這是什麼意思呢?我們來看看編譯輸出:
簡化的webpack輸出,模擬ES Modules行為
結果已經簡化並刪除了一些與此示例無關的代碼。你會發現,webpack在exports對象上將所有export 語句替換成Object.defineProperty,並使用屬性訪問器替換對引入值的所有引用。還要注意每個ESM 開始時的"use strict"指令,這是由webpack自動添加,在ESM中必須是嚴格模式。
這種實現只是模擬,因為它試圖模仿ESM和CJS的行為——但不是與其完全保持一致。比如,這種模擬並不符合某些邊緣情況。看下面這個模塊:
如果你通過加上babel-preset-es2015的Babel 來運行,結果是:
從輸出結果可以看出,Babel假設默認是ESM,因為 module模式即代表嚴格模式,在嚴格模式下會將this初始化為undefined。
然而,使用webpack,結果是:
在引導模塊時,this將指向exports ,與Node.js使用的CJS行為一致。這是因為語法上不確定是script還是module,解析器無法判斷該模塊是ESM還是 CJS。在不明確的時候,webpack會模擬CJS,因為它仍然是最受歡迎的模塊風格。
這種模擬其實已經包含了很多情況,因為模塊作者通常會避免這種代碼。然而,「很多情況」對於像Node.js這樣的平台是不夠的,因為它需要保證所有有效的JavaScript代碼都能正常運行。
Node.js 中的 ESM
Node.js在執行ESM時遇到了麻煩,因為仍然需要支持CJS,語法看起來相似,但運行時行為完全不同。Node.js核心技術委員會(CTC)成員James M Snell撰寫了一篇很好的文章來解釋CJS與ESM之間的差異。
歸結起來,CJS是一個動態模塊系統,ESM是靜態模塊系統。
CJS
允許動態同步 require()
導出僅在模塊執行後才知道
導出可以在模塊初始化後添加,替換和刪除
ESM
只允許靜態同步import
在模塊執行之前,導入和導出已經關聯
導入和導出是不可變的
由於CJS早於ES2015,所以一直在script模式下解析,封裝通過使用函數包裝器實現。在Node.js中載入CJS,實際上會執行與此類似的代碼:
Simplified function wrapper around CommonJS modules in Node.js
問題出現了,將兩個模塊系統集成到同一個運行時時,ESM和CJS之間的循環依賴可能會迅速導致類似死鎖的情況。
而且,由於現有CJS模塊數量龐大,也不能直接放棄對CJS的支持。為了避免Node.js生態的中斷,有兩點已經很明顯:
現有的CJS代碼必須以相同的方式繼續工作
兩個模塊系統都必須同時且儘可能無縫地工作
目前的權衡
2017年3月,經過幾個月的討論,CTC終於找到了一種達成目的的途徑。由於在ES規範和引擎不改變的情況下無法進行無縫集成,CTC決定開始一些權衡之後的實現工作:
1.ESM必須是*.mjs文件擴展名
這是由於上面提及的模糊語法問題,無法通過解析來確切知曉JavaScript代碼是什麼類型。為了Node.js向後兼容的目標,作者需要加入一種新模式。已經有關於各種替代品的討論,但使用不同文件擴展名是解決目前問題的最佳權衡。
2.CJS只能非同步導入ESM import()
Node.js將非同步載入ESM,以便儘可能接近瀏覽器的行為。因此,同步的require()在ESM是不可能的,並且依賴於ESM的每個功能都需要非同步:
3.CJS向ESM暴露一個不可變的默認導出
使用Babel或Webpack,我們通常將CJS重構為ESM,如下所示:
再次,他們的語法看起來很相似,但忽略了CJS中沒有命名導出的事實。只有一個叫做default的導出,等同於在CJS模塊完成計算後一個不可變的module.exports。
從技術上講,有可能將module.exports結構成命名導入,但這需要對標準作更大的變更。這就是為什麼CTC決定現在才去實現這種方式。
4.模塊範圍的變數類似module,require以及__filename在ESM不存在
Node.js和瀏覽器會實現一些ESM的特性,但標準化過程仍在進行中。
鑒於將CJS和ESM集成到一個運行時的工程挑戰,CTC在評估邊緣情況和權衡方面做了非常好的工作。比如使用不同的文件擴展名是就是一個很簡單的解決方案。
實際上,一個文件擴展名可以認為是一個二進位文件如何解釋的提示。如果一個module不是script,我們應該使用不同的文件擴展名。其他工具(如linter或IDE)可以獲取相同信息。
當然,引入新的文件擴展名有成本,但是一旦伺服器和其他應用程序確認*.mjs為JavaScript,我們很快就會忘記這個爭議。
將* .mjs作為
Node.js的Python3?
考慮到所有這些限制,人們可能會問,這種過渡將對現在的生態造成什麼樣的損害。雖然CTC會努力解決問題,但社區如何採用這一點仍然存在很大不確定性。這種不確定性被眾多知名的NPM模塊作者再次強調,他們聲稱將不會在模塊中使用 *.mjs。
Python 3 is killing Python
很難預測社區如何反應,但是應該不會對現在的生態系統造成大破壞,甚至能看到從CJS平穩過渡到 ESM。主要有兩個原因:
1.與CJS嚴格向後兼容
模塊作者不喜歡ESM,除非能保持CJS不被排擠出局。這樣他們自己的代碼不會受到採用ESM的影響,降低遷移到另一個運行時的可能性,讓NPM遷移到新生態變得容易。從CJS到ESM的重構給包維護者帶來額外工作,不能指望所有人都有時間。
2.CJS在ESM中的無縫整合
從ESM導入CJS模塊非常簡單。需要注意的是,CJS 僅導出一個默認值。一旦處於ESM,甚至可能根本不會注意到依賴關係使用的模塊風格,尤其是與在CJS 中使用await import()相比。
由於ESM的這個優點以及其他有點,比如開箱即用的 tree shaking和瀏覽器兼容性,預計在未來幾年內,我們可以看到向ESM的緩慢而穩定的過渡。
CJS的特性,如動態require()和猴子補丁導出,在Node.js社區一直是有爭議的,不比ESM帶來的好處。
這些對我來說意味著什麼?
因為最近這些事情,很容易對目前存在的所有選擇和限制感到困惑。在接下來,整理了開發人員面臨的典型問題以及我們的回答:
現在需要重構現有的代碼嗎?
不需要。Node.js才剛剛開始實現ESM,仍然有大量的工作要做。James M Snell預計至少還需要一年時間,還有很多變化的餘地,所以現在重構是不安全的。
應該在新代碼中使用ESM嗎?
如果你已經有或者打算使用像webpack這樣的構建工具,答案是肯定的。這將更容易完成代碼庫的過渡,並使tree shaking成為可能。但要小心:一旦Node.js支持原生ESM,可能需要重構其中的一些部分。
如果你正在編寫一個庫,答案也是肯定的,你的模塊使用者將受益於tree shaking。
如果你不想進行構建操作,或者正在編寫一個Node.js應用程序,還是用CJS吧。
現在應該使用.mjs嗎?
不要這樣做,目前沒有什麼好處,工具支持依然薄弱。建議一旦原生ESM支持登陸Node.js,儘快開始遷移。記住,瀏覽器只關心MIME類型,而不是文件擴展名。
應該關心瀏覽器兼容性嗎?
是的,需要在一定程度上關注這個問題。不應該在導入語句中省略.js擴展名,因為瀏覽器需要完整的URL,無法像Node.js這樣執行路徑查詢。
同樣,應該避免index.js文件。不過,人們並不會很快在瀏覽器中使用NPM軟體包,因為仍然不能bare導入。
作為庫作者該怎麼辦?
用ESM編寫代碼,並使用Rollup或Webpack轉換成單個CJS模塊,然後在package.json將main欄位指向此CJS包,並將module欄位指向原始ESM。如果還使用ESM之外的其他新語言功能,則應編譯成ES5,並提供CJS和ESM的打包。這樣,庫的用戶仍然可以從tree shaking獲利而無需對代碼進行轉換。
Look at all these tree shaken modules!
總結
關於ES模塊有很多不確定性。由於目前Node.js在實現上的權衡,開發人員擔心可能會破壞Node.js的生態。
這還不會發生,因為兩個原因:CJS的嚴格的後向兼容和CJS在ESM中的無縫集成。
在Node.js發布原生ESM支持之前,應該仍然使用Rollup和Webpack等工具。它們在一定程度上模擬了ESM環境,但要注意它們不完全符合規範。此外,使用打包仍然是個很好的選擇,一旦可以在瀏覽器中使用NPM軟體包。
我們webpack團隊正在努力進行一些工作,幫助開發者絲滑過渡。為了做到這一點,我們計劃在Node.js的ESM支持成熟後,模擬Node.js導入CJS的方式。
文章來源:
點擊展開全文
※端午紀念屈原,程序媛紀念祖師奶!四幕京劇:程序媛阿達傳(上)
※阿法狗三十六計之功成身退
※端午五美送福利 你會追哪一個?
※MockNet:Android 網路介面開發與測試神器
TAG:優才學院 |
※讓我們來談談死亡·Let』s talk about dying
※AppleWatchSeries 3上手,我來談談優缺點!
※談談 Oracle Exadata 帶來的業務價值及優勢
※談談鎚子系統Smartisan os的使用感受!
※體驗Anker Soundcore Flare,談談藍牙音箱能有哪些新玩法
※深入談談String.intern在JVM的實現
※談談對Linux的Huge Pages與Transparent Huge Pages的認識
※談談SimCloud的架構
※蘋果iPhone 8Plus好不好?上手體驗完,談談優缺點!
※不跑那就走起來吧,談談小米WaklingPad走步機
※談談對GS和Spring Drive的看法
※用了一段時間的iPhoneXSMax,我來談談優缺點!
※借 TicWatch Pro 來談談,我們真的需要一隻智能手錶嗎?
※int 和 Integer 有什麼區別?談談 Integer 的值緩存範圍
※談談微服務架構中的基礎設施:Service Mesh與Istio
※談談.NET Core中基於Generic Host來實現後台任務
※iPhone8plus換上華為P30pro,談談真實使用感受
※談談OpenStack的八年之癢
※談談ASP.NET Core中的ResponseCaching
※有沒有一直到現在還在使用iphone6的朋友,談談對iphone6的看法