在Nodejs或瀏覽器運行ESM代碼
前言
經(jīng)典面試題目就是《Common.js 和 ES module 的區(qū)別》,這一題很多人都會熟練地背出答案。
Commonjs
- CommonJs 可以動態(tài)加載語句,代碼發(fā)生在運行時
- CommonJs 導(dǎo)出值是拷貝, 不好排查引起變量污染
ES module(后續(xù)簡稱 esm)
- ESM 是靜態(tài)的,代碼發(fā)生在編譯時
- ESM 導(dǎo)出是引用值之前都存在映射關(guān)系,并且值都是可讀的,不能修改 參考聊聊什么是 CommonJs 和 Es Module 及它們的區(qū)別[1]
esm 是 JavaScript 模塊化的未來。因為它解決了變量污染,代碼維護,代碼依賴的問題。它讓你的代碼更加科學(xué)。這也是 deno 默認采用 esm 的原因。
回歸正題,我們有什么方法在 Nodejs 或者瀏覽器直接運行 esm 代碼,這是個有趣而又實際的問題。
如何在 Nodejs 環(huán)境允許
1.利用編譯工具運行 esm
最常見的方式是利用webpack[2]等打包工具搭配 babel 使用。隨著 webpack 和 vue 的大熱,這些工具似乎成為了標配,但是 webpack 的缺點也很明顯,它能讓 commonjs 和 esm 的混寫,導(dǎo)致代碼存在一些寫法不規(guī)范的情況,我相信這種情況是普遍出現(xiàn)在業(yè)務(wù)代碼里面,也存在于 antd3 這樣的知名第三方組件庫中。
而rollup[3]則是基于 ES6 的語法規(guī)范進行編譯,它的輕便小巧,非常適合 npm 庫的打包。新興的打包工具例如esbuild[4]和swc[5],也可以實現(xiàn)編譯打包,即使速度越來越快,但是還是需要編譯的過程。這些倉庫很重要的一個特點就是使用 esm 語法。
以上這些工具都可以應(yīng)用于 esm 語法編譯,但是有很多項目不一定需要打包編譯這樣耗時的流程的,例如一些 cli 工具、簡易微服務(wù)等,如何保證高效正確的運行 esm 代碼呢?
2. 利用第三方庫運行 esm
在 Nodejs 版本較低的情況,我們可以利用一些工具,工具的使用形式有幾種,一種是 Module Loader,另一種是 Command Line(簡稱為 cli)。
Module Loader,這里介紹standard-things/esm[6],它可以 preload 第三方提供的 esm 包,從此,可以做到 babelless, bundleless。你不需要使用大型編譯工具也可以直接運行 esm 代碼,使用方式如下。
node -r esm index.js
同樣,egoist/esbuild-register[7]這個庫在 esbuild 的支持下,同樣可以做到 Module Loader 的效果,利用 esbuild 的高性能特性,代碼運行效率更高。
node -r esbuild-register index.js
Command Line,基于封裝后的 cli,不過是換一種形式進行模塊的提前處理。babel-node[8]直接利用它的 babel 語法優(yōu)勢來運行 esm 代碼。由于 babel 本身還是 js 的實現(xiàn),它的官方文檔也表明了不建議在生產(chǎn)環(huán)境使用,會導(dǎo)致內(nèi)存高占用的問題,這也是這一類工具的通病。
babel-node index.js
同樣,esno[9]可以直接在命令行運行 esm 代碼。原理基于 esbuild。在這里更推薦使用這種方式,鑒于 esbuild 是由 go 語言實現(xiàn),能夠較大程度解決內(nèi)存高占用的問題,保證了一定的執(zhí)行性能。
esno index.ts
esmo index.ts
這一類第三方倉庫適合在低版本 nodejs 且非生產(chǎn)環(huán)境使用,它們的存在是為了便利性,而并非實用性和穩(wěn)定性。怎么樣才能高效地運行 esm 代碼?
3.Native Nodejs 運行 esm
Node verison 13.2.0 起開始正式支持 ES Modules 特性
所以利用 Native Nodejs 環(huán)境運行 esm 代碼是非常必要的,高版本的 Nodejs 提供了直接運行 esm 的功能,這里建議使用 lts14 版本。有兩種方式運行 esm 代碼:
第一種,package.json 中填寫 type: "modules",表明模塊的類型。此后,直接運行node index.js即可。
// pakage.json
{
...
"type": "modules"
}
第二種,則是將文件名改成.mjs,標明該文件是 esm 代碼。這兩種方式最大的區(qū)別則是模塊作用域。前者是包的作用域,它的聲明是以 package 為維度。后者則是以文件為維度,不受限于包的作用域。
如何在瀏覽器運行 esm

瀏覽器的情況有別于 Nodejs 環(huán)境,在大部分的新版本瀏覽器都支持 esm 的運行。esm 級別的代碼編譯和打包,可以有效地減少包的體積和資源傳輸速度。這也是為什么像 vite 這樣的框架會采用現(xiàn)代瀏覽器的打包模式(外加 legacy 兼容模式)的原因。具體的原理是在 html 當中的 script 標簽加入type="module" 則表明它引入的是 esm 代碼,當舊瀏覽器沒法支持 esm 的情況下,它會讀取nomodule script中的地址,讀取兼容版本的 js 代碼。這樣一來,可以有效地減少大部分瀏覽器加載的 js 體積,又保證了老瀏覽器的兼容性問題。
<script type="module" src="dist/index.js">script>
<script nomodule src="dist/index.legacy.js">script>
總結(jié)
如今 Nodejs 和瀏覽器環(huán)境都能對 esm 語法有了很好的 Native 支持。作為前端工程師的我們,應(yīng)該要保持著技術(shù)的前瞻性,在寫一個倉庫的時候,我們要想到要用 typescript,esm 還是 common.js 呢?為什么我們不選擇比較新的 js 運行環(huán)境,迎接 Javascript 的第三個時代,參考《ESM Import 與 Bundleless》[10]。
參考資料
2020 年我們可以在 Node 中使用 ES Modules 了嗎[11]
參考資料
[1]聊聊什么是 CommonJs 和 Es Module 及它們的區(qū)別: https://juejin.cn/post/6938581764432461854
[2]webpack: https://webpack.js.org/
[3]rollup: https://github.com/rollup/rollup
[4]esbuild: https://github.com/evanw/esbuild
[5]swc: https://github.com/swc-project/swc
[6]standard-things/esm: https://github.com/standard-things/esm
[7]egoist/esbuild-register: https://github.com/egoist/esbuild-register
[8]babel-node: https://babeljs.io/docs/en/babel-node
[9]esno: https://github.com/antfu/esno
[10]《ESM Import 與 Bundleless》: https://www.jianshu.com/p/ab0d5cc9b062
[11]2020 年我們可以在 Node 中使用 ES Modules 了嗎: https://zhuanlan.zhihu.com/p/337796076
