比較 commonjs 和 ESM 模塊使用方式

比較 commonjs 和 ESM 模塊使用方式

commonjs

commonjs 中有幾個特別的變量,module、exports、require、global、__filename、__dirname。

在一個 js 文件中輸入下面這行,你會發現可以打印出 5 個 argument。

console.log(arguments);

它們分別對應的就是:exports、require、module、__filename、__dirname。

commonjs 的導出機制比較簡單,只有 module.exports 和 exports。需要注意的是,他們指向的都是同一個對象。如果對 module.exports 賦值,則通過 exports.xxx 導出的所有變量都會失效,它所指向的對象和 module.exports 指向的不是同一個對象,而導出時是以 module.exports 指向的對象為準。

require 是動態導入模塊的,只有執行到 require 語句的時候才會導入模塊,它導入的是模塊的一個拷貝。模塊導入一次之後就會被緩存起來,後面再導入時都是使用的已緩存的版本。

舉個簡單例子,下面代碼會先打印 index 再打印 a。如果把 require 換成 import,則會先打印 a 再打印 index。

// a.js
console.log('a')
// index.js
console.log('index')

require('./a')
node index.js

ESM

常見導入導出

ESM 全稱叫 ECMA Script Modules,是在 ES6 語言層面提出的一種模塊化標準。

ES6 中主要有 import、export 兩個關鍵詞。要注意他們是語法層面的關鍵詞,所以不能使用 console.log(import) 這種方式來打印。而 commonjs 中的變量都是可以打印的。

常見的導入導出:

// 導入
import React from 'react'
import * as React from 'react'
import { Component } from 'react'
import {default as a} from 'react'
import {Button as Btn} from 'antd'
import 'index.less'
export default 1
export const name = 'lxfriday'
export { age }
export {name as nickname}
export { foo } from './foo-bar'
export * from './foo-bar'

ES6 模塊在編譯階段就可以分析出導出的結果,同時它導出的是值的引用。

  • 編譯階段會導出意味著模塊的導入會先於正常的執行流執行,即使 import 導入語句是在正常語句之後(見 commonjs 的例子);
  • ES6 導出的是一個引用,所以在對原模塊做更改之後會影響新導入的值,導入時可以看做是從 getter 函數里面取值,獲取的都是原模塊內部的值;

下面是一個例子:

比較 commonjs 和 ESM 模塊使用方式

image

ESM 在瀏覽器中的應用

ESM 模塊語法可以在比較新的瀏覽器中使用,它的使用方式像下面的代碼,需要使用 type = module 區分。瀏覽器對>

瀏覽器對 module 標籤的腳本是異步加載並執行的,模塊的加載不會阻塞瀏覽器渲染,等到整個頁面渲染完成之後,module 才會執行,這種加載執行策略和 defer 屬性相同(async 是異步加載完就會立即執行,會中斷瀏覽器渲染)。

下面是一個例子

// cc.js
export default { aa: "aaa", bb: "bbb" };
var value = 100;
// index.js
import cc from "./cc.js";
console.log("module in browser", cc);
console.log(value);

hello


執行結果

比較 commonjs 和 ESM 模塊使用方式

image

可以看到,模塊正常導入並打印了,但是在 cc.js 中定義的變量 value,在 index.js 中獲取不到,這說明 type=module 有作用域限制。

commonjs 和 ESM 混合使用

當它們混合使用的時候很容易產生混亂,在導入的時候,推薦把 import 語句都放在 require 語句前面。在不同時期,由於語法標準不完善,導入導出都存在差異,下面例子基於 webpack 4.41.2 測試得出。

看看下面的例子:

// 對 import xx from './aa.js'
module.exports = xx
// 等同於
export default xx
// 對 import { aa, bb } from './cc.js' 或者 const { aa, bb } = require('./cc.js')
module.exports = { aa: 'aaa', bb: 'bbb' }
// 等同於
exports.aa = 'aaa'
exports.bb = 'bbb'
// 等同於
export const aa = 'aaa'
export const bb = 'bbb'

下面兩個例子要多加註意

// cc.js
module.exports = { aa: 'aaa', bb: 'bbb' }
// index.js
import cc from './cc.js'
import * as dd from './cc.js'

console.log(cc)
console.log(dd)
// 打印的結果相同,為 {aa: "aaa", bb: "bbb"}
// cc.js
export default { aa: 'aaa', bb: 'bbb' }
// index.js
import cc from './cc.js'
import * as dd from './cc.js'
console.log(cc)
console.log(dd)
// 打印的結果不同
// cc => {aa: "aaa", bb: "bbb"}
// dd => Module {default: {…}, __esModule: true, Symbol(Symbol.toStringTag): "Module"}

可以看出,使用 export default 這種 ES6 方式導出的結果,import * as 可以準確的實現自身的語義,把模塊裡面所有導出的都掛載模塊對象中。

ES6 模塊中,import a from './xx' 是 import { default as a } from './xx' 的簡寫。表示把 xx 模塊中用 export default 導出的變量導入。

參考

  • 阮一峰 module-loader


分享到:


相關文章: